diff --git a/tests/integration/flow.test.ts b/tests/integration/flow.test.ts new file mode 100644 index 0000000..a050546 --- /dev/null +++ b/tests/integration/flow.test.ts @@ -0,0 +1,129 @@ +import { describe, it, expect, beforeEach, afterEach } from "bun:test"; +import { existsSync } from "node:fs"; +import { mkdir, writeFile, rm, readFile, rename } from "node:fs/promises"; +import { join } from "node:path"; +import { runInit } from "../../src/commands/init.ts"; +import { loadConfig, getChangeDir, getArchiveDir } from "../../src/core/config.ts"; +import { + assembleDiscussPrompt, + assemblePlanPrompt, + assembleBuildPrompt, + assembleArchivePrompt, +} from "../../src/core/assembler.ts"; +import { scanChanges, scanArchives } from "../../src/core/scanner.ts"; + +const TMP_DIR = join(import.meta.dir, "__tmp_flow_test__"); + +beforeEach(async () => { + await mkdir(TMP_DIR, { recursive: true }); +}); + +afterEach(async () => { + await rm(TMP_DIR, { recursive: true, force: true }); +}); + +describe("完整 SDD 流程", () => { + it("init → discuss → plan → build → archive 完整流程", async () => { + await runInit(TMP_DIR, ["opencode"]); + expect(existsSync(join(TMP_DIR, ".rune", "config.yaml"))).toBe(true); + expect(existsSync(join(TMP_DIR, ".opencode", "commands", "discuss.md"))).toBe(true); + + const config = await loadConfig(TMP_DIR); + const discussPrompt = assembleDiscussPrompt(config); + expect(discussPrompt).toContain("软件架构师"); + + const changeName = "user-auth"; + await mkdir(getChangeDir(TMP_DIR, changeName), { recursive: true }); + const planPrompt = await assemblePlanPrompt(config, TMP_DIR, changeName); + expect(planPrompt).toContain("user-auth"); + + const changeDir = getChangeDir(TMP_DIR, changeName); + await writeFile(join(changeDir, "design.md"), "# 用户认证设计\n\n## 背景\n需要用户登录功能"); + await writeFile(join(changeDir, "task.md"), "- [ ] 实现登录 API\n- [ ] 编写登录测试"); + + const changes = await scanChanges(TMP_DIR); + expect(changes).toHaveLength(1); + expect(changes[0].name).toBe("user-auth"); + expect(changes[0].taskProgress).toEqual({ completed: 0, total: 2 }); + + const buildPrompt = await assembleBuildPrompt(config, TMP_DIR, changeName); + expect(buildPrompt).toContain("实现登录 API"); + expect(buildPrompt).toContain("共 2 项"); + + await writeFile(join(changeDir, "task.md"), "- [x] 实现登录 API\n- [x] 编写登录测试"); + + const updatedChanges = await scanChanges(TMP_DIR); + expect(updatedChanges[0].taskProgress).toEqual({ completed: 2, total: 2 }); + + const buildPrompt2 = await assembleBuildPrompt(config, TMP_DIR, changeName); + expect(buildPrompt2).toContain("已完成"); + + const archivePrompt = assembleArchivePrompt(config, changeName); + expect(archivePrompt).toContain("归档"); + const today = new Date().toISOString().slice(0, 10); + const src = getChangeDir(TMP_DIR, changeName); + const dest = join(getArchiveDir(TMP_DIR), `${today}-${changeName}`); + await mkdir(join(TMP_DIR, ".rune", "archive"), { recursive: true }); + await rename(src, dest); + + expect(existsSync(dest)).toBe(true); + expect(existsSync(src)).toBe(false); + + const archives = await scanArchives(TMP_DIR); + expect(archives).toContain(`${today}-${changeName}`); + + const postArchiveChanges = await scanChanges(TMP_DIR); + expect(postArchiveChanges).toHaveLength(0); + }); + + it("多变更并行", async () => { + await runInit(TMP_DIR, ["opencode"]); + const config = await loadConfig(TMP_DIR); + + for (const name of ["auth", "payment"]) { + const changeDir = getChangeDir(TMP_DIR, name); + await mkdir(changeDir, { recursive: true }); + await writeFile(join(changeDir, "task.md"), `- [ ] ${name} 任务`); + } + + const changes = await scanChanges(TMP_DIR); + expect(changes).toHaveLength(2); + + const authPrompt = await assembleBuildPrompt(config, TMP_DIR, "auth"); + expect(authPrompt).toContain("auth 任务"); + + const paymentPrompt = await assembleBuildPrompt(config, TMP_DIR, "payment"); + expect(paymentPrompt).toContain("payment 任务"); + }); + + it("自定义配置覆盖默认配置", async () => { + await runInit(TMP_DIR, ["opencode"]); + + await writeFile( + join(TMP_DIR, ".rune", "config.yaml"), + `stages: + discuss: + prompt: 自定义讨论 + plan: + documents: + - name: spec + prompt: 生成规格文档 + template: "# {{change-name}} 规格" +`, + ); + + const config = await loadConfig(TMP_DIR); + + const discussPrompt = assembleDiscussPrompt(config); + expect(discussPrompt).toBe("自定义讨论"); + + await mkdir(getChangeDir(TMP_DIR, "test"), { recursive: true }); + const planPrompt = await assemblePlanPrompt(config, TMP_DIR, "test"); + expect(planPrompt).toContain("spec"); + expect(planPrompt).toContain("test 规格"); + expect(planPrompt).not.toContain("design"); + + expect(config.stages.build).toBeDefined(); + expect(config.stages.archive).toBeDefined(); + }); +});