#!/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 }); }); });