feat: 提示词拼装器及测试
This commit is contained in:
102
src/core/assembler.ts
Normal file
102
src/core/assembler.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import type { RuneConfig } from "../types.ts";
|
||||
import { getChangeDir } from "./config.ts";
|
||||
import { parseTasks } from "./task-parser.ts";
|
||||
|
||||
export function assembleDiscussPrompt(config: RuneConfig): string {
|
||||
const discuss = config.stages.discuss;
|
||||
if (!discuss) throw new Error("discuss 阶段未配置");
|
||||
return discuss.prompt;
|
||||
}
|
||||
|
||||
export async function assemblePlanPrompt(
|
||||
config: RuneConfig,
|
||||
projectRoot: string,
|
||||
changeName: string,
|
||||
): Promise<string> {
|
||||
const plan = config.stages.plan;
|
||||
if (!plan) throw new Error("plan 阶段未配置");
|
||||
|
||||
const changeDir = getChangeDir(projectRoot, changeName);
|
||||
const parts: string[] = [];
|
||||
|
||||
parts.push(`# 规划阶段:${changeName}\n`);
|
||||
parts.push("请为当前变更生成以下文档:\n");
|
||||
|
||||
for (const doc of plan.documents) {
|
||||
parts.push(`## 文档:${doc.name}.md`);
|
||||
parts.push(doc.prompt);
|
||||
|
||||
const docPath = join(changeDir, `${doc.name}.md`);
|
||||
if (existsSync(docPath)) {
|
||||
const existing = await readFile(docPath, "utf-8");
|
||||
parts.push(`\n### 已有内容(请在此基础上修订):\n${existing}`);
|
||||
}
|
||||
|
||||
if (doc.template) {
|
||||
const rendered = doc.template.replace(/\{\{change-name\}\}/g, changeName);
|
||||
parts.push(`\n### 格式模板:\n${rendered}`);
|
||||
}
|
||||
|
||||
parts.push("");
|
||||
}
|
||||
|
||||
parts.push(`请将文档写入目录:${changeDir}`);
|
||||
return parts.join("\n");
|
||||
}
|
||||
|
||||
export async function assembleBuildPrompt(
|
||||
config: RuneConfig,
|
||||
projectRoot: string,
|
||||
changeName: string,
|
||||
): Promise<string> {
|
||||
const build = config.stages.build;
|
||||
if (!build) throw new Error("build 阶段未配置");
|
||||
|
||||
const changeDir = getChangeDir(projectRoot, changeName);
|
||||
const taskPath = join(changeDir, "task.md");
|
||||
|
||||
let taskContent: string;
|
||||
try {
|
||||
taskContent = await readFile(taskPath, "utf-8");
|
||||
} catch {
|
||||
throw new Error(`task.md not found in ${changeDir}`);
|
||||
}
|
||||
|
||||
const tasks = parseTasks(taskContent);
|
||||
const pendingTasks = tasks.filter((t) => !t.checked);
|
||||
|
||||
if (pendingTasks.length === 0) {
|
||||
return `所有任务已完成。变更 "${changeName}" 可以归档。`;
|
||||
}
|
||||
|
||||
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。`,
|
||||
);
|
||||
|
||||
return parts.join("\n");
|
||||
}
|
||||
|
||||
export function assembleArchivePrompt(
|
||||
config: RuneConfig,
|
||||
changeName: string,
|
||||
): string {
|
||||
const archive = config.stages.archive;
|
||||
if (!archive) throw new Error("archive 阶段未配置");
|
||||
|
||||
const parts: string[] = [];
|
||||
parts.push(`# 归档阶段:${changeName}\n`);
|
||||
parts.push(archive.prompt);
|
||||
return parts.join("\n");
|
||||
}
|
||||
144
tests/core/assembler.test.ts
Normal file
144
tests/core/assembler.test.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
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",
|
||||
);
|
||||
expect(prompt).toContain("user-auth");
|
||||
expect(prompt).toContain("design");
|
||||
expect(prompt).toContain("task");
|
||||
expect(prompt).toContain("格式模板");
|
||||
});
|
||||
|
||||
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",
|
||||
);
|
||||
expect(prompt).toContain("已有设计");
|
||||
expect(prompt).toContain("在此基础上修订");
|
||||
});
|
||||
|
||||
it("替换模板中的 {{change-name}}", async () => {
|
||||
const prompt = await assemblePlanPrompt(
|
||||
defaultConfig,
|
||||
TMP_DIR,
|
||||
"user-auth",
|
||||
);
|
||||
expect(prompt).toContain("user-auth 设计文档");
|
||||
expect(prompt).toContain("user-auth 任务列表");
|
||||
expect(prompt).not.toContain("{{change-name}}");
|
||||
});
|
||||
|
||||
it("使用自定义 plan 配置", async () => {
|
||||
const config: RuneConfig = {
|
||||
stages: {
|
||||
plan: {
|
||||
documents: [
|
||||
{
|
||||
name: "spec",
|
||||
prompt: "生成规格",
|
||||
template: "# {{change-name}} 规格",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const prompt = await assemblePlanPrompt(config, TMP_DIR, "my-feature");
|
||||
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 不存在时抛出错误", async () => {
|
||||
await expect(
|
||||
assembleBuildPrompt(defaultConfig, TMP_DIR, "nonexistent"),
|
||||
).rejects.toThrow("task.md not found");
|
||||
});
|
||||
});
|
||||
|
||||
describe("assembleArchivePrompt", () => {
|
||||
it("返回归档提示词", () => {
|
||||
const prompt = assembleArchivePrompt(defaultConfig, "user-auth");
|
||||
expect(prompt).toContain("user-auth");
|
||||
expect(prompt).toContain("归档");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user