import { describe, it, expect, beforeEach, afterEach } from "bun:test"; import { createRunner } from "./tier1-command.ts"; import { setupTempDir, cleanupTempDir, getTempDir, createFreshProject, changeFileExists, } from "./fixtures.ts"; import { assertDocCreated, assertDocContains, assertConfigInvalid } from "./validators.ts"; import { scanChanges } from "../../src/core/scanner.ts"; import type { RuneConfig } from "../../src/types.ts"; describe("e2e: plan 阶段", () => { let runner: ReturnType; beforeEach(async () => { await setupTempDir(); runner = createRunner(); }); afterEach(async () => { await cleanupTempDir(); }); it("单文档按模板生成在正确路径", async () => { const config = await createFreshProject(); const result = await runner.runPlan(getTempDir(), "user-auth", "design", config); expect(result.files).toContain("design.md"); assertDocCreated(getTempDir(), "user-auth", "design"); assertDocContains(getTempDir(), "user-auth", "design", "user-auth"); }); it("多文档无依赖时均生成", async () => { const config = await createFreshProject(); await runner.runPlan(getTempDir(), "my-change", "design", config); await runner.runPlan(getTempDir(), "my-change", "task", config); expect(changeFileExists("my-change", "design.md")).toBe(true); expect(changeFileExists("my-change", "task.md")).toBe(true); const changes = await scanChanges(getTempDir(), config); expect(changes).toHaveLength(1); expect(changes[0]!.planCompleted).toBe(true); expect(changes[0]!.buildUnlocked).toBe(true); }); it("模板变量 {{change-name}} 被替换", async () => { const config = await createFreshProject(); await runner.runPlan(getTempDir(), "用户-login", "design", config); assertDocContains(getTempDir(), "用户-login", "design", "用户-login 设计文档"); }); it("使用自定义 plan 配置(单文档 spec)", async () => { const config: RuneConfig = { stages: { plan: { documents: [{ name: "spec", prompt: "生成规格", template: "# {{change-name}} 规格\n" }], }, }, }; await createFreshProject(["opencode"]); const result = await runner.runPlan(getTempDir(), "my-feature", "spec", config); expect(result.files).toContain("spec.md"); assertDocContains(getTempDir(), "my-feature", "spec", "my-feature 规格"); }); it("多文档有依赖时 planCompleted 全完成后才为 true", async () => { const config = await createFreshProject(); await runner.runPlan(getTempDir(), "dep-test", "design", config); let changes = await scanChanges(getTempDir(), config); expect(changes[0]!.planCompleted).toBe(false); await runner.runPlan(getTempDir(), "dep-test", "task", config); changes = await scanChanges(getTempDir(), config); expect(changes[0]!.planCompleted).toBe(true); expect(changes[0]!.buildUnlocked).toBe(true); }); it("已有变更再次 plan 另一个文档(重复 plan)", async () => { const config: RuneConfig = { stages: { plan: { documents: [ { name: "design", prompt: "生成设计", template: "# 设计 #\n" }, { name: "task", prompt: "生成任务", template: "# 任务 #\n" }, ], }, }, }; await runner.runPlan(getTempDir(), "multi", "design", config); assertDocCreated(getTempDir(), "multi", "design"); const result = await runner.runPlan(getTempDir(), "multi", "task", config); expect(result.files).toContain("task.md"); const changes = await scanChanges(getTempDir(), config); expect(changes[0]!.planCompleted).toBe(true); }); it("循环依赖配置被校验拒绝", () => { const config: RuneConfig = { stages: { plan: { documents: [ { name: "a", prompt: "a", depend: ["b"] }, { name: "b", prompt: "b", depend: ["a"] }, ], }, }, }; assertConfigInvalid(config); }); it("自依赖配置被校验拒绝", () => { const config: RuneConfig = { stages: { plan: { documents: [{ name: "design", prompt: "设计", depend: ["design"] }], }, }, }; assertConfigInvalid(config); }); });