import { describe, it, expect, beforeEach, afterEach } from "bun:test"; import { mkdir, writeFile, rm } from "node:fs/promises"; import { join } from "node:path"; import { assembleDiscussPrompt, assemblePlanPrompt, assembleBuildPrompt, assembleArchivePrompt, } from "../../src/core/assembler.ts"; import type { RuneConfig } from "../../src/types.ts"; import { defaultConfig } from "../../src/defaults/config.ts"; const TMP_DIR = join(import.meta.dir, "__tmp_assembler_test__"); beforeEach(async () => { await mkdir(TMP_DIR, { recursive: true }); }); afterEach(async () => { await rm(TMP_DIR, { recursive: true, force: true }); }); describe("assembleDiscussPrompt", () => { it("返回默认 discuss 提示词", () => { const prompt = assembleDiscussPrompt(defaultConfig); expect(prompt).toBeTruthy(); expect(prompt).toContain("软件架构师"); }); it("返回自定义 discuss 提示词", () => { const config: RuneConfig = { stages: { discuss: { prompt: "自定义讨论" } }, }; const prompt = assembleDiscussPrompt(config); expect(prompt).toBe("自定义讨论"); }); }); describe("assemblePlanPrompt", () => { it("包含指定文档名称和提示词", async () => { const prompt = await assemblePlanPrompt( defaultConfig, TMP_DIR, "user-auth", "design", ); expect(prompt).toContain("user-auth"); expect(prompt).toContain("design"); expect(prompt).not.toContain("task"); }); it("包含已有文档内容(重复 plan 场景)", async () => { const changeDir = join(TMP_DIR, ".rune", "changes", "user-auth"); await mkdir(changeDir, { recursive: true }); await writeFile(join(changeDir, "design.md"), "# 已有设计"); const prompt = await assemblePlanPrompt( defaultConfig, TMP_DIR, "user-auth", "design", ); expect(prompt).toContain("已有设计"); expect(prompt).toContain("在此基础上修订"); }); it("替换模板中的 {{change-name}}", async () => { const prompt = await assemblePlanPrompt( defaultConfig, TMP_DIR, "user-auth", "design", ); expect(prompt).toContain("user-auth 设计文档"); expect(prompt).not.toContain("{{change-name}}"); }); it("包含依赖说明(有依赖时)", async () => { const config: RuneConfig = { stages: { plan: { documents: [ { name: "design", prompt: "生成设计" }, { name: "task", prompt: "生成任务", depend: ["design"] }, ], }, }, }; const prompt = await assemblePlanPrompt( config, TMP_DIR, "user-auth", "task", ); expect(prompt).toContain("依赖说明"); expect(prompt).toContain("design.md"); }); it("无依赖时不包含依赖说明", async () => { const prompt = await assemblePlanPrompt( defaultConfig, TMP_DIR, "user-auth", "design", ); expect(prompt).not.toContain("依赖说明"); }); it("依赖说明标注完成状态", async () => { const changeDir = join(TMP_DIR, ".rune", "changes", "user-auth"); await mkdir(changeDir, { recursive: true }); await writeFile(join(changeDir, "design.md"), "# 设计"); const config: RuneConfig = { stages: { plan: { documents: [ { name: "design", prompt: "生成设计" }, { name: "task", prompt: "生成任务", depend: ["design"] }, ], }, }, }; const prompt = await assemblePlanPrompt( config, TMP_DIR, "user-auth", "task", ); expect(prompt).toContain("已完成"); }); it("使用自定义 plan 配置", async () => { const config: RuneConfig = { stages: { plan: { documents: [ { name: "spec", prompt: "生成规格", template: "# {{change-name}} 规格", }, ], }, }, }; const prompt = await assemblePlanPrompt(config, TMP_DIR, "my-feature", "spec"); expect(prompt).toContain("spec"); expect(prompt).toContain("my-feature 规格"); expect(prompt).not.toContain("design"); }); }); describe("assembleBuildPrompt", () => { it("包含待执行任务列表", async () => { const changeDir = join(TMP_DIR, ".rune", "changes", "user-auth"); await mkdir(changeDir, { recursive: true }); await writeFile( join(changeDir, "task.md"), `- [x] 任务一\n- [ ] 任务二\n- [ ] 任务三`, ); const prompt = await assembleBuildPrompt( defaultConfig, TMP_DIR, "user-auth", ); expect(prompt).toContain("任务二"); expect(prompt).toContain("待执行任务"); expect(prompt).toContain("共 2 项"); }); it("所有任务完成时提示可归档", async () => { const changeDir = join(TMP_DIR, ".rune", "changes", "user-auth"); await mkdir(changeDir, { recursive: true }); await writeFile( join(changeDir, "task.md"), `- [x] 任务一\n- [x] 任务二`, ); const prompt = await assembleBuildPrompt( defaultConfig, TMP_DIR, "user-auth", ); expect(prompt).toContain("已完成"); expect(prompt).toContain("归档"); }); it("task.md 不存在时抛出 CommandError 并附带提示", async () => { try { await assembleBuildPrompt(defaultConfig, TMP_DIR, "nonexistent"); expect.unreachable(); } catch (e: any) { expect(e.message).toContain("尚未完成规划"); expect(e.message).toContain("nonexistent"); expect(e.hint).toContain("rune plan"); } }); }); describe("assembleArchivePrompt", () => { it("返回归档提示词", async () => { const changeDir = join(TMP_DIR, ".rune", "changes", "user-auth"); await mkdir(changeDir, { recursive: true }); const prompt = await assembleArchivePrompt(defaultConfig, TMP_DIR, "user-auth"); expect(prompt).toContain("user-auth"); expect(prompt).toContain("归档"); }); });