import { describe, it, expect, beforeEach, afterEach } from "bun:test"; import { createRunner } from "./agent-mock.ts"; import { ScenarioRunner, type BuildOverride } from "./agent-scenario.ts"; import { setupTempDir, cleanupTempDir, getTempDir, createFreshProject, writeDoc, } from "./fixtures.ts"; import { scanChanges } from "../../src/core/scanner.ts"; import { validateConfig } from "../../src/core/config.ts"; import { ConfigError } from "../../src/cli/errors.ts"; import type { RuneConfig } from "../../src/types.ts"; describe("e2e: 文档依赖", () => { let runner: ReturnType; beforeEach(async () => { await setupTempDir(); runner = createRunner(); }); afterEach(async () => { await cleanupTempDir(); }); it("依赖文档按顺序创建(A → B → C 链式依赖)", async () => { const config: RuneConfig = { stages: { plan: { documents: [ { name: "a", prompt: "文档 A" }, { name: "b", prompt: "文档 B", depend: ["a"] }, { name: "c", prompt: "文档 C", depend: ["b"] }, ], }, }, }; await createFreshProject(); await runner.runPlan(getTempDir(), "chain", "a", config); let changes = await scanChanges(getTempDir(), config); expect(changes).toHaveLength(1); const chain = changes[0]!; const docsA = chain.documents; const aDoc = docsA.find((d) => d.name === "a")!; const bDoc = docsA.find((d) => d.name === "b")!; const cDoc = docsA.find((d) => d.name === "c")!; expect(aDoc.completed).toBe(true); expect(aDoc.dependMet).toBe(true); expect(bDoc.completed).toBe(false); // a.md 已存在,所以 b 的依赖已满足 expect(bDoc.dependMet).toBe(true); expect(cDoc.completed).toBe(false); // c 依赖 b,b.md 不存在,所以 dependMet=false expect(cDoc.dependMet).toBe(false); expect(chain.planCompleted).toBe(false); await runner.runPlan(getTempDir(), "chain", "b", config); changes = await scanChanges(getTempDir(), config); const chain2 = changes[0]!; const docsB = chain2.documents; const bDoc2 = docsB.find((d) => d.name === "b")!; const cDoc2 = docsB.find((d) => d.name === "c")!; expect(bDoc2.completed).toBe(true); expect(bDoc2.dependMet).toBe(true); expect(cDoc2.completed).toBe(false); // b.md 已存在,c 的依赖现在满足 expect(cDoc2.dependMet).toBe(true); expect(chain2.planCompleted).toBe(false); await runner.runPlan(getTempDir(), "chain", "c", config); changes = await scanChanges(getTempDir(), config); const chain3 = changes[0]!; const docsC = chain3.documents; expect(docsC.every((d) => d.completed)).toBe(true); expect(docsC.every((d) => d.dependMet)).toBe(true); expect(chain3.planCompleted).toBe(true); expect(chain3.buildUnlocked).toBe(true); }); it("引用不存在文档的依赖被校验拒绝", () => { const config: RuneConfig = { stages: { plan: { documents: [{ name: "design", prompt: "设计", depend: ["ghost"] }], }, }, }; try { validateConfig(config); // 不应该走到这里 expect(true).toBe(false); } catch (e) { expect(e).toBeInstanceOf(ConfigError); expect((e as ConfigError).message).toContain("ghost"); } }); it("依赖链断开时 planCompleted 仍为 false", async () => { const config: RuneConfig = { stages: { plan: { documents: [ { name: "design", prompt: "设计" }, { name: "task", prompt: "任务", depend: ["design"] }, ], }, }, }; await createFreshProject(); await writeDoc("broken", "task", "# 任务\n"); const changes = await scanChanges(getTempDir(), config); expect(changes).toHaveLength(1); const broken = changes[0]!; const taskDoc = broken.documents.find((d) => d.name === "task")!; const designDoc = broken.documents.find((d) => d.name === "design")!; expect(taskDoc.completed).toBe(true); expect(taskDoc.dependMet).toBe(false); expect(designDoc.completed).toBe(false); expect(broken.planCompleted).toBe(false); expect(broken.buildUnlocked).toBe(false); }); it("依赖满足后才允许 build", async () => { const config: RuneConfig = { stages: { plan: { documents: [ { name: "design", prompt: "设计" }, { name: "task", prompt: "任务", depend: ["design"] }, ], }, }, }; await createFreshProject(); const baseRunner = createRunner(); const buildWithDependCheck: BuildOverride = async (projectDir, changeName, cfg) => { const changes = await scanChanges(projectDir, cfg); const change = changes.find((c) => c.name === changeName); if (!change) throw new Error(`变更 "${changeName}" 不存在`); if (!change.planCompleted) { throw new Error(`变更 "${changeName}" 的 plan 阶段未完成`); } return baseRunner.runBuild(projectDir, changeName, cfg); }; const scenarioRunner = new ScenarioRunner(baseRunner, { build: buildWithDependCheck }); await writeDoc("build-dep", "task", "# 任务\n- [ ] do something\n"); // 依赖未满足时 build 应抛出错误 await expect(scenarioRunner.runBuild(getTempDir(), "build-dep", config)).rejects.toThrow( "plan 阶段未完成", ); await runner.runPlan(getTempDir(), "build-dep", "design", config); const changesAfter = await scanChanges(getTempDir(), config); expect(changesAfter[0]!.planCompleted).toBe(true); // 依赖满足后 build 应正常执行 await expect(scenarioRunner.runBuild(getTempDir(), "build-dep", config)).resolves.toBeDefined(); }); });