feat: metadata.tracked 类型、validateConfig 校验与 mergeConfig 深合并

This commit is contained in:
2026-06-10 08:52:18 +08:00
parent af58f786af
commit 7d5af32ce5
5 changed files with 108 additions and 7 deletions

View File

@@ -37,6 +37,13 @@ export function validateConfig(config: RuneConfig): void {
const plan = config.stages.plan; const plan = config.stages.plan;
if (!plan) return; if (!plan) return;
if (config.metadata?.tracked && plan) {
const hasTaskDoc = plan.documents.some((d) => d.name === "task");
if (!hasTaskDoc) {
throw new ConfigError('tracked 开启时 plan.documents 必须包含 name 为 "task" 的文档');
}
}
const docNames = new Set(plan.documents.map((d) => d.name)); const docNames = new Set(plan.documents.map((d) => d.name));
for (const doc of plan.documents) { for (const doc of plan.documents) {
@@ -94,9 +101,10 @@ function mergeConfig(userConfig: Partial<RuneConfig>): RuneConfig {
} }
} }
if (userConfig.metadata) { result.metadata = {
result.metadata = userConfig.metadata; ...defaultConfig.metadata,
} ...userConfig.metadata,
};
return result; return result;
} }

View File

@@ -1,6 +1,9 @@
import type { RuneConfig } from "../types.ts"; import type { RuneConfig } from "../types.ts";
export const defaultConfig: RuneConfig = { export const defaultConfig: RuneConfig = {
metadata: {
tracked: true,
},
stages: { stages: {
discuss: { discuss: {
prompt: `进入探索模式。深度思考,自由发散。跟随对话走向。 prompt: `进入探索模式。深度思考,自由发散。跟随对话走向。

View File

@@ -38,6 +38,7 @@ export interface RuneConfig {
stages: StagesConfig; stages: StagesConfig;
metadata?: { metadata?: {
command?: string; command?: string;
tracked?: boolean;
}; };
} }

View File

@@ -65,7 +65,9 @@ describe("loadConfig", () => {
await mkdir(runeDir, { recursive: true }); await mkdir(runeDir, { recursive: true });
await writeFile( await writeFile(
join(runeDir, "config.yaml"), join(runeDir, "config.yaml"),
`stages: `metadata:
tracked: false
stages:
plan: plan:
documents: documents:
- name: spec - name: spec
@@ -169,6 +171,56 @@ describe("validateConfig", () => {
}; };
expect(() => validateConfig(config)).not.toThrow(); expect(() => validateConfig(config)).not.toThrow();
}); });
it("tracked=true 时 plan.documents 必须包含 task 文档", () => {
const config: RuneConfig = {
stages: {
plan: {
documents: [{ name: "design", prompt: "生成设计" }],
},
},
metadata: { tracked: true },
};
expect(() => validateConfig(config)).toThrow(ConfigError);
});
it("tracked=true 且 plan.documents 包含 task 时不报错", () => {
const config: RuneConfig = {
stages: {
plan: {
documents: [
{ name: "design", prompt: "生成设计" },
{ name: "task", prompt: "生成任务" },
],
},
},
metadata: { tracked: true },
};
expect(() => validateConfig(config)).not.toThrow();
});
it("tracked=false 时 plan.documents 不包含 task 也不报错", () => {
const config: RuneConfig = {
stages: {
plan: {
documents: [{ name: "design", prompt: "生成设计" }],
},
},
metadata: { tracked: false },
};
expect(() => validateConfig(config)).not.toThrow();
});
it("tracked 未配置时等同于 false不强制要求 task 文档", () => {
const config: RuneConfig = {
stages: {
plan: {
documents: [{ name: "design", prompt: "生成设计" }],
},
},
};
expect(() => validateConfig(config)).not.toThrow();
});
}); });
describe("mergeConfig 保留 metadata", () => { describe("mergeConfig 保留 metadata", () => {
@@ -191,7 +243,7 @@ describe("mergeConfig 保留 metadata", () => {
} }
}); });
it("无 metadata 时不设置该字段", async () => { it("无 metadata 时保留默认 metadatatracked: true", async () => {
const tmpDir = join(import.meta.dir, "__tmp_config_nometa_test__"); const tmpDir = join(import.meta.dir, "__tmp_config_nometa_test__");
await mkdir(tmpDir, { recursive: true }); await mkdir(tmpDir, { recursive: true });
try { try {
@@ -199,7 +251,42 @@ describe("mergeConfig 保留 metadata", () => {
await mkdir(join(tmpDir, ".rune"), { recursive: true }); await mkdir(join(tmpDir, ".rune"), { recursive: true });
await writeFile(configPath, `stages:\n discuss:\n prompt: "测试"\n`); await writeFile(configPath, `stages:\n discuss:\n prompt: "测试"\n`);
const config = await loadConfig(tmpDir); const config = await loadConfig(tmpDir);
expect(config.metadata).toBeUndefined(); expect(config.metadata?.tracked).toBe(true);
} finally {
await rm(tmpDir, { recursive: true, force: true });
}
});
it("用户 metadata 与默认 metadata 深合并", async () => {
const tmpDir = join(import.meta.dir, "__tmp_config_deep_merge__");
await mkdir(tmpDir, { recursive: true });
try {
const configPath = join(tmpDir, ".rune", "config.yaml");
await mkdir(join(tmpDir, ".rune"), { recursive: true });
await writeFile(
configPath,
`metadata:\n command: "rune"\nstages:\n discuss:\n prompt: "测试"\n`,
);
const config = await loadConfig(tmpDir);
expect(config.metadata?.command).toBe("rune");
expect(config.metadata?.tracked).toBe(true);
} finally {
await rm(tmpDir, { recursive: true, force: true });
}
});
it("用户 metadata.tracked 显式覆盖默认值", async () => {
const tmpDir = join(import.meta.dir, "__tmp_config_tracked_override__");
await mkdir(tmpDir, { recursive: true });
try {
const configPath = join(tmpDir, ".rune", "config.yaml");
await mkdir(join(tmpDir, ".rune"), { recursive: true });
await writeFile(
configPath,
`metadata:\n tracked: false\nstages:\n discuss:\n prompt: "测试"\n`,
);
const config = await loadConfig(tmpDir);
expect(config.metadata?.tracked).toBe(false);
} finally { } finally {
await rm(tmpDir, { recursive: true, force: true }); await rm(tmpDir, { recursive: true, force: true });
} }

View File

@@ -106,7 +106,9 @@ describe("完整 SDD 流程", () => {
await writeFile( await writeFile(
join(TMP_DIR, ".rune", "config.yaml"), join(TMP_DIR, ".rune", "config.yaml"),
`stages: `metadata:
tracked: false
stages:
discuss: discuss:
prompt: 自定义讨论 prompt: 自定义讨论
plan: plan: