import { describe, it, expect, beforeEach, afterEach } from "bun:test"; import { mkdir, writeFile, rm } from "node:fs/promises"; import { join } from "node:path"; import { loadConfig, findProjectRoot, getRuneDir, validateConfig } from "../../src/core/config.ts"; import { ConfigError } from "../../src/cli/errors.ts"; import type { RuneConfig } from "../../src/types.ts"; const TMP_DIR = join(import.meta.dir, "__tmp_config_test__"); beforeEach(async () => { await mkdir(TMP_DIR, { recursive: true }); }); afterEach(async () => { await rm(TMP_DIR, { recursive: true, force: true }); }); describe("findProjectRoot", () => { it("当前目录存在 .rune/ 时返回当前目录", async () => { await mkdir(join(TMP_DIR, ".rune")); expect(findProjectRoot(TMP_DIR)).toBe(TMP_DIR); }); it("子目录中向上查找到 .rune/", async () => { await mkdir(join(TMP_DIR, ".rune")); const sub = join(TMP_DIR, "src", "commands"); await mkdir(sub, { recursive: true }); expect(findProjectRoot(sub)).toBe(TMP_DIR); }); it("找不到 .rune/ 时返回 null", () => { expect(findProjectRoot("/tmp/__nonexistent__")).toBeNull(); }); }); describe("loadConfig", () => { it("无配置文件时返回默认配置", async () => { await mkdir(join(TMP_DIR, ".rune")); const config = await loadConfig(TMP_DIR); expect(config.stages.discuss).toBeDefined(); expect(config.stages.plan).toBeDefined(); expect(config.stages.build).toBeDefined(); expect(config.stages.archive).toBeDefined(); }); it("用户配置覆盖默认配置(全量替换)", async () => { const runeDir = join(TMP_DIR, ".rune"); await mkdir(runeDir, { recursive: true }); await writeFile( join(runeDir, "config.yaml"), `stages: discuss: prompt: 自定义讨论提示词 `, ); const config = await loadConfig(TMP_DIR); expect(config.stages.discuss!.prompt).toBe("自定义讨论提示词"); expect(config.stages.plan).toBeDefined(); expect(config.stages.build).toBeDefined(); expect(config.stages.archive).toBeDefined(); }); it("用户配置 plan 时完全替换内置 documents", async () => { const runeDir = join(TMP_DIR, ".rune"); await mkdir(runeDir, { recursive: true }); await writeFile( join(runeDir, "config.yaml"), `stages: plan: documents: - name: spec prompt: 生成规格文档 `, ); const config = await loadConfig(TMP_DIR); expect(config.stages.plan!.documents).toHaveLength(1); expect(config.stages.plan!.documents[0].name).toBe("spec"); }); it("空配置 stages: {} 时返回完整默认配置", async () => { const runeDir = join(TMP_DIR, ".rune"); await mkdir(runeDir, { recursive: true }); await writeFile(join(runeDir, "config.yaml"), "stages: {}"); const config = await loadConfig(TMP_DIR); expect(config.stages.discuss).toBeDefined(); expect(config.stages.plan).toBeDefined(); expect(config.stages.build).toBeDefined(); expect(config.stages.archive).toBeDefined(); }); it("YAML 解析错误时返回默认配置", async () => { const runeDir = join(TMP_DIR, ".rune"); await mkdir(runeDir, { recursive: true }); await writeFile(join(runeDir, "config.yaml"), `stages: [invalid yaml {{{`); const config = await loadConfig(TMP_DIR); expect(config.stages.discuss).toBeDefined(); }); }); describe("getRuneDir", () => { it("返回 .rune 目录路径", () => { expect(getRuneDir("/project")).toBe(join("/project", ".rune")); }); }); describe("validateConfig", () => { it("正常配置不抛错", () => { const config: RuneConfig = { stages: { plan: { documents: [ { name: "design", prompt: "生成设计" }, { name: "task", prompt: "生成任务", depend: ["design"] }, ], }, }, }; expect(() => validateConfig(config)).not.toThrow(); }); it("depend 引用不存在的文档时报错", () => { const config: RuneConfig = { stages: { plan: { documents: [{ name: "task", prompt: "生成任务", depend: ["nonexistent"] }], }, }, }; expect(() => validateConfig(config)).toThrow(ConfigError); }); it("自依赖报错", () => { const config: RuneConfig = { stages: { plan: { documents: [{ name: "design", prompt: "生成设计", depend: ["design"] }], }, }, }; expect(() => validateConfig(config)).toThrow(ConfigError); }); it("循环依赖报错", () => { const config: RuneConfig = { stages: { plan: { documents: [ { name: "a", prompt: "a", depend: ["b"] }, { name: "b", prompt: "b", depend: ["a"] }, ], }, }, }; expect(() => validateConfig(config)).toThrow(ConfigError); }); it("无 plan 阶段时不报错", () => { const config: RuneConfig = { stages: {} }; expect(() => validateConfig(config)).not.toThrow(); }); it("depend 为空数组时不报错", () => { const config: RuneConfig = { stages: { plan: { documents: [{ name: "design", prompt: "生成设计", depend: [] }], }, }, }; expect(() => validateConfig(config)).not.toThrow(); }); });