feat: 将 task 从 plan 文档提升为独立 SDD 阶段
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
assemblePlanPrompt,
|
||||
assembleBuildPrompt,
|
||||
assembleArchivePrompt,
|
||||
assembleTaskPrompt,
|
||||
} from "../../src/core/assembler.ts";
|
||||
import type { RuneConfig } from "../../src/types.ts";
|
||||
import { defaultConfig } from "../../src/defaults/config.ts";
|
||||
@@ -150,28 +151,18 @@ describe("assembleBuildPrompt", () => {
|
||||
await assembleBuildPrompt(defaultConfig, TMP_DIR, "nonexistent");
|
||||
expect.unreachable();
|
||||
} catch (e: any) {
|
||||
expect(e.message).toContain("尚未完成规划");
|
||||
expect(e.message).toContain("尚未完成任务拆解");
|
||||
expect(e.message).toContain("nonexistent");
|
||||
expect(e.hint).toContain("plan nonexistent");
|
||||
expect(e.hint).toContain("task");
|
||||
}
|
||||
});
|
||||
|
||||
it("tracked=false 时只输出通用提示词", async () => {
|
||||
const config: RuneConfig = {
|
||||
stages: { build: { prompt: "按规划文档逐步实现功能" } },
|
||||
metadata: { tracked: false },
|
||||
};
|
||||
const prompt = await assembleBuildPrompt(config, TMP_DIR, "user-auth");
|
||||
expect(prompt).toBe("按规划文档逐步实现功能");
|
||||
});
|
||||
|
||||
it("tracked=true 且 task.md 格式不合法时抛错", async () => {
|
||||
it("task.md 格式不合法时抛错", async () => {
|
||||
const changeDir = join(TMP_DIR, ".rune", "changes", "user-auth");
|
||||
await mkdir(changeDir, { recursive: true });
|
||||
await writeFile(join(changeDir, "task.md"), "# 标题\n无 checkbox");
|
||||
const config: RuneConfig = {
|
||||
stages: { build: { prompt: "构建阶段" } },
|
||||
metadata: { tracked: true },
|
||||
};
|
||||
try {
|
||||
await assembleBuildPrompt(config, TMP_DIR, "user-auth");
|
||||
@@ -183,13 +174,12 @@ describe("assembleBuildPrompt", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("tracked=true 且 task.md 有空 checkbox 文本时抛错", async () => {
|
||||
it("task.md 有空 checkbox 文本时抛错", async () => {
|
||||
const changeDir = join(TMP_DIR, ".rune", "changes", "user-auth");
|
||||
await mkdir(changeDir, { recursive: true });
|
||||
await writeFile(join(changeDir, "task.md"), "- [ ] \n- [x] 有内容");
|
||||
const config: RuneConfig = {
|
||||
stages: { build: { prompt: "构建阶段" } },
|
||||
metadata: { tracked: true },
|
||||
};
|
||||
try {
|
||||
await assembleBuildPrompt(config, TMP_DIR, "user-auth");
|
||||
@@ -210,44 +200,110 @@ describe("assembleArchivePrompt", () => {
|
||||
expect(prompt).toContain("归档");
|
||||
});
|
||||
|
||||
it("tracked=false 时不读取 task.md,只输出通用提示词", async () => {
|
||||
const changeDir = join(TMP_DIR, ".rune", "changes", "user-auth");
|
||||
await mkdir(changeDir, { recursive: true });
|
||||
await writeFile(join(changeDir, "task.md"), "- [ ] 未完成任务");
|
||||
const config: RuneConfig = {
|
||||
stages: { archive: { prompt: "确认归档" } },
|
||||
metadata: { tracked: false },
|
||||
};
|
||||
const prompt = await assembleArchivePrompt(config, TMP_DIR, "user-auth");
|
||||
expect(prompt).toContain("确认归档");
|
||||
expect(prompt).not.toContain("未完成");
|
||||
});
|
||||
|
||||
it("tracked=true 时内嵌未完成任务列表并引导用户确认", async () => {
|
||||
const changeDir = join(TMP_DIR, ".rune", "changes", "user-auth");
|
||||
await mkdir(changeDir, { recursive: true });
|
||||
await writeFile(join(changeDir, "task.md"), "- [ ] 未完成任务");
|
||||
it("未完成任务时注入警告并引导用户确认", async () => {
|
||||
const config: RuneConfig = {
|
||||
stages: { archive: { prompt: "归档阶段" } },
|
||||
metadata: { tracked: true },
|
||||
};
|
||||
const changeDir = join(TMP_DIR, ".rune", "changes", "user-auth");
|
||||
await mkdir(changeDir, { recursive: true });
|
||||
await writeFile(join(changeDir, "task.md"), "- [ ] 未完成任务");
|
||||
const prompt = await assembleArchivePrompt(config, TMP_DIR, "user-auth");
|
||||
expect(prompt).toContain("未完成");
|
||||
expect(prompt).toContain("未完成任务");
|
||||
expect(prompt).toContain("是否确认");
|
||||
});
|
||||
|
||||
it("tracked=true 且所有任务完成时不注入警告", async () => {
|
||||
it("所有任务完成时不注入警告", async () => {
|
||||
const config: RuneConfig = {
|
||||
stages: { archive: { prompt: "归档阶段" } },
|
||||
};
|
||||
const changeDir = join(TMP_DIR, ".rune", "changes", "user-auth");
|
||||
await mkdir(changeDir, { recursive: true });
|
||||
await writeFile(join(changeDir, "task.md"), "- [x] 已完成任务");
|
||||
const config: RuneConfig = {
|
||||
stages: { archive: { prompt: "归档阶段" } },
|
||||
metadata: { tracked: true },
|
||||
};
|
||||
const prompt = await assembleArchivePrompt(config, TMP_DIR, "user-auth");
|
||||
expect(prompt).not.toContain("未完成");
|
||||
});
|
||||
|
||||
it("task.md 不存在时不注入警告", async () => {
|
||||
const config: RuneConfig = {
|
||||
stages: { archive: { prompt: "归档阶段" } },
|
||||
};
|
||||
const prompt = await assembleArchivePrompt(config, TMP_DIR, "no-task-change");
|
||||
expect(prompt).toContain("归档阶段");
|
||||
expect(prompt).not.toContain("警告");
|
||||
});
|
||||
});
|
||||
|
||||
describe("assembleTaskPrompt", () => {
|
||||
it("任务拆解阶段提示词包含变更名和文档路径", async () => {
|
||||
const config: RuneConfig = {
|
||||
stages: {
|
||||
plan: { documents: [{ name: "design", prompt: "生成设计" }] },
|
||||
task: { prompt: "拆解任务" },
|
||||
},
|
||||
};
|
||||
const changeDir = join(TMP_DIR, ".rune", "changes", "feature-x");
|
||||
await mkdir(changeDir, { recursive: true });
|
||||
await writeFile(join(changeDir, "design.md"), "# 设计");
|
||||
const prompt = await assembleTaskPrompt(config, TMP_DIR, "feature-x");
|
||||
expect(prompt).toContain("feature-x");
|
||||
expect(prompt).toContain("design.md");
|
||||
});
|
||||
|
||||
it("task.md 已存在时提示修订", async () => {
|
||||
const config: RuneConfig = {
|
||||
stages: {
|
||||
plan: { documents: [{ name: "design", prompt: "生成设计" }] },
|
||||
task: { prompt: "拆解任务" },
|
||||
},
|
||||
};
|
||||
const changeDir = join(TMP_DIR, ".rune", "changes", "revise-task");
|
||||
await mkdir(changeDir, { recursive: true });
|
||||
await writeFile(join(changeDir, "design.md"), "# 设计");
|
||||
await writeFile(join(changeDir, "task.md"), "- [ ] 已有任务");
|
||||
const prompt = await assembleTaskPrompt(config, TMP_DIR, "revise-task");
|
||||
expect(prompt).toContain("已有内容");
|
||||
});
|
||||
|
||||
it("task 阶段未配置时抛错", async () => {
|
||||
const config: RuneConfig = {
|
||||
stages: { plan: { documents: [{ name: "design", prompt: "生成设计" }] } },
|
||||
};
|
||||
try {
|
||||
await assembleTaskPrompt(config, TMP_DIR, "test");
|
||||
expect.unreachable();
|
||||
} catch (e: any) {
|
||||
expect(e.message).toContain("任务拆解阶段未配置");
|
||||
}
|
||||
});
|
||||
|
||||
it("plan 阶段未配置时抛错", async () => {
|
||||
const config: RuneConfig = {
|
||||
stages: { task: { prompt: "拆解任务" } },
|
||||
};
|
||||
try {
|
||||
await assembleTaskPrompt(config, TMP_DIR, "test");
|
||||
expect.unreachable();
|
||||
} catch (e: any) {
|
||||
expect(e.message).toContain("规划阶段未配置");
|
||||
}
|
||||
});
|
||||
|
||||
it("plan 文档未完成时抛错", async () => {
|
||||
const config: RuneConfig = {
|
||||
stages: {
|
||||
plan: { documents: [{ name: "design", prompt: "生成设计" }] },
|
||||
task: { prompt: "拆解任务" },
|
||||
},
|
||||
};
|
||||
try {
|
||||
await assembleTaskPrompt(config, TMP_DIR, "incomplete-plan");
|
||||
expect.unreachable();
|
||||
} catch (e: any) {
|
||||
expect(e.message).toContain("规划文档");
|
||||
expect(e.message).toContain("尚未全部完成");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("命令前缀替换", () => {
|
||||
@@ -273,13 +329,13 @@ describe("命令前缀替换", () => {
|
||||
it("assembleBuildPrompt 错误提示使用动态前缀", async () => {
|
||||
const config: RuneConfig = {
|
||||
stages: { build: { prompt: "构建阶段" } },
|
||||
metadata: { command: "pnpx @lanyuanxiaoyao/rune", tracked: true },
|
||||
metadata: { command: "pnpx @lanyuanxiaoyao/rune" },
|
||||
};
|
||||
try {
|
||||
await assembleBuildPrompt(config, TMP_DIR, "nonexistent-build");
|
||||
expect.unreachable();
|
||||
} catch (e: any) {
|
||||
expect(e.hint).toContain("pnpx @lanyuanxiaoyao/rune plan nonexistent-build");
|
||||
expect(e.hint).toContain("pnpx @lanyuanxiaoyao/rune task nonexistent-build");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user