Files
Rune-Spec/tests/core/config.test.ts

295 lines
9.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"),
`metadata:
tracked: false
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();
});
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", () => {
it("保留用户配置中的 metadata", async () => {
const tmpDir = join(import.meta.dir, "__tmp_config_meta_test__");
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: "bunx @lanyuanxiaoyao/rune"\nstages:\n discuss:\n prompt: "自定义讨论"\n`,
);
const config = await loadConfig(tmpDir);
expect(config.metadata).toBeDefined();
expect(config.metadata!.command).toBe("bunx @lanyuanxiaoyao/rune");
expect(config.stages.discuss!.prompt).toBe("自定义讨论");
} finally {
await rm(tmpDir, { recursive: true, force: true });
}
});
it("无 metadata 时保留默认 metadatatracked: true", async () => {
const tmpDir = join(import.meta.dir, "__tmp_config_nometa_test__");
await mkdir(tmpDir, { recursive: true });
try {
const configPath = join(tmpDir, ".rune", "config.yaml");
await mkdir(join(tmpDir, ".rune"), { recursive: true });
await writeFile(configPath, `stages:\n discuss:\n prompt: "测试"\n`);
const config = await loadConfig(tmpDir);
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 });
}
});
});