refactor: 修复代码审查发现的问题
- Bug修复: formatChangeStatus 使用实际配置而非 defaultConfig - 统一 assembler 中所有错误抛出为 CommandError - 提取 writeIfChanged 到 adapters/utils.ts,消除 claude-code/opencode 重复代码 - 导出 SUPPORTED_TOOLS,cli.ts update 命令复用同一工具注册表 - 提取 mapError/mapCacError 函数,支持单元测试 - 补充 claude-code 适配器测试(10 个用例) - 补充 validateChangeName、formatChangeStatus、suggestNextStep、mapError 单元测试(18 个用例) - 共新增 3 个测试文件,测试从 96 增至 133,全部通过
This commit is contained in:
62
tests/cli/map-error.test.ts
Normal file
62
tests/cli/map-error.test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import {
|
||||
CliError,
|
||||
UsageError,
|
||||
ConfigError,
|
||||
CommandError,
|
||||
InternalError,
|
||||
} from "../../src/cli/errors.ts";
|
||||
import { mapError } from "../../src/cli.ts";
|
||||
|
||||
describe("mapError", () => {
|
||||
it("CliError 原样返回", () => {
|
||||
const err = new ConfigError("未初始化");
|
||||
const result = mapError(err);
|
||||
expect(result).toBe(err);
|
||||
expect(result.message).toBe("未初始化");
|
||||
});
|
||||
|
||||
it("Unknown option 转为 UsageError", () => {
|
||||
const err = new Error("Unknown option `--badflag`");
|
||||
const result = mapError(err);
|
||||
expect(result).toBeInstanceOf(UsageError);
|
||||
expect(result.message).toBe("未知选项: --badflag");
|
||||
expect(result.hint).toBe("运行 rune help 查看所有命令");
|
||||
});
|
||||
|
||||
it("Unknown command 转为 UsageError", () => {
|
||||
const err = new Error("Unknown command `foo`");
|
||||
const result = mapError(err);
|
||||
expect(result).toBeInstanceOf(UsageError);
|
||||
expect(result.message).toBe("未知命令: foo");
|
||||
});
|
||||
|
||||
it("Unused args 转为 UsageError", () => {
|
||||
const err = new Error("Unused args: --extra");
|
||||
const result = mapError(err);
|
||||
expect(result).toBeInstanceOf(UsageError);
|
||||
expect(result.message).toContain("未知命令");
|
||||
expect(result.message).toContain("--extra");
|
||||
});
|
||||
|
||||
it("missing required args 转为 UsageError", () => {
|
||||
const err = new Error("missing required args for command `plan`");
|
||||
const result = mapError(err);
|
||||
expect(result).toBeInstanceOf(UsageError);
|
||||
expect(result.message).toBe("命令 'plan' 缺少必填参数");
|
||||
expect(result.usage).toBe("rune plan <change-name>");
|
||||
expect(result.hint).toContain("rune help plan");
|
||||
});
|
||||
|
||||
it("未知 Error 转为 InternalError", () => {
|
||||
const err = new Error("something unexpected");
|
||||
const result = mapError(err);
|
||||
expect(result).toBeInstanceOf(InternalError);
|
||||
expect(result.message).toBe("发生了未预期的错误");
|
||||
});
|
||||
|
||||
it("非 Error 类型转为 InternalError", () => {
|
||||
const result = mapError("字符串错误");
|
||||
expect(result).toBeInstanceOf(InternalError);
|
||||
});
|
||||
});
|
||||
156
tests/cli/status.test.ts
Normal file
156
tests/cli/status.test.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import { formatChangeStatus, suggestNextStep } from "../../src/cli.ts";
|
||||
import type { ChangeStatus, RuneConfig } from "../../src/types.ts";
|
||||
|
||||
function makeStatus(overrides: Partial<ChangeStatus> = {}): ChangeStatus {
|
||||
return {
|
||||
name: "test-change",
|
||||
documents: [],
|
||||
planCompleted: false,
|
||||
buildUnlocked: false,
|
||||
taskProgress: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("formatChangeStatus", () => {
|
||||
it("显示变更名", () => {
|
||||
const output = formatChangeStatus(makeStatus());
|
||||
expect(output).toContain("test-change");
|
||||
});
|
||||
|
||||
it("显示已完成和待完成文档", () => {
|
||||
const status = makeStatus({
|
||||
documents: [
|
||||
{ name: "design", completed: true, dependMet: true },
|
||||
{ name: "task", completed: false, dependMet: true },
|
||||
],
|
||||
});
|
||||
const output = formatChangeStatus(status);
|
||||
expect(output).toContain("design.md ✓ 已完成");
|
||||
expect(output).toContain("task.md ○ 待完成");
|
||||
});
|
||||
|
||||
it("显示文档依赖信息(dependMet 为 false 且 config 中有依赖)", () => {
|
||||
const status = makeStatus({
|
||||
documents: [
|
||||
{ name: "task", completed: false, dependMet: false },
|
||||
],
|
||||
});
|
||||
const config: RuneConfig = {
|
||||
stages: {
|
||||
plan: {
|
||||
documents: [
|
||||
{ name: "design", prompt: "生成设计" },
|
||||
{ name: "task", prompt: "生成任务", depend: ["design"] },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const output = formatChangeStatus(status, config);
|
||||
expect(output).toContain("依赖 design.md");
|
||||
});
|
||||
|
||||
it("dependMet 为 false 但无 config 时不显示文档依赖信息", () => {
|
||||
const status = makeStatus({
|
||||
documents: [
|
||||
{ name: "task", completed: false, dependMet: false },
|
||||
],
|
||||
});
|
||||
const output = formatChangeStatus(status);
|
||||
expect(output).not.toContain("(依赖");
|
||||
});
|
||||
|
||||
it("显示规划进度", () => {
|
||||
const status = makeStatus({
|
||||
documents: [
|
||||
{ name: "design", completed: true, dependMet: true },
|
||||
{ name: "task", completed: false, dependMet: true },
|
||||
],
|
||||
});
|
||||
const output = formatChangeStatus(status);
|
||||
expect(output).toContain("1/2 文档已完成");
|
||||
});
|
||||
|
||||
it("规划完成时显示构建已解锁", () => {
|
||||
const status = makeStatus({
|
||||
documents: [
|
||||
{ name: "design", completed: true, dependMet: true },
|
||||
{ name: "task", completed: true, dependMet: true },
|
||||
],
|
||||
planCompleted: true,
|
||||
buildUnlocked: true,
|
||||
});
|
||||
const output = formatChangeStatus(status);
|
||||
expect(output).toContain("已解锁");
|
||||
});
|
||||
|
||||
it("显示任务进度", () => {
|
||||
const status = makeStatus({
|
||||
taskProgress: { completed: 3, total: 5 },
|
||||
});
|
||||
const output = formatChangeStatus(status);
|
||||
expect(output).toContain("3/5 已完成");
|
||||
});
|
||||
|
||||
it("包含下一步建议", () => {
|
||||
const output = formatChangeStatus(makeStatus());
|
||||
expect(output).toContain("建议下一步");
|
||||
});
|
||||
});
|
||||
|
||||
describe("suggestNextStep", () => {
|
||||
it("规划未完成时返回下一个可规划文档", () => {
|
||||
const status = makeStatus({
|
||||
documents: [
|
||||
{ name: "design", completed: false, dependMet: true },
|
||||
],
|
||||
});
|
||||
expect(suggestNextStep(status)).toContain("rune plan test-change design");
|
||||
});
|
||||
|
||||
it("规划未完成且依赖未满足时提示完成前置依赖", () => {
|
||||
const status = makeStatus({
|
||||
documents: [
|
||||
{ name: "design", completed: false, dependMet: false },
|
||||
],
|
||||
});
|
||||
expect(suggestNextStep(status)).toBe("完成前置依赖后再规划文档");
|
||||
});
|
||||
|
||||
it("规划完成且有未完成任务时建议 build", () => {
|
||||
const status = makeStatus({
|
||||
documents: [
|
||||
{ name: "design", completed: true, dependMet: true },
|
||||
{ name: "task", completed: true, dependMet: true },
|
||||
],
|
||||
planCompleted: true,
|
||||
taskProgress: { completed: 2, total: 5 },
|
||||
});
|
||||
expect(suggestNextStep(status)).toContain("rune build test-change");
|
||||
});
|
||||
|
||||
it("任务全部完成时建议 archive", () => {
|
||||
const status = makeStatus({
|
||||
documents: [
|
||||
{ name: "design", completed: true, dependMet: true },
|
||||
{ name: "task", completed: true, dependMet: true },
|
||||
],
|
||||
planCompleted: true,
|
||||
taskProgress: { completed: 5, total: 5 },
|
||||
});
|
||||
expect(suggestNextStep(status)).toContain("rune archive test-change");
|
||||
});
|
||||
|
||||
it("规划完成但无 taskProgress 时建议 build", () => {
|
||||
const status = makeStatus({
|
||||
documents: [
|
||||
{ name: "design", completed: true, dependMet: true },
|
||||
{ name: "task", completed: true, dependMet: true },
|
||||
],
|
||||
planCompleted: true,
|
||||
taskProgress: null,
|
||||
});
|
||||
expect(suggestNextStep(status)).toContain("rune build test-change");
|
||||
});
|
||||
});
|
||||
37
tests/cli/validate.test.ts
Normal file
37
tests/cli/validate.test.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import { validateChangeName } from "../../src/cli.ts";
|
||||
import { CommandError } from "../../src/cli/errors.ts";
|
||||
|
||||
describe("validateChangeName", () => {
|
||||
it("英文名通过", () => {
|
||||
expect(() => validateChangeName("user-auth")).not.toThrow();
|
||||
expect(() => validateChangeName("addLogin")).not.toThrow();
|
||||
});
|
||||
|
||||
it("中文名通过", () => {
|
||||
expect(() => validateChangeName("用户登录")).not.toThrow();
|
||||
expect(() => validateChangeName("修复内存泄漏")).not.toThrow();
|
||||
});
|
||||
|
||||
it("中英混合通过", () => {
|
||||
expect(() => validateChangeName("用户-login")).not.toThrow();
|
||||
});
|
||||
|
||||
it("空格不通过", () => {
|
||||
expect(() => validateChangeName("my change")).toThrow(CommandError);
|
||||
});
|
||||
|
||||
it("下划线不通过", () => {
|
||||
expect(() => validateChangeName("my_change")).toThrow(CommandError);
|
||||
});
|
||||
|
||||
it("特殊符号不通过", () => {
|
||||
expect(() => validateChangeName("my-change!")).toThrow(CommandError);
|
||||
expect(() => validateChangeName("my.change")).toThrow(CommandError);
|
||||
expect(() => validateChangeName("my@change")).toThrow(CommandError);
|
||||
});
|
||||
|
||||
it("空字符串不通过", () => {
|
||||
expect(() => validateChangeName("")).toThrow(CommandError);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user