diff --git a/src/core/assembler.ts b/src/core/assembler.ts index eba93f8..b62e322 100644 --- a/src/core/assembler.ts +++ b/src/core/assembler.ts @@ -16,6 +16,15 @@ export function assembleDiscussPrompt(config: RuneConfig): string { return applyCommandPrefix(discuss.prompt, config); } +export function assembleCreatePrompt(config: RuneConfig): string { + const create = config.stages.create; + if (!create) + throw new CommandError("创建阶段未配置", { + hint: "请在 .rune/config.yaml 中配置 stages.create", + }); + return applyCommandPrefix(create.prompt, config); +} + export async function assemblePlanPrompt( config: RuneConfig, projectRoot: string, @@ -45,8 +54,7 @@ export async function assemblePlanPrompt( const docPath = join(changeDir, `${doc.name}.md`); if (existsSync(docPath)) { - const existing = await readFile(docPath, "utf-8"); - parts.push(`\n### 已有内容(请在此基础上修订):\n${existing}`); + parts.push(`\n文档已存在,请先读取 ${docPath} 查看已有内容,在此基础上修订。`); } if (doc.template) { @@ -114,13 +122,9 @@ export async function assembleBuildPrompt( const parts: string[] = []; parts.push(`# 构建阶段:${changeName}\n`); parts.push(build.prompt); - parts.push(`\n## 任务列表\n`); - parts.push(taskContent); - parts.push(`\n## 待执行任务(共 ${pendingTasks.length} 项)`); - for (const task of pendingTasks) { - parts.push(`- [ ] ${task.text}`); - } - parts.push(`\n请从第一个待执行任务开始。完成后更新 ${taskPath} 中的 checkbox。`); + parts.push(`\n请先读取 ${taskPath} 查看任务列表。`); + parts.push(`当前有 ${pendingTasks.length} 个待执行任务,从第一个未完成的任务开始。`); + parts.push(`完成后更新 ${taskPath} 中的 checkbox。`); return applyCommandPrefix(parts.join("\n"), config); } @@ -142,23 +146,21 @@ export async function assembleArchivePrompt( if (config.metadata?.tracked) { const changeDir = getChangeDir(projectRoot, changeName); const taskPath = join(changeDir, "task.md"); - try { - const taskContent = await readFile(taskPath, "utf-8"); - const tasks = parseTasks(taskContent); - const incompleteTasks = tasks.filter((t) => !t.checked); - if (incompleteTasks.length > 0) { - parts.push("## ⚠️ 警告:存在未完成的任务\n"); - parts.push(`以下 ${incompleteTasks.length} 个任务尚未完成:`); - for (const t of incompleteTasks) { - parts.push(`- [ ] ${t.text}`); + if (existsSync(taskPath)) { + try { + const taskContent = await readFile(taskPath, "utf-8"); + const tasks = parseTasks(taskContent); + const incompleteTasks = tasks.filter((t) => !t.checked); + if (incompleteTasks.length > 0) { + parts.push("## ⚠️ 警告:存在未完成的任务\n"); + parts.push(`请先读取 ${taskPath} 检查是否有未完成的任务。`); + parts.push("如有未完成任务,询问用户是否确认在任务未全部完成的情况下归档。"); + parts.push("如用户确认,则继续归档;否则中止并返回构建阶段。"); + parts.push(""); } - parts.push(""); - parts.push("请询问用户是否确认在任务未全部完成的情况下归档。"); - parts.push("如用户确认,则继续归档;否则中止并返回构建阶段。"); - parts.push(""); + } catch { + // task.md 读取失败时不追加警告 } - } catch { - // task.md 不存在时不追加警告 } } diff --git a/tests/core/assembler.test.ts b/tests/core/assembler.test.ts index 2b56b01..a713fdc 100644 --- a/tests/core/assembler.test.ts +++ b/tests/core/assembler.test.ts @@ -3,12 +3,14 @@ import { mkdir, writeFile, rm } from "node:fs/promises"; import { join } from "node:path"; import { assembleDiscussPrompt, + assembleCreatePrompt, assemblePlanPrompt, assembleBuildPrompt, assembleArchivePrompt, } from "../../src/core/assembler.ts"; import type { RuneConfig } from "../../src/types.ts"; import { defaultConfig } from "../../src/defaults/config.ts"; +import { CommandError } from "../../src/cli/errors.ts"; const TMP_DIR = join(import.meta.dir, "__tmp_assembler_test__"); @@ -38,6 +40,35 @@ describe("assembleDiscussPrompt", () => { }); }); +describe("assembleCreatePrompt", () => { + it("返回默认 create 提示词", () => { + const prompt = assembleCreatePrompt(defaultConfig); + expect(prompt).toBeTruthy(); + expect(prompt).toContain("变更名称"); + expect(prompt).toContain("/rune-plan"); + }); + + it("返回自定义 create 提示词", () => { + const config: RuneConfig = { + stages: { create: { prompt: "自定义创建" } }, + }; + const prompt = assembleCreatePrompt(config); + expect(prompt).toBe("自定义创建"); + }); + + it("create 阶段未配置时抛出 CommandError", () => { + const config: RuneConfig = { + stages: { build: { prompt: "构建" } }, + }; + try { + assembleCreatePrompt(config); + expect.unreachable(); + } catch (e) { + expect(e).toBeInstanceOf(CommandError); + } + }); +}); + describe("assemblePlanPrompt", () => { it("包含指定文档名称和提示词", async () => { const prompt = await assemblePlanPrompt(defaultConfig, TMP_DIR, "user-auth", "design"); @@ -46,13 +77,14 @@ describe("assemblePlanPrompt", () => { expect(prompt).not.toContain("task"); }); - it("包含已有文档内容(重复 plan 场景)", async () => { + it("已有文档时引导 AI 读取而非内嵌内容", 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("在此基础上修订"); + expect(prompt).toContain("已有内容"); + expect(prompt).toContain("design.md"); + expect(prompt).not.toContain("# 已有设计"); }); it("替换模板中的 {{change-name}}", async () => { @@ -123,14 +155,15 @@ describe("assemblePlanPrompt", () => { }); describe("assembleBuildPrompt", () => { - it("包含待执行任务列表", async () => { + it("引导 AI 读取 task.md 而非内嵌任务内容", 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 项"); + expect(prompt).toContain("task.md"); + expect(prompt).toContain("2"); + expect(prompt).not.toContain("任务二"); + expect(prompt).not.toContain("任务三"); }); it("所有任务完成时提示可归档", async () => { @@ -218,7 +251,7 @@ describe("assembleArchivePrompt", () => { expect(prompt).not.toContain("未完成"); }); - it("tracked=true 时读取 task.md 并注入未完成任务警告", async () => { + it("tracked=true 时引导 AI 检查 task.md 而非内嵌任务内容", async () => { const changeDir = join(TMP_DIR, ".rune", "changes", "user-auth"); await mkdir(changeDir, { recursive: true }); await writeFile(join(changeDir, "task.md"), "- [ ] 未完成任务"); @@ -227,8 +260,9 @@ describe("assembleArchivePrompt", () => { metadata: { tracked: true }, }; const prompt = await assembleArchivePrompt(config, TMP_DIR, "user-auth"); + expect(prompt).toContain("task.md"); expect(prompt).toContain("未完成"); - expect(prompt).toContain("未完成任务"); + expect(prompt).not.toContain("- [ ] 未完成任务"); }); it("tracked=true 且所有任务完成时不注入警告", async () => { diff --git a/tests/integration/flow.test.ts b/tests/integration/flow.test.ts index 47a2edc..3efbf55 100644 --- a/tests/integration/flow.test.ts +++ b/tests/integration/flow.test.ts @@ -50,8 +50,8 @@ describe("完整 SDD 流程", () => { expect(changes[0].documents.length).toBeGreaterThan(0); const buildPrompt = await assembleBuildPrompt(config, TMP_DIR, changeName); - expect(buildPrompt).toContain("实现登录 API"); - expect(buildPrompt).toContain("共 2 项"); + expect(buildPrompt).toContain("task.md"); + expect(buildPrompt).toContain("2"); await writeFile(join(changeDir, "task.md"), "- [x] 实现登录 API\n- [x] 编写登录测试"); @@ -95,10 +95,10 @@ describe("完整 SDD 流程", () => { expect(changes).toHaveLength(2); const authPrompt = await assembleBuildPrompt(config, TMP_DIR, "auth"); - expect(authPrompt).toContain("auth 任务"); + expect(authPrompt).toContain("task.md"); const paymentPrompt = await assembleBuildPrompt(config, TMP_DIR, "payment"); - expect(paymentPrompt).toContain("payment 任务"); + expect(paymentPrompt).toContain("task.md"); }); it("自定义配置覆盖默认配置", async () => { @@ -189,7 +189,9 @@ describe("archive 校验", () => { const prompt = await assembleArchivePrompt(config, TMP_DIR, "incomplete-task"); expect(prompt).toContain("警告"); - expect(prompt).toContain("未完成任务"); + expect(prompt).toContain("task.md"); + expect(prompt).toContain("未完成"); + expect(prompt).not.toContain("- [ ] 未完成任务"); expect(prompt).toContain("是否确认"); });