Files
16gagent/test/orchestrator.test.mjs
2026-06-06 10:40:48 +08:00

216 lines
9.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* Orchestrator Test Suite — V2 × Factory Fusion
*
* Tests the minimal orchestrator pipeline:
* - Empty input → error
* - Valid input → PRD + Arch + Full-stack generation
* - progress.log heartbeat entries are written
* - Task Tree + Plan structure are returned
* - Governance integration (heartbeat frequency, status flow)
*
* Run: node --test test/orchestrator.test.mjs
*/
import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
import { existsSync, readFileSync, rmSync, mkdirSync } from "node:fs";
import { resolve, join } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = resolve(fileURLToPath(new URL(".", import.meta.url)), "..");
const ROOT = __dirname;
const SCRIPTS = join(ROOT, "scripts");
let runOrchestrator;
// ═══════════════════════════════════════════════════════
// 1 — Empty / Invalid Input
// ═══════════════════════════════════════════════════════
describe("1 — Empty / Invalid Input", () => {
before(async () => {
const mod = await import(join(SCRIPTS, "orchestrator.mjs"));
runOrchestrator = mod.run;
});
it("空输入抛出 EMPTY_INPUT 错误", async () => {
try {
await runOrchestrator("");
assert.fail("Should have thrown");
} catch (e) {
assert.ok(e.message.includes("empty input"));
}
});
it("空格输入抛出 EMPTY_INPUT 错误", async () => {
try {
await runOrchestrator(" ");
assert.fail("Should have thrown");
} catch (e) {
assert.ok(e.message.includes("empty input"));
}
});
it("极简输入仍能生成完整项目", async () => {
const tmpDir = resolve(ROOT, ".tmp-orchestrator-test");
const result = await runOrchestrator("做一个宠物管理 App", { output: tmpDir });
assert.ok(result.summary.projectName);
assert.ok(result.summary.outputDir);
assert.ok(result.summary.stats.totalFiles > 50);
rmSync(tmpDir, { recursive: true, force: true });
});
});
// ═══════════════════════════════════════════════════════
// 2 — Task Tree + Plan Structure
// ═══════════════════════════════════════════════════════
describe("2 — Task Tree + Plan Structure", () => {
it("返回 plan 包含 goal / subtasks / risks", async () => {
const tmpDir = resolve(ROOT, ".tmp-orchestrator-plan-test");
const result = await runOrchestrator("做一个在线教育平台", { output: tmpDir });
assert.ok(result.plan.goal);
assert.ok(Array.isArray(result.plan.subtasks));
assert.ok(result.plan.subtasks.length === 4);
assert.ok(Array.isArray(result.plan.risks));
assert.ok(result.plan.heartbeat);
rmSync(tmpDir, { recursive: true, force: true });
});
it("返回 taskTree 包含 root + tasks", async () => {
const tmpDir = resolve(ROOT, ".tmp-orchestrator-tasktree-test");
const result = await runOrchestrator("做一个电商小程序", { output: tmpDir });
assert.ok(result.taskTree.root);
assert.equal(result.taskTree.root.id, "T0");
assert.ok(Array.isArray(result.taskTree.tasks));
assert.ok(result.taskTree.tasks.length === 4);
assert.ok(result.taskTree.injectedRisks.length > 0);
rmSync(tmpDir, { recursive: true, force: true });
});
});
// ═══════════════════════════════════════════════════════
// 3 — progress.log Heartbeat (Governance Integration)
// ═══════════════════════════════════════════════════════
describe("3 — progress.log Heartbeat", () => {
const LOG_PATH = join(ROOT, "progress.log");
before(async () => {
// Clean up
rmSync(LOG_PATH, { force: true });
});
it("progress.log 写入 5 条心跳", async () => {
const tmpDir = resolve(ROOT, ".tmp-orchestrator-heartbeat-test");
await runOrchestrator("做一个宠物管理 App", { output: tmpDir });
const content = readFileSync(LOG_PATH, "utf8");
const lines = content.trim().split("\n").filter(Boolean);
assert.equal(lines.length, 5);
// Parse all entries
const entries = lines.map(l => JSON.parse(l));
// First: IN_PROGRESS (NOT_STARTED phase)
assert.equal(entries[0].status, "IN_PROGRESS");
assert.deepStrictEqual(entries[0].completed, []);
// Middle: progressive completion
assert.equal(entries[1].status, "IN_PROGRESS");
assert.deepStrictEqual(entries[1].completed, ["T1"]);
assert.equal(entries[2].status, "IN_PROGRESS");
assert.deepStrictEqual(entries[2].completed, ["T1", "T2"]);
assert.equal(entries[3].status, "IN_PROGRESS");
assert.deepStrictEqual(entries[3].completed, ["T1", "T2", "T3"]);
// Final: ARCHIVED
assert.equal(entries[4].status, "ARCHIVED");
assert.deepStrictEqual(entries[4].completed, ["T1", "T2", "T3", "T4"]);
assert.deepStrictEqual(entries[4].remaining, []);
assert.equal(entries[4].next_action, "none");
rmSync(tmpDir, { recursive: true, force: true });
});
it("progress.log 每条心跳都有 ts/status/completed/remaining/next_action", async () => {
const tmpDir = resolve(ROOT, ".tmp-orchestrator-schema-test");
await runOrchestrator("做一个在线教育平台", { output: tmpDir });
const content = readFileSync(LOG_PATH, "utf8");
const lines = content.trim().split("\n").filter(Boolean);
for (const line of lines) {
const entry = JSON.parse(line);
assert.ok(entry.ts, "Missing ts");
assert.ok(entry.status, "Missing status");
assert.ok(Array.isArray(entry.completed), "Missing completed array");
assert.ok(Array.isArray(entry.remaining), "Missing remaining array");
assert.ok("next_action" in entry, "Missing next_action");
}
rmSync(tmpDir, { recursive: true, force: true });
});
});
// ═══════════════════════════════════════════════════════
// 4 — Pipeline Integration
// ═══════════════════════════════════════════════════════
describe("4 — Pipeline Integration", () => {
it("输出目录包含 apps/web/package.json 和 apps/api/package.json", async () => {
const tmpDir = resolve(ROOT, ".tmp-orchestrator-pipeline-test");
await runOrchestrator("做一个宠物管理 App", { output: tmpDir });
assert.ok(existsSync(join(tmpDir, "apps", "web", "package.json")), "Missing apps/web/package.json");
assert.ok(existsSync(join(tmpDir, "apps", "api", "package.json")), "Missing apps/api/package.json");
assert.ok(existsSync(join(tmpDir, "packages", "shared-types", "package.json")), "Missing shared-types/package.json");
assert.ok(existsSync(join(tmpDir, "package.json")), "Missing root package.json");
rmSync(tmpDir, { recursive: true, force: true });
});
it("多次运行不产生冲突(progress.log 追加)", async () => {
const tmpDir = resolve(ROOT, ".tmp-orchestrator-multi-test");
const logPath = join(ROOT, "progress.log");
// Reset log for this test
rmSync(logPath, { force: true });
// Run 1
await runOrchestrator("做一个宠物管理 App", { output: join(tmpDir, "v1") });
// Run 2
await runOrchestrator("做一个在线教育平台", { output: join(tmpDir, "v2") });
const content = readFileSync(logPath, "utf8");
const lines = content.trim().split("\n").filter(Boolean);
// 2 runs × 5 heartbeats = 10
assert.equal(lines.length, 10);
rmSync(tmpDir, { recursive: true, force: true });
});
});
// ═══════════════════════════════════════════════════════
// 5 — Summary Structure
// ═══════════════════════════════════════════════════════
describe("5 — Summary Structure", () => {
it("summary 包含所有必需字段", async () => {
const tmpDir = resolve(ROOT, ".tmp-orchestrator-summary-test");
const result = await runOrchestrator("做一个宠物管理 App", { output: tmpDir });
const s = result.summary;
assert.ok(s.projectName, "Missing projectName");
assert.ok(s.chineseName, "Missing chineseName");
assert.ok(s.domain, "Missing domain");
assert.ok(s.factory, "Missing factory");
assert.ok(s.outputDir, "Missing outputDir");
assert.ok(s.stats, "Missing stats");
assert.ok(s.startTs, "Missing startTs");
assert.ok(s.endTs, "Missing endTs");
assert.ok(typeof s.durationMs === "number", "Missing durationMs");
assert.ok(s.durationMs >= 0, "durationMs should be >= 0");
rmSync(tmpDir, { recursive: true, force: true });
});
});