From c45f6e1d4588a47f61266460f2b9bc2873ad943b Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Tue, 9 Jun 2026 12:36:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20archive=20=E9=98=B6=E6=AE=B5=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=20task=20=E5=AE=8C=E6=88=90=E7=8A=B6=E6=80=81?= =?UTF-8?q?=EF=BC=8C=E6=9C=AA=E5=AE=8C=E6=88=90=E6=97=B6=E6=B3=A8=E5=85=A5?= =?UTF-8?q?=E8=AD=A6=E5=91=8A=E6=8F=90=E7=A4=BA=E8=AF=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/cli.ts | 2 +- src/core/assembler.ts | 28 +++++++++++++++++++-- tests/core/assembler.test.ts | 6 +++-- tests/integration/flow.test.ts | 45 +++++++++++++++++++++++++++++++++- 4 files changed, 75 insertions(+), 6 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 952e8ae..750bf39 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -201,7 +201,7 @@ cli.command("archive ", "归档阶段").action( }); } const config = await loadConfig(root); - const prompt = assembleArchivePrompt(config, changeName); + const prompt = await assembleArchivePrompt(config, root, changeName); const today = new Date().toISOString().slice(0, 10); const src = changeDir; const dest = join(getArchiveDir(root), `${today}-${changeName}`); diff --git a/src/core/assembler.ts b/src/core/assembler.ts index 6a5647b..e2da0f8 100644 --- a/src/core/assembler.ts +++ b/src/core/assembler.ts @@ -111,15 +111,39 @@ export async function assembleBuildPrompt( return parts.join("\n"); } -export function assembleArchivePrompt( +export async function assembleArchivePrompt( config: RuneConfig, + projectRoot: string, changeName: string, -): string { +): Promise { const archive = config.stages.archive; if (!archive) throw new Error("archive 阶段未配置"); + const changeDir = getChangeDir(projectRoot, changeName); + const taskPath = join(changeDir, "task.md"); + const parts: string[] = []; parts.push(`# 归档阶段:${changeName}\n`); + + 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}`); + } + parts.push(""); + parts.push("请询问用户是否确认在任务未全部完成的情况下归档。"); + parts.push("如用户确认,则继续归档;否则中止并返回构建阶段。"); + parts.push(""); + } + } catch { + // task.md 不存在时不追加警告 + } + parts.push(archive.prompt); return parts.join("\n"); } diff --git a/tests/core/assembler.test.ts b/tests/core/assembler.test.ts index 45d5604..887348a 100644 --- a/tests/core/assembler.test.ts +++ b/tests/core/assembler.test.ts @@ -197,8 +197,10 @@ describe("assembleBuildPrompt", () => { }); describe("assembleArchivePrompt", () => { - it("返回归档提示词", () => { - const prompt = assembleArchivePrompt(defaultConfig, "user-auth"); + 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("归档"); }); diff --git a/tests/integration/flow.test.ts b/tests/integration/flow.test.ts index feceb79..79a9f20 100644 --- a/tests/integration/flow.test.ts +++ b/tests/integration/flow.test.ts @@ -63,7 +63,7 @@ describe("完整 SDD 流程", () => { const buildPrompt2 = await assembleBuildPrompt(config, TMP_DIR, changeName); expect(buildPrompt2).toContain("已完成"); - const archivePrompt = assembleArchivePrompt(config, changeName); + const archivePrompt = await assembleArchivePrompt(config, TMP_DIR, changeName); expect(archivePrompt).toContain("归档"); const today = new Date().toISOString().slice(0, 10); const src = getChangeDir(TMP_DIR, changeName); @@ -174,3 +174,46 @@ describe("变更名校验", () => { expect(validRegex.test("")).toBe(false); }); }); + +describe("archive 校验", () => { + it("task 未全部完成时注入警告提示词", async () => { + await runInit(TMP_DIR, ["opencode"]); + const config = await loadConfig(TMP_DIR); + + const changeDir = getChangeDir(TMP_DIR, "incomplete-task"); + await mkdir(changeDir, { recursive: true }); + await writeFile(join(changeDir, "design.md"), "# 设计"); + await writeFile(join(changeDir, "task.md"), "- [x] 已完成任务\n- [ ] 未完成任务"); + + const prompt = await assembleArchivePrompt(config, TMP_DIR, "incomplete-task"); + expect(prompt).toContain("警告"); + expect(prompt).toContain("未完成任务"); + expect(prompt).toContain("是否确认"); + }); + + it("task 全部完成时不注入警告", async () => { + await runInit(TMP_DIR, ["opencode"]); + const config = await loadConfig(TMP_DIR); + + const changeDir = getChangeDir(TMP_DIR, "complete-task"); + await mkdir(changeDir, { recursive: true }); + await writeFile(join(changeDir, "design.md"), "# 设计"); + await writeFile(join(changeDir, "task.md"), "- [x] 已完成任务"); + + const prompt = await assembleArchivePrompt(config, TMP_DIR, "complete-task"); + expect(prompt).not.toContain("警告"); + expect(prompt).toContain("归档阶段"); + }); + + it("task.md 不存在时不追加警告", async () => { + await runInit(TMP_DIR, ["opencode"]); + const config = await loadConfig(TMP_DIR); + + const changeDir = getChangeDir(TMP_DIR, "no-task"); + await mkdir(changeDir, { recursive: true }); + await writeFile(join(changeDir, "design.md"), "# 设计"); + + const prompt = await assembleArchivePrompt(config, TMP_DIR, "no-task"); + expect(prompt).not.toContain("警告"); + }); +});