import { describe, it, expect, beforeEach, afterEach } from "bun:test"; import { createRunner } from "./agent-mock.ts"; import { setupTempDir, cleanupTempDir, getTempDir, createFreshProject, changeFileExists, } from "./fixtures.ts"; import { assertAllTasksDone } from "./validators.ts"; import { scanChanges, scanArchives } from "../../src/core/scanner.ts"; import { assembleDiscussPrompt } from "../../src/core/assembler.ts"; import { validateChangeName } from "../../src/cli.ts"; describe("e2e: 全流程", () => { let runner: ReturnType; beforeEach(async () => { await setupTempDir(); runner = createRunner(); }); afterEach(async () => { await cleanupTempDir(); }); it("完整四阶段流程(discuss → plan → build → archive)", async () => { const config = await createFreshProject(); const discussPrompt = assembleDiscussPrompt(config); expect(typeof discussPrompt).toBe("string"); expect(discussPrompt.length).toBeGreaterThan(0); await runner.runPlan(getTempDir(), "full-flow", "design", config); await runner.runPlan(getTempDir(), "full-flow", "task", config); let changes = await scanChanges(getTempDir(), config); expect(changes).toHaveLength(1); expect(changes[0]!.name).toBe("full-flow"); expect(changes[0]!.planCompleted).toBe(true); expect(changes[0]!.buildUnlocked).toBe(true); const buildResult = await runner.runBuild(getTempDir(), "full-flow", config); expect(buildResult.files.length).toBeGreaterThan(0); changes = await scanChanges(getTempDir(), config); expect(changes).toHaveLength(1); assertAllTasksDone(changes[0]!); await runner.runArchive(getTempDir(), "full-flow", config); expect(changeFileExists("full-flow", "task.md")).toBe(false); const archives = await scanArchives(getTempDir()); expect(archives.length).toBeGreaterThanOrEqual(1); changes = await scanChanges(getTempDir(), config); expect(changes.find((c) => c.name === "full-flow")).toBeUndefined(); }); it("多变更并行互不干扰", async () => { const config = await createFreshProject(); await runner.runPlan(getTempDir(), "变更A", "design", config); await runner.runPlan(getTempDir(), "变更A", "task", config); await runner.runPlan(getTempDir(), "变更B", "design", config); await runner.runPlan(getTempDir(), "变更B", "task", config); let changes = await scanChanges(getTempDir(), config); expect(changes).toHaveLength(2); const changeA = changes.find((c) => c.name === "变更A"); const changeB = changes.find((c) => c.name === "变更B"); expect(changeA).toBeDefined(); expect(changeB).toBeDefined(); expect(changeA!.planCompleted).toBe(true); expect(changeB!.planCompleted).toBe(true); await runner.runBuild(getTempDir(), "变更A", config); await runner.runBuild(getTempDir(), "变更B", config); changes = await scanChanges(getTempDir(), config); expect(changes).toHaveLength(2); assertAllTasksDone(changes.find((c) => c.name === "变更A")!); assertAllTasksDone(changes.find((c) => c.name === "变更B")!); expect(changes[0]!.taskProgress?.completed).toBe(changes[0]!.taskProgress?.total); expect(changes[1]!.taskProgress?.completed).toBe(changes[1]!.taskProgress?.total); }); describe("变更名非法字符拒绝", () => { it("空字符串抛出错误", () => { expect(() => validateChangeName("")).toThrow(); }); it("包含 / 抛出错误", () => { expect(() => validateChangeName("变更/A")).toThrow(); }); it("包含 . 抛出错误", () => { expect(() => validateChangeName("变更.A")).toThrow(); }); it("合法中文名称不抛出错误", () => { expect(() => validateChangeName("变更名")).not.toThrow(); }); it("合法英文名称不抛出错误", () => { expect(() => validateChangeName("my-change")).not.toThrow(); }); it("合法短横线名称不抛出错误", () => { expect(() => validateChangeName("abc-def-xyz")).not.toThrow(); }); }); });