293 lines
11 KiB
TypeScript
293 lines
11 KiB
TypeScript
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("探索模式");
|
||
expect(prompt).toContain("立场");
|
||
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("已有文档时引导 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("design.md");
|
||
expect(prompt).not.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("引导 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("task.md");
|
||
expect(prompt).toContain("2");
|
||
expect(prompt).not.toContain("任务二");
|
||
expect(prompt).not.toContain("任务三");
|
||
});
|
||
|
||
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("plan nonexistent");
|
||
}
|
||
});
|
||
|
||
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 () => {
|
||
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");
|
||
expect.unreachable();
|
||
} catch (e: any) {
|
||
expect(e.message).toContain("task.md");
|
||
expect(e.message).toContain("checkbox");
|
||
}
|
||
});
|
||
|
||
it("tracked=true 且 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");
|
||
expect.unreachable();
|
||
} catch (e: any) {
|
||
expect(e.message).toContain("checkbox");
|
||
}
|
||
});
|
||
});
|
||
|
||
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("归档");
|
||
});
|
||
|
||
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 时引导 AI 检查 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: true },
|
||
};
|
||
const prompt = await assembleArchivePrompt(config, TMP_DIR, "user-auth");
|
||
expect(prompt).toContain("task.md");
|
||
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"), "- [x] 已完成任务");
|
||
const config: RuneConfig = {
|
||
stages: { archive: { prompt: "归档阶段" } },
|
||
metadata: { tracked: true },
|
||
};
|
||
const prompt = await assembleArchivePrompt(config, TMP_DIR, "user-auth");
|
||
expect(prompt).not.toContain("未完成");
|
||
});
|
||
});
|
||
|
||
describe("命令前缀替换", () => {
|
||
it("assembleDiscussPrompt 替换 rune 为配置前缀", () => {
|
||
const config: RuneConfig = {
|
||
stages: { discuss: { prompt: "执行 rune status 查看状态" } },
|
||
metadata: { command: "bunx @lanyuanxiaoyao/rune" },
|
||
};
|
||
const prompt = assembleDiscussPrompt(config);
|
||
expect(prompt).toContain("bunx @lanyuanxiaoyao/rune status");
|
||
expect(prompt).not.toContain("执行 rune ");
|
||
});
|
||
|
||
it("assembleDiscussPrompt 无配置时追加降级说明", () => {
|
||
const config: RuneConfig = {
|
||
stages: { discuss: { prompt: "执行 rune status 查看" } },
|
||
};
|
||
const prompt = assembleDiscussPrompt(config);
|
||
expect(prompt).toContain("bunx @lanyuanxiaoyao/rune status");
|
||
expect(prompt).toContain("如果没有安装 bun");
|
||
});
|
||
|
||
it("assembleBuildPrompt 错误提示使用动态前缀", async () => {
|
||
const config: RuneConfig = {
|
||
stages: { build: { prompt: "构建阶段" } },
|
||
metadata: { command: "pnpx @lanyuanxiaoyao/rune", tracked: true },
|
||
};
|
||
try {
|
||
await assembleBuildPrompt(config, TMP_DIR, "nonexistent-build");
|
||
expect.unreachable();
|
||
} catch (e: any) {
|
||
expect(e.hint).toContain("pnpx @lanyuanxiaoyao/rune plan nonexistent-build");
|
||
}
|
||
});
|
||
|
||
it("不替换 /rune- 形式", () => {
|
||
const config: RuneConfig = {
|
||
stages: { discuss: { prompt: "使用 /rune-plan 进入规划,然后 rune build 构建" } },
|
||
metadata: { command: "bunx @lanyuanxiaoyao/rune" },
|
||
};
|
||
const prompt = assembleDiscussPrompt(config);
|
||
expect(prompt).toContain("/rune-plan");
|
||
expect(prompt).toContain("bunx @lanyuanxiaoyao/rune build");
|
||
});
|
||
});
|