From 7d5af32ce530a3993c3d897e6cd630cc48403e35 Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Wed, 10 Jun 2026 08:52:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20metadata.tracked=20=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E3=80=81validateConfig=20=E6=A0=A1=E9=AA=8C=E4=B8=8E=20mergeCo?= =?UTF-8?q?nfig=20=E6=B7=B1=E5=90=88=E5=B9=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/config.ts | 14 +++-- src/defaults/config.ts | 3 ++ src/types.ts | 1 + tests/core/config.test.ts | 93 ++++++++++++++++++++++++++++++++-- tests/integration/flow.test.ts | 4 +- 5 files changed, 108 insertions(+), 7 deletions(-) diff --git a/src/core/config.ts b/src/core/config.ts index 2da0444..5a429dd 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -37,6 +37,13 @@ export function validateConfig(config: RuneConfig): void { const plan = config.stages.plan; 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)); for (const doc of plan.documents) { @@ -94,9 +101,10 @@ function mergeConfig(userConfig: Partial): RuneConfig { } } - if (userConfig.metadata) { - result.metadata = userConfig.metadata; - } + result.metadata = { + ...defaultConfig.metadata, + ...userConfig.metadata, + }; return result; } diff --git a/src/defaults/config.ts b/src/defaults/config.ts index 5c7b238..5c0162d 100644 --- a/src/defaults/config.ts +++ b/src/defaults/config.ts @@ -1,6 +1,9 @@ import type { RuneConfig } from "../types.ts"; export const defaultConfig: RuneConfig = { + metadata: { + tracked: true, + }, stages: { discuss: { prompt: `进入探索模式。深度思考,自由发散。跟随对话走向。 diff --git a/src/types.ts b/src/types.ts index a6af50b..a6133b9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -38,6 +38,7 @@ export interface RuneConfig { stages: StagesConfig; metadata?: { command?: string; + tracked?: boolean; }; } diff --git a/tests/core/config.test.ts b/tests/core/config.test.ts index 6f040c9..8571951 100644 --- a/tests/core/config.test.ts +++ b/tests/core/config.test.ts @@ -65,7 +65,9 @@ describe("loadConfig", () => { await mkdir(runeDir, { recursive: true }); await writeFile( join(runeDir, "config.yaml"), - `stages: + `metadata: + tracked: false +stages: plan: documents: - name: spec @@ -169,6 +171,56 @@ describe("validateConfig", () => { }; 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", () => { @@ -191,7 +243,7 @@ describe("mergeConfig 保留 metadata", () => { } }); - it("无 metadata 时不设置该字段", async () => { + it("无 metadata 时保留默认 metadata(tracked: true)", async () => { const tmpDir = join(import.meta.dir, "__tmp_config_nometa_test__"); await mkdir(tmpDir, { recursive: true }); try { @@ -199,7 +251,42 @@ describe("mergeConfig 保留 metadata", () => { await mkdir(join(tmpDir, ".rune"), { recursive: true }); await writeFile(configPath, `stages:\n discuss:\n prompt: "测试"\n`); 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 { await rm(tmpDir, { recursive: true, force: true }); } diff --git a/tests/integration/flow.test.ts b/tests/integration/flow.test.ts index 5dae795..47a2edc 100644 --- a/tests/integration/flow.test.ts +++ b/tests/integration/flow.test.ts @@ -106,7 +106,9 @@ describe("完整 SDD 流程", () => { await writeFile( join(TMP_DIR, ".rune", "config.yaml"), - `stages: + `metadata: + tracked: false +stages: discuss: prompt: 自定义讨论 plan: