From bfa0f29dd5f9dacd018af7bdeb6b8ed0314fb8c0 Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Tue, 9 Jun 2026 12:57:28 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BF=AE=E5=A4=8D=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E5=AE=A1=E6=9F=A5=E5=8F=91=E7=8E=B0=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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,全部通过 --- src/adapters/claude-code.ts | 19 +--- src/adapters/opencode.ts | 14 +-- src/adapters/utils.ts | 13 +++ src/cli.ts | 73 ++++++++------ src/commands/init.ts | 2 +- src/core/assembler.ts | 16 ++- tests/adapters/claude-code.test.ts | 127 +++++++++++++++++++++++ tests/cli/map-error.test.ts | 62 ++++++++++++ tests/cli/status.test.ts | 156 +++++++++++++++++++++++++++++ tests/cli/validate.test.ts | 37 +++++++ 10 files changed, 459 insertions(+), 60 deletions(-) create mode 100644 src/adapters/utils.ts create mode 100644 tests/adapters/claude-code.test.ts create mode 100644 tests/cli/map-error.test.ts create mode 100644 tests/cli/status.test.ts create mode 100644 tests/cli/validate.test.ts diff --git a/src/adapters/claude-code.ts b/src/adapters/claude-code.ts index b8146e2..702be42 100644 --- a/src/adapters/claude-code.ts +++ b/src/adapters/claude-code.ts @@ -1,7 +1,8 @@ import { existsSync } from "node:fs"; -import { mkdir, writeFile, readFile } from "node:fs/promises"; +import { mkdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { STAGES } from "../types.ts"; +import { writeIfChanged } from "./utils.ts"; const COMMANDS_DIR = ".claude/commands"; @@ -50,21 +51,9 @@ export async function updateClaudeCode(projectRoot: string): Promise { ? "\n如果用户没有指定变更名称,请向用户确认。" : ""; const newContent = `执行以下命令,将输出作为当前阶段的工作指引:\n\`\`\`bash\n${cmd}\n\`\`\`${nameHint}\n`; - await writeIfChangedClaude(commandPath, newContent); + await writeIfChanged(commandPath, newContent); } const statusPath = join(projectRoot, COMMANDS_DIR, "rune-status.md"); - await writeIfChangedClaude(statusPath, `执行以下命令查看变更状态:\n\`\`\`bash\nrune status\n\`\`\`\n`); -} - -async function writeIfChangedClaude(filePath: string, newContent: string): Promise { - try { - const existing = await readFile(filePath, "utf-8"); - if (existing === newContent) { - return; - } - } catch { - // 文件不存在,创建 - } - await writeFile(filePath, newContent); + await writeIfChanged(statusPath, `执行以下命令查看变更状态:\n\`\`\`bash\nrune status\n\`\`\`\n`); } diff --git a/src/adapters/opencode.ts b/src/adapters/opencode.ts index e0fe22c..6931a49 100644 --- a/src/adapters/opencode.ts +++ b/src/adapters/opencode.ts @@ -1,5 +1,5 @@ import { existsSync } from "node:fs"; -import { mkdir, writeFile, readFile } from "node:fs/promises"; +import { mkdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { STAGES } from "../types.ts"; @@ -66,17 +66,7 @@ export async function updateOpenCode(projectRoot: string): Promise { await writeIfChanged(statusSkillPath, generateStatusSkill()); } -async function writeIfChanged(filePath: string, newContent: string): Promise { - try { - const existing = await readFile(filePath, "utf-8"); - if (existing === newContent) { - return; - } - } catch { - // 文件不存在,创建 - } - await writeFile(filePath, newContent); -} +import { writeIfChanged } from "./utils.ts"; function generateCommand(stage: string, hasChangeName: boolean): string { if (hasChangeName) { diff --git a/src/adapters/utils.ts b/src/adapters/utils.ts new file mode 100644 index 0000000..fba4f66 --- /dev/null +++ b/src/adapters/utils.ts @@ -0,0 +1,13 @@ +import { readFile, writeFile } from "node:fs/promises"; + +export async function writeIfChanged(filePath: string, newContent: string): Promise { + try { + const existing = await readFile(filePath, "utf-8"); + if (existing === newContent) { + return; + } + } catch { + // 文件不存在,创建 + } + await writeFile(filePath, newContent); +} diff --git a/src/cli.ts b/src/cli.ts index bfb3f34..6333eea 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -15,8 +15,8 @@ import { scanChanges } from "./core/scanner.ts"; import { UsageError, ConfigError, CommandError, InternalError, CliError } from "./cli/errors.ts"; import { printError } from "./cli/output.ts"; import { showGlobalHelp, showCommandHelp } from "./cli/help.ts"; -import type { ChangeStatus } from "./types.ts"; -import { defaultConfig } from "./defaults/config.ts"; +import type { ChangeStatus, RuneConfig } from "./types.ts"; + function requireProjectRoot(): string { const root = findProjectRoot(); @@ -26,7 +26,7 @@ function requireProjectRoot(): string { return root; } -function validateChangeName(name: string): void { +export function validateChangeName(name: string): void { if (!/^[\u4e00-\u9fa5a-zA-Z-]+$/.test(name)) { throw new CommandError( `变更名 "${name}" 包含不支持的字符`, @@ -35,16 +35,17 @@ function validateChangeName(name: string): void { } } -function formatChangeStatus(change: ChangeStatus): string { +export function formatChangeStatus(change: ChangeStatus, config?: RuneConfig): string { const lines: string[] = []; lines.push(`变更:${change.name}`); lines.push(" 规划阶段:"); + const planDocs = config?.stages.plan?.documents; for (const doc of change.documents) { if (doc.completed) { lines.push(` ${doc.name}.md ✓ 已完成`); } else { - const docConfig = defaultConfig.stages.plan?.documents.find((d) => d.name === doc.name); + const docConfig = planDocs?.find((d) => d.name === doc.name); const depInfo = !doc.dependMet && docConfig?.depend?.length ? `(依赖 ${docConfig.depend.map((d) => `${d}.md`).join("、")})` : ""; @@ -71,7 +72,7 @@ function formatChangeStatus(change: ChangeStatus): string { return lines.join("\n"); } -function suggestNextStep(change: ChangeStatus): string { +export function suggestNextStep(change: ChangeStatus): string { if (!change.planCompleted) { const nextDoc = change.documents.find((d) => !d.completed && d.dependMet); if (nextDoc) { @@ -140,19 +141,20 @@ cli.command("update [...tools]", "更新已注入的工具配置").action( const root = requireProjectRoot(); const { updateOpenCode } = await import("./adapters/opencode.ts"); const { updateClaudeCode } = await import("./adapters/claude-code.ts"); - const validators: Record Promise> = { + const { SUPPORTED_TOOLS } = await import("./commands/init.ts"); + const updaters: Record Promise> = { opencode: updateOpenCode, "claude-code": updateClaudeCode, }; for (const tool of tools) { - if (!validators[tool]) { + if (!SUPPORTED_TOOLS[tool]) { throw new CommandError(`不支持的工具: ${tool}`, { - hint: `支持的工具: ${Object.keys(validators).join(", ")}`, + hint: `支持的工具: ${Object.keys(SUPPORTED_TOOLS).join(", ")}`, }); } } for (const tool of tools) { - await validators[tool](root); + await updaters[tool](root); } console.log(`工具配置已更新:${tools.join(", ")}`); }, @@ -252,51 +254,66 @@ cli.command("status [change-name]", "查看变更状态").action( hint: "运行 rune status 查看所有变更", }); } - console.log(formatChangeStatus(change)); + console.log(formatChangeStatus(change, config)); } else { if (changes.length === 0) { console.log("当前无进行中的变更。"); return; } for (const change of changes) { - console.log(formatChangeStatus(change)); + console.log(formatChangeStatus(change, config)); console.log("---\n"); } } }, ); -function handleError(e: unknown): never { +export function mapError(e: unknown): CliError { if (e instanceof CliError) { - printError(e); - } else if (e instanceof Error && e.message.includes("Unknown option")) { + return e; + } + if (e instanceof Error) { + const err = mapCacError(e); + if (err) return err; + } + return new InternalError(); +} + +function mapCacError(e: Error): CliError | null { + if (e.message.includes("Unknown option")) { const match = e.message.match(/Unknown option `([^`]+)`/); const flag = match ? match[1] : "未知选项"; - printError(new UsageError(`未知选项: ${flag}`, { + return new UsageError(`未知选项: ${flag}`, { hint: "运行 rune help 查看所有命令", - })); - } else if (e instanceof Error && e.message.includes("Unknown command")) { + }); + } + if (e.message.includes("Unknown command")) { const match = e.message.match(/Unknown command `([^`]+)`/); const cmd = match ? match[1] : "未知命令"; - printError(new UsageError(`未知命令: ${cmd}`, { + return new UsageError(`未知命令: ${cmd}`, { hint: "运行 rune help 查看所有命令", - })); - } else if (e instanceof Error && e.message.includes("Unused args")) { + }); + } + if (e.message.includes("Unused args")) { const match = e.message.match(/Unused args: (.+)/); const args = match ? match[1] : "未知参数"; - printError(new UsageError(`未知命令: ${args.replace(/`/g, "")}`, { + return new UsageError(`未知命令: ${args.replace(/`/g, "")}`, { hint: "运行 rune help 查看所有命令", - })); - } else if (e instanceof Error && e.message.includes("missing required args")) { + }); + } + if (e.message.includes("missing required args")) { const match = e.message.match(/command `(\w+)/); const cmd = match ? match[1] : "未知命令"; - printError(new UsageError(`命令 '${cmd}' 缺少必填参数`, { + return new UsageError(`命令 '${cmd}' 缺少必填参数`, { usage: `rune ${cmd} `, hint: `运行 rune help ${cmd} 查看用法`, - })); - } else { - printError(new InternalError()); + }); } + return null; +} + +function handleError(e: unknown): never { + printError(mapError(e)); } process.on("unhandledRejection", (e) => { diff --git a/src/commands/init.ts b/src/commands/init.ts index 53f3354..45e1e3d 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -38,7 +38,7 @@ const CONFIG_TEMPLATE = `# Rune 配置文件 # # {{change-name}} 任务清单 `; -const SUPPORTED_TOOLS: Record Promise> = { +export const SUPPORTED_TOOLS: Record Promise> = { opencode: injectOpenCode, "claude-code": injectClaudeCode, }; diff --git a/src/core/assembler.ts b/src/core/assembler.ts index e2da0f8..e314c13 100644 --- a/src/core/assembler.ts +++ b/src/core/assembler.ts @@ -8,7 +8,9 @@ import { parseTasks } from "./task-parser.ts"; export function assembleDiscussPrompt(config: RuneConfig): string { const discuss = config.stages.discuss; - if (!discuss) throw new Error("discuss 阶段未配置"); + if (!discuss) throw new CommandError("讨论阶段未配置", { + hint: "请在 .rune/config.yaml 中配置 stages.discuss", + }); return discuss.prompt; } @@ -19,11 +21,15 @@ export async function assemblePlanPrompt( documentName: string, ): Promise { const plan = config.stages.plan; - if (!plan) throw new Error("plan 阶段未配置"); + if (!plan) throw new CommandError("规划阶段未配置", { + hint: "请在 .rune/config.yaml 中配置 stages.plan", + }); const doc = plan.documents.find((d) => d.name === documentName); if (!doc) { - throw new Error(`文档 "${documentName}" 不在配置的 plan.documents 中`); + throw new CommandError(`文档 "${documentName}" 不在配置的 plan.documents 中`, { + hint: `可用文档:${plan.documents.map((d) => d.name).join(", ")}`, + }); } const changeDir = getChangeDir(projectRoot, changeName); @@ -117,7 +123,9 @@ export async function assembleArchivePrompt( changeName: string, ): Promise { const archive = config.stages.archive; - if (!archive) throw new Error("archive 阶段未配置"); + if (!archive) throw new CommandError("归档阶段未配置", { + hint: "请在 .rune/config.yaml 中配置 stages.archive", + }); const changeDir = getChangeDir(projectRoot, changeName); const taskPath = join(changeDir, "task.md"); diff --git a/tests/adapters/claude-code.test.ts b/tests/adapters/claude-code.test.ts new file mode 100644 index 0000000..5e1c365 --- /dev/null +++ b/tests/adapters/claude-code.test.ts @@ -0,0 +1,127 @@ +import { describe, it, expect, beforeEach, afterEach } from "bun:test"; +import { existsSync } from "node:fs"; +import { mkdir, rm, readFile, readdir, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { injectClaudeCode, updateClaudeCode } from "../../src/adapters/claude-code.ts"; + +const TMP_DIR = join(import.meta.dir, "__tmp_claude_code_test__"); + +beforeEach(async () => { + await mkdir(TMP_DIR, { recursive: true }); +}); + +afterEach(async () => { + await rm(TMP_DIR, { recursive: true, force: true }); +}); + +describe("injectClaudeCode", () => { + it("生成 discuss、plan、build、archive 的 command 文件", async () => { + await injectClaudeCode(TMP_DIR); + + const commands = await readdir(join(TMP_DIR, ".claude", "commands")); + for (const stage of ["discuss", "plan", "build", "archive"]) { + expect(commands).toContain(`rune-${stage}.md`); + } + }); + + it("生成 rune-status command 文件", async () => { + await injectClaudeCode(TMP_DIR); + + const commands = await readdir(join(TMP_DIR, ".claude", "commands")); + expect(commands).toContain("rune-status.md"); + }); + + it("command 文件包含 bash 命令", async () => { + await injectClaudeCode(TMP_DIR); + const content = await readFile( + join(TMP_DIR, ".claude", "commands", "rune-discuss.md"), + "utf-8", + ); + expect(content).toContain("rune discuss"); + expect(content).toContain("```bash"); + }); + + it("plan/build/archive command 包含变更名称提示", async () => { + await injectClaudeCode(TMP_DIR); + + for (const stage of ["plan", "build", "archive"]) { + const content = await readFile( + join(TMP_DIR, ".claude", "commands", `rune-${stage}.md`), + "utf-8", + ); + expect(content).toContain("变更名"); + expect(content).toContain("如果用户没有指定变更名称"); + } + }); + + it("discuss command 不包含变更名称提示", async () => { + await injectClaudeCode(TMP_DIR); + const content = await readFile( + join(TMP_DIR, ".claude", "commands", "rune-discuss.md"), + "utf-8", + ); + expect(content).not.toContain("如果用户没有指定变更名称"); + }); + + it("重复注入时不覆盖已存在的文件", async () => { + await injectClaudeCode(TMP_DIR); + const originalContent = await readFile( + join(TMP_DIR, ".claude", "commands", "rune-discuss.md"), + "utf-8", + ); + + await injectClaudeCode(TMP_DIR); + const content = await readFile( + join(TMP_DIR, ".claude", "commands", "rune-discuss.md"), + "utf-8", + ); + expect(content).toBe(originalContent); + }); +}); + +describe("updateClaudeCode", () => { + it("文件不存在时创建", async () => { + await updateClaudeCode(TMP_DIR); + expect( + existsSync(join(TMP_DIR, ".claude", "commands", "rune-discuss.md")), + ).toBe(true); + }); + + it("文件存在且内容一致时不覆盖", async () => { + await injectClaudeCode(TMP_DIR); + const originalContent = await readFile( + join(TMP_DIR, ".claude", "commands", "rune-discuss.md"), + "utf-8", + ); + + await updateClaudeCode(TMP_DIR); + const content = await readFile( + join(TMP_DIR, ".claude", "commands", "rune-discuss.md"), + "utf-8", + ); + expect(content).toBe(originalContent); + }); + + it("文件存在但内容不一致时覆盖", async () => { + await injectClaudeCode(TMP_DIR); + await writeFile( + join(TMP_DIR, ".claude", "commands", "rune-discuss.md"), + "旧内容", + ); + + await updateClaudeCode(TMP_DIR); + const content = await readFile( + join(TMP_DIR, ".claude", "commands", "rune-discuss.md"), + "utf-8", + ); + expect(content).not.toBe("旧内容"); + expect(content).toContain("rune discuss"); + }); + + it("更新 status 命令", async () => { + await updateClaudeCode(TMP_DIR); + expect( + existsSync(join(TMP_DIR, ".claude", "commands", "rune-status.md")), + ).toBe(true); + }); +}); diff --git a/tests/cli/map-error.test.ts b/tests/cli/map-error.test.ts new file mode 100644 index 0000000..0b3dcad --- /dev/null +++ b/tests/cli/map-error.test.ts @@ -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 "); + 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); + }); +}); diff --git a/tests/cli/status.test.ts b/tests/cli/status.test.ts new file mode 100644 index 0000000..7a99c83 --- /dev/null +++ b/tests/cli/status.test.ts @@ -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 { + 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"); + }); +}); diff --git a/tests/cli/validate.test.ts b/tests/cli/validate.test.ts new file mode 100644 index 0000000..d682a88 --- /dev/null +++ b/tests/cli/validate.test.ts @@ -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); + }); +});