216 lines
9.2 KiB
JavaScript
216 lines
9.2 KiB
JavaScript
#!/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 });
|
||
});
|
||
});
|