diff --git a/src/cli.ts b/src/cli.ts index c201a80..952e8ae 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -26,6 +26,15 @@ function requireProjectRoot(): string { return root; } +function validateChangeName(name: string): void { + if (!/^[\u4e00-\u9fa5a-zA-Z-]+$/.test(name)) { + throw new CommandError( + `变更名 "${name}" 包含不支持的字符`, + { hint: "变更名仅支持中文、英文和短横线(-)" }, + ); + } +} + function formatChangeStatus(change: ChangeStatus): string { const lines: string[] = []; lines.push(`变更:${change.name}`); @@ -129,6 +138,7 @@ cli.command("discuss", "讨论阶段").action(async () => { cli.command("plan ", "规划阶段").action( async (changeName: string, documentName: string) => { + validateChangeName(changeName); const root = requireProjectRoot(); const config = await loadConfig(root); const planDocs = config.stages.plan?.documents; @@ -166,6 +176,7 @@ cli.command("plan ", "规划阶段").action( cli.command("build ", "构建阶段").action( async (changeName: string) => { + validateChangeName(changeName); const root = requireProjectRoot(); const changeDir = getChangeDir(root, changeName); if (!existsSync(changeDir)) { @@ -181,6 +192,7 @@ cli.command("build ", "构建阶段").action( cli.command("archive ", "归档阶段").action( async (changeName: string) => { + validateChangeName(changeName); const root = requireProjectRoot(); const changeDir = getChangeDir(root, changeName); if (!existsSync(changeDir)) { diff --git a/tests/integration/flow.test.ts b/tests/integration/flow.test.ts index 3c8380b..feceb79 100644 --- a/tests/integration/flow.test.ts +++ b/tests/integration/flow.test.ts @@ -153,3 +153,24 @@ describe("完整 SDD 流程", () => { expect(taskDoc!.dependMet).toBe(true); }); }); + +describe("变更名校验", () => { + it("合法变更名(中文、英文、短横线)通过校验", async () => { + await runInit(TMP_DIR, ["opencode"]); + const config = await loadConfig(TMP_DIR); + await mkdir(getChangeDir(TMP_DIR, "用户-login"), { recursive: true }); + await writeFile(join(getChangeDir(TMP_DIR, "用户-login"), "design.md"), "# 设计"); + await writeFile(join(getChangeDir(TMP_DIR, "用户-login"), "task.md"), "- [ ] 任务"); + const prompt = await assemblePlanPrompt(config, TMP_DIR, "用户-login", "design"); + expect(prompt).toContain("用户-login"); + }); + + it("非法变更名(空格、下划线、特殊符号)被拒绝", () => { + const validRegex = /^[\u4e00-\u9fa5a-zA-Z-]+$/; + expect(validRegex.test("my change")).toBe(false); + expect(validRegex.test("my_change")).toBe(false); + expect(validRegex.test("my-change!")).toBe(false); + expect(validRegex.test("my.change")).toBe(false); + expect(validRegex.test("")).toBe(false); + }); +});