diff --git a/docs/test-plans/10-fix-weak-tests.md b/docs/test-plans/10-fix-weak-tests.md new file mode 100644 index 0000000..7fe0353 --- /dev/null +++ b/docs/test-plans/10-fix-weak-tests.md @@ -0,0 +1,361 @@ +# Plan 10 — 修复 WEAK 评分测试文件 + +> 优先级:高 | 8 个文件 | 预估新增/修改 ~60 个测试用例 + +本计划修复 testing-spec.md 中评定为 WEAK 的 8 个测试文件的断言缺陷和覆盖缺口。 + +--- + +## 10.1 `src/utils/__tests__/format.test.ts` + +**问题**:`formatNumber`、`formatTokens`、`formatRelativeTime` 使用 `toContain` 代替精确匹配,无法检测格式回归。 + +### 修改清单 + +#### formatNumber — toContain → toBe + +```typescript +// 当前(弱) +expect(formatNumber(1321)).toContain("k"); +expect(formatNumber(1500000)).toContain("m"); + +// 修复为 +expect(formatNumber(1321)).toBe("1.3k"); +expect(formatNumber(1500000)).toBe("1.5m"); +``` + +> 注意:`Intl.NumberFormat` 输出可能因 locale 不同。若 CI locale 不一致,改用 `toMatch(/^\d+(\.\d)?[km]$/)` 正则匹配。 + +#### formatTokens — 补精确断言 + +```typescript +expect(formatTokens(1000)).toBe("1k"); +expect(formatTokens(1500)).toBe("1.5k"); +``` + +#### formatRelativeTime — toContain → toBe + +```typescript +// 当前(弱) +expect(formatRelativeTime(diff, now)).toContain("30"); +expect(formatRelativeTime(diff, now)).toContain("ago"); + +// 修复为 +expect(formatRelativeTime(diff, now)).toBe("30s ago"); +``` + +#### 新增:formatDuration 进位边界 + +| 用例 | 输入 | 期望 | +|------|------|------| +| 59.5s 进位 | 59500ms | 至少含 `1m` | +| 59m59s 进位 | 3599000ms | 至少含 `1h` | +| sub-millisecond | 0.5ms | `"<1ms"` 或 `"0ms"` | + +#### 新增:未测试函数 + +| 函数 | 最少用例 | +|------|---------| +| `formatRelativeTimeAgo` | 2(过去 / 未来) | +| `formatLogMetadata` | 1(基本调用不抛错) | +| `formatResetTime` | 2(有值 / null) | +| `formatResetText` | 1(基本调用) | + +--- + +## 10.2 `src/tools/shared/__tests__/gitOperationTracking.test.ts` + +**问题**:`detectGitOperation` 内部调用 `getCommitCounter()`、`getPrCounter()`、`logEvent()`,测试产生分析副作用。 + +### 修改清单 + +#### 添加 analytics mock + +在文件顶部添加 `mock.module`: + +```typescript +import { mock, afterAll, afterEach, beforeEach } from "bun:test"; + +mock.module("src/services/analytics/index.ts", () => ({ + logEvent: mock(() => {}), +})); + +mock.module("src/bootstrap/state.ts", () => ({ + getCommitCounter: mock(() => ({ increment: mock(() => {}) })), + getPrCounter: mock(() => ({ increment: mock(() => {}) })), +})); +``` + +> 需验证 `detectGitOperation` 的实际导入路径,按需调整 mock 目标。 + +#### 新增:缺失的 GH PR actions + +| 用例 | 输入 | 期望 | +|------|------|------| +| gh pr edit | `'gh pr edit 123 --title "fix"'` | `result.pr.number === 123` | +| gh pr close | `'gh pr close 456'` | `result.pr.number === 456` | +| gh pr ready | `'gh pr ready 789'` | `result.pr.number === 789` | +| gh pr comment | `'gh pr comment 123 --body "done"'` | `result.pr.number === 123` | + +#### 新增:parseGitCommitId 边界 + +| 用例 | 输入 | 期望 | +|------|------|------| +| 完整 40 字符 SHA | `'[abcdef0123456789abcdef0123456789abcdef01] ...'` | 返回完整 40 字符 | +| 畸形括号输出 | `'create mode 100644 file.txt'` | 返回 `null` | + +--- + +## 10.3 `src/utils/permissions/__tests__/PermissionMode.test.ts` + +**问题**:`isExternalPermissionMode` 在非 ant 环境永远返回 true,false 路径从未执行;mode 覆盖不完整。 + +### 修改清单 + +#### 补全 mode 覆盖 + +| 函数 | 缺失的 mode | +|------|-------------| +| `permissionModeTitle` | `bypassPermissions`, `dontAsk` | +| `permissionModeShortTitle` | `dontAsk`, `acceptEdits` | +| `getModeColor` | `dontAsk`, `acceptEdits`, `plan` | +| `permissionModeFromString` | `acceptEdits`, `bypassPermissions` | +| `toExternalPermissionMode` | `acceptEdits`, `bypassPermissions` | + +#### 修复 isExternalPermissionMode + +```typescript +// 当前:只测了非 ant 环境(永远 true) +// 需要新增 ant 环境测试 +describe("when USER_TYPE is 'ant'", () => { + beforeEach(() => { + process.env.USER_TYPE = "ant"; + }); + afterEach(() => { + delete process.env.USER_TYPE; + }); + + test("returns false for 'auto' in ant context", () => { + expect(isExternalPermissionMode("auto")).toBe(false); + }); + + test("returns false for 'bubble' in ant context", () => { + expect(isExternalPermissionMode("bubble")).toBe(false); + }); + + test("returns true for non-ant modes in ant context", () => { + expect(isExternalPermissionMode("plan")).toBe(true); + }); +}); +``` + +#### 新增:permissionModeSchema + +| 用例 | 输入 | 期望 | +|------|------|------| +| 有效 mode | `'plan'` | `success: true` | +| 无效 mode | `'invalid'` | `success: false` | + +--- + +## 10.4 `src/utils/permissions/__tests__/dangerousPatterns.test.ts` + +**问题**:纯数据 smoke test,无行为验证。 + +### 修改清单 + +#### 新增:重复值检查 + +```typescript +test("CROSS_PLATFORM_CODE_EXEC has no duplicates", () => { + const set = new Set(CROSS_PLATFORM_CODE_EXEC); + expect(set.size).toBe(CROSS_PLATFORM_CODE_EXEC.length); +}); + +test("DANGEROUS_BASH_PATTERNS has no duplicates", () => { + const set = new Set(DANGEROUS_BASH_PATTERNS); + expect(set.size).toBe(DANGEROUS_BASH_PATTERNS.length); +}); +``` + +#### 新增:全量成员断言(用 Set 确保精确) + +```typescript +test("CROSS_PLATFORM_CODE_EXEC contains expected interpreters", () => { + const expected = ["node", "python", "python3", "ruby", "perl", "php", + "bun", "deno", "npx", "tsx"]; + const set = new Set(CROSS_PLATFORM_CODE_EXEC); + for (const entry of expected) { + expect(set.has(entry)).toBe(true); + } +}); +``` + +#### 新增:空字符串不匹配 + +```typescript +test("empty string does not match any pattern", () => { + for (const pattern of DANGEROUS_BASH_PATTERNS) { + expect("".startsWith(pattern)).toBe(false); + } +}); +``` + +--- + +## 10.5 `src/utils/__tests__/zodToJsonSchema.test.ts` + +**问题**:object 属性仅 `toBeDefined` 未验证类型结构;optional 字段未验证 absence。 + +### 修改清单 + +#### 修复 object schema 测试 + +```typescript +// 当前(弱) +expect(schema.properties!.name).toBeDefined(); +expect(schema.properties!.age).toBeDefined(); + +// 修复为 +expect(schema.properties!.name).toEqual({ type: "string" }); +expect(schema.properties!.age).toEqual({ type: "number" }); +``` + +#### 修复 optional 字段测试 + +```typescript +test("optional field is not in required array", () => { + const schema = zodToJsonSchema(z.object({ + required: z.string(), + optional: z.string().optional(), + })); + expect(schema.required).toEqual(["required"]); + expect(schema.required).not.toContain("optional"); +}); +``` + +#### 新增:缺失的 schema 类型 + +| 用例 | 输入 | 期望 | +|------|------|------| +| `z.literal("foo")` | `z.literal("foo")` | `{ const: "foo" }` | +| `z.null()` | `z.null()` | `{ type: "null" }` | +| `z.union()` | `z.union([z.string(), z.number()])` | `{ anyOf: [...] }` | +| `z.record()` | `z.record(z.string(), z.number())` | `{ type: "object", additionalProperties: { type: "number" } }` | +| `z.tuple()` | `z.tuple([z.string(), z.number()])` | `{ type: "array", items: [...], additionalItems: false }` | +| 嵌套 object | `z.object({ a: z.object({ b: z.string() }) })` | 验证嵌套属性结构 | + +--- + +## 10.6 `src/utils/__tests__/envValidation.test.ts` + +**问题**:`validateBoundedIntEnvVar` lower bound=100 时 value=1 返回 `status: "valid"`,疑似源码 bug。 + +### 修改清单 + +#### 验证 lower bound 行为 + +```typescript +// 当前测试 +test("value of 1 with lower bound 100", () => { + const result = validateBoundedIntEnvVar("1", { defaultValue: 100, upperLimit: 1000, lowerLimit: 100 }); + // 如果源码有 bug,这里应该暴露 + expect(result.effective).toBeGreaterThanOrEqual(100); + expect(result.status).toBe(result.effective !== 100 ? "capped" : "valid"); +}); +``` + +#### 新增边界用例 + +| 用例 | value | lowerLimit | 期望 | +|------|-------|------------|------| +| 低于 lower bound | `"50"` | 100 | `effective: 100, status: "capped"` | +| 等于 lower bound | `"100"` | 100 | `effective: 100, status: "valid"` | +| 浮点截断 | `"50.7"` | 100 | `effective: 100`(parseInt 截断后 cap) | +| 空白字符 | `" 500 "` | 1 | `effective: 500, status: "valid"` | +| defaultValue 为 0 | `"0"` | 0 | 需确认 `parsed <= 0` 逻辑 | + +> **行动**:先确认 `validateBoundedIntEnvVar` 源码中 lower bound 的实际执行路径。如果确实不生效,需先修源码再补测试。 + +--- + +## 10.7 `src/utils/__tests__/file.test.ts` + +**问题**:`addLineNumbers` 仅 `toContain`,未验证完整格式。 + +### 修改清单 + +#### 修复 addLineNumbers 断言 + +```typescript +// 当前(弱) +expect(result).toContain("1"); +expect(result).toContain("hello"); + +// 修复为(需确定 isCompactLinePrefixEnabled 行为) +// 假设 compact=false,格式为 " 1→hello" +test("formats single line with tab prefix", () => { + // 先确认环境,如果 compact 模式不确定,用正则 + expect(result).toMatch(/^\s*\d+[→\t]hello$/m); +}); +``` + +#### 新增:stripLineNumberPrefix 边界 + +| 用例 | 输入 | 期望 | +|------|------|------| +| 纯数字行 | `"123"` | `""` | +| 无内容前缀 | `"→"` | `""` | +| compact 格式 `"1\thello"` | `"1\thello"` | `"hello"` | + +#### 新增:pathsEqual 边界 + +| 用例 | a | b | 期望 | +|------|---|---|------| +| 尾部斜杠差异 | `"/a/b"` | `"/a/b/"` | `false` | +| `..` 段 | `"/a/../b"` | `"/b"` | 视实现而定 | + +--- + +## 10.8 `src/utils/__tests__/notebook.test.ts` + +**问题**:`mapNotebookCellsToToolResult` 内容检查用 `toContain`,未验证 XML 格式。 + +### 修改清单 + +#### 修复 content 断言 + +```typescript +// 当前(弱) +expect(result).toContain("cell-0"); +expect(result).toContain("print('hello')"); + +// 修复为 +expect(result).toContain(''); +expect(result).toContain(""); +``` + +#### 新增:parseCellId 边界 + +| 用例 | 输入 | 期望 | +|------|------|------| +| 负数 | `"cell--1"` | `null` | +| 前导零 | `"cell-007"` | `7` | +| 极大数 | `"cell-999999999"` | `999999999` | + +#### 新增:mapNotebookCellsToToolResult 边界 + +| 用例 | 输入 | 期望 | +|------|------|------| +| 空 data 数组 | `{ cells: [] }` | 空字符串或空结果 | +| 无 cell_id | `{ cell_type: "code", source: "x" }` | fallback 到 `cell-${index}` | +| error output | `{ output_type: "error", ename: "Error", evalue: "msg" }` | 包含 error 信息 | + +--- + +## 验收标准 + +- [ ] `bun test` 全部通过 +- [ ] 8 个文件评分从 WEAK 提升至 ACCEPTABLE 或 GOOD +- [ ] `toContain` 仅用于警告文本等确实不确定精确值的场景 +- [ ] envValidation bug 确认并修复(或确认非 bug 并更新测试) diff --git a/docs/test-plans/11-strengthen-acceptable-tests.md b/docs/test-plans/11-strengthen-acceptable-tests.md new file mode 100644 index 0000000..c1f563c --- /dev/null +++ b/docs/test-plans/11-strengthen-acceptable-tests.md @@ -0,0 +1,177 @@ +# Plan 11 — 加强 ACCEPTABLE 评分测试 + +> 优先级:中 | ~15 个文件 | 预估新增 ~80 个测试用例 + +本计划对 ACCEPTABLE 评分文件中的具体缺陷进行定向加强。每个条目只列出需要改动的部分,不做全量重写。 + +--- + +## 11.1 `src/utils/__tests__/diff.test.ts` + +| 改动 | 当前 | 改为 | +|------|------|------| +| `getPatchFromContents` 断言 | `hunks.length > 0` | 验证具体 `+`/`-` 行内容 | +| `$` 字符转义 | 未测试 | 新增含 `$` 的内容测试 | +| `ignoreWhitespace` 选项 | 未测试 | 新增 `ignoreWhitespace: true` 用例 | +| 删除全部内容 | 未测试 | `newContent: ""` | +| 多 hunk 偏移 | `adjustHunkLineNumbers` 仅单 hunk | 新增多 hunk 同数组测试 | + +--- + +## 11.2 `src/utils/__tests__/path.test.ts` + +当前仅覆盖 2/5+ 导出函数。新增: + +| 函数 | 最少用例 | 关键边界 | +|------|---------|---------| +| `expandPath` | 6 | `~/` 展开、绝对路径直通、相对路径、空串、含 null 字节、`~user` 格式 | +| `toRelativePath` | 3 | 同级文件、子目录、父目录 | +| `sanitizePath` | 3 | 正常路径、含 `..` 段、空串 | + +`containsPathTraversal` 补充: +- URL 编码 `%2e%2e%2f`(确认不匹配,记录为非需求) +- 混合分隔符 `foo/..\bar` + +`normalizePathForConfigKey` 补充: +- 混合分隔符 `foo/bar\baz` +- 冗余分隔符 `foo//bar` +- Windows 盘符 `C:\foo\bar` + +--- + +## 11.3 `src/utils/__tests__/uuid.test.ts` + +| 改动 | 说明 | +|------|------| +| 大写测试断言强化 | `not.toBeNull()` → 验证标准化输出(小写+连字符格式) | +| 新增 `createAgentId` | 3 用例:无 label / 有 label / 输出格式正则 `/^a[a-z]*-[a-f0-9]{16}$/` | +| 前后空白 | `" 550e8400-... "` 期望 `null` | + +--- + +## 11.4 `src/utils/__tests__/semver.test.ts` + +| 用例 | 输入 | 期望 | +|------|------|------| +| pre-release 比较 | `gt("1.0.0", "1.0.0-alpha")` | `true` | +| pre-release 间比较 | `order("1.0.0-alpha", "1.0.0-beta")` | `-1` | +| tilde range | `satisfies("1.2.5", "~1.2.3")` | `true` | +| `*` 通配符 | `satisfies("2.0.0", "*")` | `true` | +| 畸形版本 | `order("abc", "1.0.0")` | 确认不抛错 | +| `0.0.0` | `gt("0.0.0", "0.0.0")` | `false` | + +--- + +## 11.5 `src/utils/__tests__/hash.test.ts` + +| 改动 | 当前 | 改为 | +|------|------|------| +| djb2 32 位检查 | `hash \| 0`(恒 true) | `Number.isSafeInteger(hash) && Math.abs(hash) <= 0x7FFFFFFF` | +| hashContent 空串 | 未测试 | 新增 | +| hashContent 格式 | 未验证输出为数字串 | `toMatch(/^\d+$/)` | +| hashPair 空串 | 未测试 | `hashPair("", "b")`, `hashPair("", "")` | +| 已知答案 | 无 | 断言 `djb2Hash("hello")` 为特定值(需先在控制台运行一次确定) | + +--- + +## 11.6 `src/utils/__tests__/claudemd.test.ts` + +当前仅覆盖 3 个辅助函数。新增: + +| 用例 | 函数 | 说明 | +|------|------|------| +| 未闭合注释 | `stripHtmlComments` | `"text"` → `"text"` | +| 同行注释+内容 | `stripHtmlComments` | `"some text"` → `"some text"` | +| 内联代码中的注释 | `stripHtmlComments` | `` `` `` → 保留 | +| 大小写不敏感 | `isMemoryFilePath` | `"claude.md"`, `"CLAUDE.MD"` | +| 非 .md 规则文件 | `isMemoryFilePath` | `.claude/rules/foo.txt` → `false` | +| 空数组 | `getLargeMemoryFiles` | `[]` → `[]` | + +--- + +## 11.7 `src/tools/FileEditTool/__tests__/utils.test.ts` + +| 函数 | 新增用例 | +|------|---------| +| `normalizeQuotes` | 混合引号 `"`she said 'hello'"` | +| `stripTrailingWhitespace` | CR-only `\r`、无尾部换行、全空白串 | +| `findActualString` | 空 content、Unicode content | +| `preserveQuoteStyle` | 单引号、缩写中的撇号(如 `it's`)、空串 | +| `applyEditToFile` | `replaceAll=true` 零匹配、`oldString` 无尾部 `\n`、多行内容 | + +--- + +## 11.8 `src/utils/model/__tests__/providers.test.ts` + +| 改动 | 说明 | +|------|------| +| 删除 `originalEnv` | 未使用,消除死代码 | +| env 恢复改为快照 | `beforeEach` 保存 `process.env`,`afterEach` 恢复 | +| 新增三变量同时设置 | bedrock + vertex + foundry 全部为 `"1"`,验证优先级 | +| 新增非 `"1"` 值 | `"true"`, `"0"`, `""` | +| `isFirstPartyAnthropicBaseUrl` | URL 含路径 `/v1`、含尾斜杠、非 HTTPS | + +--- + +## 11.9 `src/utils/__tests__/hyperlink.test.ts` + +| 用例 | 说明 | +|------|------| +| 空 URL | `createHyperlink("http://x.com", "", { supported: true })` 不抛错 | +| undefined supportsHyperlinks | 选项未传时走默认检测 | +| 非 ant staging URL | `USER_TYPE !== "ant"` 时 staging 返回 `false` | + +--- + +## 11.10 `src/utils/__tests__/objectGroupBy.test.ts` + +| 用例 | 说明 | +|------|------| +| key 返回 undefined | `(_, i) => undefined` → 全部归入 `undefined` 组 | +| key 为特殊字符 | `({ name }) => name` 含空格/中文 | + +--- + +## 11.11 `src/utils/__tests__/CircularBuffer.test.ts` + +| 用例 | 说明 | +|------|------| +| capacity=1 | 添加 2 个元素,仅保留最后一个 | +| 空 buffer 调用 getRecent | 返回空数组 | +| getRecent(0) | 返回空数组 | + +--- + +## 11.12 `src/utils/__tests__/contentArray.test.ts` + +| 用例 | 说明 | +|------|------| +| 混合交替 | `[tool_result, text, tool_result]` — 验证插入到正确位置 | + +--- + +## 11.13 `src/utils/__tests__/argumentSubstitution.test.ts` + +| 用例 | 说明 | +|------|------| +| 转义引号 | `"he said \"hello\""` | +| 越界索引 | `$ARGUMENTS[99]`(参数不够时) | +| 多占位符 | `"cmd $0 $1 $0"` | + +--- + +## 11.14 `src/utils/__tests__/messages.test.ts` + +| 改动 | 说明 | +|------|------| +| `normalizeMessages` 断言加强 | 验证拆分后的消息内容,不只是长度 | +| `isNotEmptyMessage` 空白 | `[{ type: "text", text: " " }]` | + +--- + +## 验收标准 + +- [ ] `bun test` 全部通过 +- [ ] 目标文件评分从 ACCEPTABLE 提升至 GOOD +- [ ] 无 `toContain` 用于精确值检查的场景 diff --git a/docs/test-plans/12-mock-reliability.md b/docs/test-plans/12-mock-reliability.md new file mode 100644 index 0000000..0deb02d --- /dev/null +++ b/docs/test-plans/12-mock-reliability.md @@ -0,0 +1,145 @@ +# Plan 12 — Mock 可靠性修复 + +> 优先级:高 | 影响 4 个测试文件 | 预估修改 ~15 处 + +本计划修复测试中 mock 相关的副作用、状态泄漏和虚假测试。 + +--- + +## 12.1 `gitOperationTracking.test.ts` — 消除分析副作用 + +**当前问题**:`detectGitOperation` 内部调用 `logEvent()`、`getCommitCounter().increment()`、`getPrCounter().increment()`,每次测试运行都触发真实分析代码。 + +**修复步骤**: + +1. 读取 `src/tools/shared/gitOperationTracking.ts`,确认 analytics 导入路径 +2. 在测试文件顶部添加 `mock.module`: + +```typescript +import { mock } from "bun:test"; + +mock.module("src/services/analytics/index.ts", () => ({ + logEvent: mock(() => {}), + // 按需补充其他导出 +})); +``` + +3. 如果 `getCommitCounter` / `getPrCounter` 来自 `src/bootstrap/state.ts`: + +```typescript +mock.module("src/bootstrap/state.ts", () => ({ + getCommitCounter: mock(() => ({ increment: mock(() => {}) })), + getPrCounter: mock(() => ({ increment: mock(() => {}) })), + // 保留其他被测函数实际需要的导出 +})); +``` + +4. 使用 `await import()` 模式加载被测模块 +5. 运行测试验证无副作用 + +**风险**:`mock.module` 会替换整个模块。如果 `detectGitOperation` 还需要其他来自这些模块的导出,需在 mock 工厂中提供。 + +--- + +## 12.2 `PermissionMode.test.ts` — 修复 `isExternalPermissionMode` 虚假测试 + +**当前问题**:`isExternalPermissionMode` 依赖 `process.env.USER_TYPE`。非 ant 环境下所有 mode 都返回 true,测试从未覆盖 false 分支。 + +**修复步骤**: + +1. 新增 ant 环境测试组(见 Plan 10.3 详细用例) +2. 使用 `beforeEach`/`afterEach` 管理 `process.env.USER_TYPE` + +```typescript +describe("when USER_TYPE is 'ant'", () => { + const originalUserType = process.env.USER_TYPE; + beforeEach(() => { process.env.USER_TYPE = "ant"; }); + afterEach(() => { + if (originalUserType !== undefined) { + process.env.USER_TYPE = originalUserType; + } else { + delete process.env.USER_TYPE; + } + }); + + test("returns false for 'auto'", () => { + expect(isExternalPermissionMode("auto")).toBe(false); + }); + test("returns false for 'bubble'", () => { + expect(isExternalPermissionMode("bubble")).toBe(false); + }); + test("returns true for 'plan'", () => { + expect(isExternalPermissionMode("plan")).toBe(true); + }); +}); +``` + +3. 验证新增测试确实执行 false 路径 + +--- + +## 12.3 `providers.test.ts` — 环境变量快照恢复 + +**当前问题**: +- `originalEnv` 声明后未使用 +- `afterEach` 仅删除已知 3 个 key,如果源码新增 env var,测试间状态泄漏 + +**修复步骤**: + +```typescript +let savedEnv: Record; + +beforeEach(() => { + savedEnv = {}; + for (const key of Object.keys(process.env)) { + savedEnv[key] = process.env[key]; + } +}); + +afterEach(() => { + // 删除所有当前 env,恢复快照 + for (const key of Object.keys(process.env)) { + delete process.env[key]; + } + for (const [key, value] of Object.entries(savedEnv)) { + if (value !== undefined) { + process.env[key] = value; + } + } +}); +``` + +> 简化方案:只保存/恢复相关 key 列表 `["CLAUDE_CODE_USE_BEDROCK", "CLAUDE_CODE_USE_VERTEX", "CLAUDE_CODE_USE_FOUNDRY", "ANTHROPIC_BASE_URL", "USER_TYPE"]`,但需注释说明新增 env var 时需同步更新。 + +--- + +## 12.4 `envUtils.test.ts` — 验证环境变量恢复完整性 + +**当前状态**:已有 `afterEach` 恢复。需审查: + +1. 确认所有 `describe` 块中的 `afterEach` 都完整恢复了修改的 env var +2. 确认 `process.argv` 修改也被恢复(`getClaudeConfigHomeDir` 测试修改了 argv) +3. 新增:`afterEach` 中断言无意外 env 泄漏(可选,CI-only) + +--- + +## 12.5 `sleep.test.ts` / `memoize.test.ts` — 时间敏感测试加固 + +**当前状态**:已有合理 margin。可选加固: + +| 文件 | 用例 | 当前 | 加固 | +|------|------|------|------| +| `sleep.test.ts` | `resolves after timeout` | `sleep(50)`, check `>= 40ms` | 增大 margin:`sleep(50)`, check `>= 30ms` | +| `memoize.test.ts` | stale serve & refresh | TTL=1ms, wait 10ms | 增大 margin:TTL=5ms, wait 50ms | + +> 仅在 CI 出现 flaky 时执行此加固。 + +--- + +## 验收标准 + +- [ ] `gitOperationTracking.test.ts` 无分析副作用(可通过在 mock 中增加 `expect(logEvent).toHaveBeenCalledTimes(N)` 验证) +- [ ] `PermissionMode.test.ts` 的 `isExternalPermissionMode` 覆盖 true + false 分支 +- [ ] `providers.test.ts` 的 `originalEnv` 死代码已删除 +- [ ] 所有修改 env 的测试文件恢复完整 +- [ ] `bun test` 全部通过 diff --git a/docs/test-plans/13-cjk-truncate-tests.md b/docs/test-plans/13-cjk-truncate-tests.md new file mode 100644 index 0000000..1bd1cd7 --- /dev/null +++ b/docs/test-plans/13-cjk-truncate-tests.md @@ -0,0 +1,71 @@ +# Plan 13 — truncate CJK/Emoji 补充测试 + +> 优先级:中 | 1 个文件 | 预估新增 ~15 个测试用例 + +`truncate.ts` 使用 `stringWidth` 和 grapheme segmentation 实现宽度感知截断,但现有测试仅覆盖 ASCII。这是核心场景缺失。 + +--- + +## 被测函数 + +- `truncateToWidth(text, maxWidth)` — 尾部截断加 `…` +- `truncateStartToWidth(text, maxWidth)` — 头部截断加 `…` +- `truncateToWidthNoEllipsis(text, maxWidth)` — 尾部截断无省略号 +- `truncatePathMiddle(path, maxLength)` — 路径中间截断 +- `wrapText(text, maxWidth)` — 按宽度换行 + +--- + +## 新增用例 + +### CJK 全角字符 + +| 用例 | 函数 | 输入 | maxWidth | 期望行为 | +|------|------|------|----------|----------| +| 纯中文截断 | `truncateToWidth` | `"你好世界"` | 4 | `"你好…"` (每个中文字占 2 宽度) | +| 中英混合 | `truncateToWidth` | `"hello你好"` | 8 | `"hello你…"` | +| 全角不截断 | `truncateToWidth` | `"你好"` | 4 | `"你好"` (恰好 4) | +| emoji 单字符 | `truncateToWidth` | `"👋"` | 2 | `"👋"` (emoji 通常 2 宽度) | +| emoji 截断 | `truncateToWidth` | `"hello 👋 world"` | 8 | 确认宽度计算正确 | +| 头部中文 | `truncateStartToWidth` | `"你好世界"` | 4 | `"…界"` | +| 无省略中文 | `truncateToWidthNoEllipsis` | `"你好世界"` | 4 | `"你好"` | + +> **注意**:`stringWidth` 对 CJK/emoji 的宽度计算取决于具体实现。先在 REPL 中运行确认实际宽度再写断言: +> ```typescript +> import { stringWidth } from "src/utils/truncate.ts"; +> console.log(stringWidth("你好")); // 确认是 4 还是 2 +> console.log(stringWidth("👋")); // 确认 emoji 宽度 +> ``` + +### 路径中间截断补充 + +| 用例 | 输入 | maxLength | 期望 | +|------|------|-----------|------| +| 文件名超长 | `"/very/long/path/to/MyComponent.tsx"` | 10 | 含 `…` 且以 `.tsx` 结尾 | +| 无斜杠短串 | `"abc"` | 1 | 确认行为不抛错 | +| maxLength 极小 | `"/a/b"` | 1 | 确认不抛错 | +| maxLength=4 | `"/a/b/c.ts"` | 4 | 确认行为 | + +### wrapText 补充 + +| 用例 | 输入 | maxWidth | 期望 | +|------|------|----------|------| +| 含换行符 | `"hello\nworld"` | 10 | 保留原有换行 | +| 宽度=0 | `"hello"` | 0 | 空串或原串(确认不抛错) | + +--- + +## 实施步骤 + +1. 在 REPL 中确认 `stringWidth` 对 CJK/emoji 的实际返回值 +2. 按实际值编写精确断言 +3. 如果 `stringWidth` 依赖 ICU 或平台特性,添加平台检查(`process.platform !== "win32"` 跳过条件) +4. 运行测试 + +--- + +## 验收标准 + +- [ ] 至少 5 个 CJK/emoji 相关测试通过 +- [ ] 断言基于实际 `stringWidth` 返回值,非猜测 +- [ ] `bun test` 全部通过 diff --git a/docs/test-plans/14-integration-tests.md b/docs/test-plans/14-integration-tests.md new file mode 100644 index 0000000..9777a87 --- /dev/null +++ b/docs/test-plans/14-integration-tests.md @@ -0,0 +1,191 @@ +# Plan 14 — 集成测试搭建 + +> 优先级:中 | 新建 ~3 个测试文件 | 预估 ~30 个测试用例 + +当前 `tests/integration/` 目录为空,spec 设计的三个集成测试均未创建。本计划搭建 mock 基础设施并实现核心集成测试。 + +--- + +## 14.1 搭建 `tests/mocks/` 基础设施 + +### 文件结构 + +``` +tests/ +├── mocks/ +│ ├── api-responses.ts # Claude API mock 响应 +│ ├── file-system.ts # 临时文件系统工具 +│ └── fixtures/ +│ ├── sample-claudemd.md # CLAUDE.md 样本 +│ └── sample-messages.json # 消息样本 +├── integration/ +│ ├── tool-chain.test.ts +│ ├── context-build.test.ts +│ └── message-pipeline.test.ts +└── helpers/ + └── setup.ts # 共享 beforeAll/afterAll +``` + +### `tests/mocks/file-system.ts` + +```typescript +import { mkdtemp, rm, writeFile, mkdir } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +export async function createTempDir(prefix = "claude-test-"): Promise { + const dir = await mkdtemp(join(tmpdir(), prefix)); + return dir; +} + +export async function cleanupTempDir(dir: string): Promise { + await rm(dir, { recursive: true, force: true }); +} + +export async function writeTempFile(dir: string, name: string, content: string): Promise { + const path = join(dir, name); + await writeFile(path, content, "utf-8"); + return path; +} +``` + +### `tests/mocks/fixtures/sample-claudemd.md` + +```markdown +# Project Instructions + +This is a sample CLAUDE.md file for testing. +``` + +### `tests/mocks/api-responses.ts` + +```typescript +export const mockStreamResponse = { + type: "message_start" as const, + message: { + id: "msg_mock_001", + type: "message" as const, + role: "assistant", + content: [], + model: "claude-sonnet-4-20250514", + stop_reason: null, + stop_sequence: null, + usage: { input_tokens: 100, output_tokens: 0 }, + }, +}; + +export const mockTextBlock = { + type: "content_block_start" as const, + index: 0, + content_block: { type: "text" as const, text: "Mock response" }, +}; + +export const mockToolUseBlock = { + type: "content_block_start" as const, + index: 1, + content_block: { + type: "tool_use" as const, + id: "toolu_mock_001", + name: "Read", + input: { file_path: "/tmp/test.txt" }, + }, +}; + +export const mockEndEvent = { + type: "message_stop" as const, +}; +``` + +--- + +## 14.2 `tests/integration/tool-chain.test.ts` + +**目标**:验证 Tool 注册 → 发现 → 权限检查链路。 + +### 前置条件 + +`src/tools.ts` 的 `getAllBaseTools` / `getTools` 导入链过重。策略: +- 尝试直接 import 并 mock 最重依赖 +- 若不可行,改为测试 `src/Tool.ts` 的 `findToolByName` + 手动构造 tool 列表 + +### 用例 + +| # | 用例 | 验证点 | +|---|------|--------| +| 1 | `findToolByName("Bash")` 在已注册列表中查找 | 返回正确的 tool 定义 | +| 2 | `findToolByName("NonExistent")` | 返回 `undefined` | +| 3 | `findToolByName` 大小写不敏感 | `"bash"` 也能找到 | +| 4 | `filterToolsByDenyRules` 拒绝特定工具 | 被拒绝工具不在结果中 | +| 5 | `parseToolPreset("default")` 返回已知列表 | 包含核心 tools | +| 6 | `buildTool` 构建的 tool 可被 `findToolByName` 发现 | 端到端验证 | + +> 如果 `getAllBaseTools` 确实不可导入,改用 mock tool list 替代。 + +--- + +## 14.3 `tests/integration/context-build.test.ts` + +**目标**:验证系统提示组装流程(CLAUDE.md 加载 + git status + 日期注入)。 + +### 前置条件 + +`src/context.ts` 依赖链极重。策略: +- Mock `src/bootstrap/state.ts`(提供 cwd、projectRoot) +- Mock `src/utils/git.ts`(提供 git status) +- 使用真实 `src/utils/claudemd.ts` + 临时文件 + +### 用例 + +| # | 用例 | 验证点 | +|---|------|--------| +| 1 | 基本 context 构建 | 返回值包含系统提示字符串 | +| 2 | CLAUDE.md 内容出现在 context 中 | `stripHtmlComments` 后的内容被包含 | +| 3 | 多层目录 CLAUDE.md 合并 | 父目录 + 子目录 CLAUDE.md 都被加载 | +| 4 | 无 CLAUDE.md 时不报错 | context 正常返回,无 crash | +| 5 | git status 为 null | context 正常构建(测试环境中 git 不可用时) | + +> **风险评估**:如果 mock `context.ts` 的依赖链成本过高,退化为测试 `buildEffectiveSystemPrompt`(已在 systemPrompt.test.ts 中完成),记录为已知限制。 + +--- + +## 14.4 `tests/integration/message-pipeline.test.ts` + +**目标**:验证用户输入 → 消息格式化 → API 请求构建。 + +### 前置条件 + +`src/services/api/claude.ts` 构建最终 API 请求。策略: +- Mock Anthropic SDK 的 streaming endpoint +- 验证请求参数结构 + +### 用例 + +| # | 用例 | 验证点 | +|---|------|--------| +| 1 | 文本消息格式化 | `createUserMessage` 生成正确 role+content | +| 2 | tool_result 消息格式化 | 包含 tool_use_id 和 content | +| 3 | 多轮消息序列化 | messages 数组保持顺序 | +| 4 | 系统提示注入到请求 | API 请求的 system 字段非空 | +| 5 | 消息 normalize 后格式一致 | `normalizeMessages` 输出结构正确 | + +> **现实评估**:消息格式化的大部分已在 `messages.test.ts` 覆盖。API 请求构建需要 mock SDK,复杂度高。如果投入产出比低,仅实现用例 1-3 和 5,用例 4 标记为 stretch goal。 + +--- + +## 实施步骤 + +1. 创建 `tests/mocks/` 目录和基础文件 +2. 实现 `tool-chain.test.ts`(最低风险,最高价值) +3. 评估 `context-build.test.ts` 可行性,决定是否实施 +4. 实现 `message-pipeline.test.ts`(可降级为单元测试) +5. 更新 `testing-spec.md` 状态 + +--- + +## 验收标准 + +- [ ] `tests/mocks/` 基础设施可用 +- [ ] 至少 `tool-chain.test.ts` 实现并通过 +- [ ] 集成测试独立于单元测试运行:`bun test tests/integration/` +- [ ] 所有集成测试使用 `createTempDir` + `cleanupTempDir`,不留文件系统残留 +- [ ] `bun test` 全部通过 diff --git a/docs/test-plans/15-cli-coverage-baseline.md b/docs/test-plans/15-cli-coverage-baseline.md new file mode 100644 index 0000000..09fa9ea --- /dev/null +++ b/docs/test-plans/15-cli-coverage-baseline.md @@ -0,0 +1,67 @@ +# Plan 15 — CLI 参数测试 + 覆盖率基线 + +> 优先级:低 | 预估 ~15 个测试用例 + +--- + +## 15.1 `src/main.tsx` CLI 参数测试 + +**目标**:覆盖 Commander.js 配置的参数解析和模式切换。 + +### 前置条件 + +`src/main.tsx` 的 Commander 实例通常在模块顶层创建。测试策略: +- 直接构造 Commander 实例或 mock `main.tsx` 的 program 导出 +- 使用 `parseArgs` 而非 `parse`(不触发 `process.exit`) + +### 用例 + +| # | 用例 | 输入 | 期望 | +|---|------|------|------| +| 1 | 默认模式 | `[]` | 模式为 REPL | +| 2 | pipe 模式 | `["-p"]` | 模式为 pipe | +| 3 | pipe 带输入 | `["-p", "say hello"]` | 输入为 `"say hello"` | +| 4 | print 模式 | `["--print", "hello"]` | 等效于 pipe | +| 5 | verbose | `["-v"]` | verbose 标志为 true | +| 6 | model 选择 | `["--model", "claude-opus-4-6"]` | model 值正确传递 | +| 7 | system prompt | `["--system-prompt", "custom"]` | system prompt 被设置 | +| 8 | help | `["--help"]` | 显示帮助信息,不报错 | +| 9 | version | `["--version"]` | 显示版本号 | +| 10 | unknown flag | `["--nonexistent"]` | 不报错(Commander 允许未知参数时) | + +> **风险**:`main.tsx` 可能执行初始化逻辑(auth、analytics),需要在 mock 环境中运行。如果复杂度过高,降级为只测试参数解析部分。 + +--- + +## 15.2 覆盖率基线 + +### 运行命令 + +```bash +bun test --coverage 2>&1 | tail -50 +``` + +### 记录内容 + +| 模块 | 当前覆盖率 | 目标 | +|------|-----------|------| +| `src/utils/` | 待测量 | >= 80% | +| `src/utils/permissions/` | 待测量 | >= 60% | +| `src/utils/model/` | 待测量 | >= 60% | +| `src/Tool.ts` + `src/tools.ts` | 待测量 | >= 80% | +| `src/utils/claudemd.ts` | 待测量 | >= 40%(核心逻辑难测) | +| 整体 | 待测量 | 不设强制指标 | + +### 后续行动 + +- 将基线数据填入 `testing-spec.md` §4 +- 识别覆盖率最低的 10 个文件,排入后续测试计划 +- 如 `bun test --coverage` 输出不可用(Bun 版本限制),改用手动计算已测/总导出函数比 + +--- + +## 验收标准 + +- [ ] CLI 参数至少覆盖 5 个核心 flag +- [ ] 覆盖率基线数据记录到 testing-spec.md +- [ ] `bun test` 全部通过 diff --git a/docs/testing-spec.md b/docs/testing-spec.md index 4b0c88b..90a9ef5 100644 --- a/docs/testing-spec.md +++ b/docs/testing-spec.md @@ -1,455 +1,256 @@ # Testing Specification -本文档定义了 claude-code 项目的测试规范,作为编写和维护测试代码的统一标准。 +本文档定义 claude-code 项目的测试规范、当前覆盖状态和改进计划。 -## 1. 测试目标 +## 1. 技术栈 -| 目标 | 说明 | -|------|------| -| **防止回归** | 确保已有功能不被新改动破坏,每次 PR 必须通过全部测试 | -| **验证核心流程** | 覆盖 CLI 核心交互流程:Tool 调用链、Context 构建、消息处理 | -| **文档化行为** | 通过测试用例记录各模块的预期行为,作为活文档供开发者参考 | +| 项 | 选型 | +|----|------| +| 测试框架 | `bun:test` | +| 断言/Mock | `bun:test` 内置 | +| 覆盖率 | `bun test --coverage` | +| CI | GitHub Actions,push/PR 到 main 自动运行 | -## 2. 技术栈 - -| 项 | 选型 | 说明 | -|----|------|------| -| 测试框架 | `bun:test` | Bun 内置,零配置,与运行时一致 | -| 断言库 | `bun:test` 内置 `expect` | 兼容 Jest `expect` API | -| Mock | `bun:test` 内置 `mock`/`spyOn` | 配合手动 mock fixtures | -| 覆盖率 | `bun test --coverage` | 内置覆盖率报告 | - -## 3. 测试层次 +## 2. 测试层次 本项目采用 **单元测试 + 集成测试** 两层结构,不做 E2E 或快照测试。 -### 3.1 单元测试 +- **单元测试** — 纯函数、工具类、解析器。文件就近放置于 `src/**/__tests__/`。 +- **集成测试** — 多模块协作流程。集中于 `tests/integration/`。 -- **对象**:纯函数、工具类、解析器、独立模块 -- **特征**:无外部依赖、执行快、可并行 -- **示例场景**: - - `src/utils/array.ts` — 数组操作函数 - - `src/utils/path.ts` — 路径解析 - - `src/utils/diff.ts` — diff 算法 - - `src/utils/permissions/` — 权限判断逻辑 - - `src/utils/model/` — 模型选择与 provider 路由 - - Tool 的 `inputSchema` 校验逻辑 - -### 3.2 集成测试 - -- **对象**:多模块协作流程 -- **特征**:可能需要 mock 外部服务(API、文件系统),测试模块间协作 -- **示例场景**: - - Tool 调用链:`tools.ts` 注册 → `findToolByName` → tool `call()` 执行 - - Context 构建:`context.ts` 组装系统提示(CLAUDE.md 加载 + git status + 日期) - - 消息处理管线:用户输入 → 消息格式化 → API 请求构建 - -## 4. 文件结构 - -采用 **混合模式**:单元测试就近放置,集成测试集中管理。 +## 3. 文件结构与命名 ``` src/ -├── utils/ -│ ├── array.ts -│ ├── __tests__/ # 单元测试:就近放置 -│ │ ├── array.test.ts -│ │ ├── set.test.ts -│ │ └── path.test.ts -│ ├── model/ -│ │ ├── providers.ts -│ │ └── __tests__/ -│ │ └── providers.test.ts -│ └── permissions/ -│ ├── index.ts -│ └── __tests__/ -│ └── permissions.test.ts -├── tools/ -│ ├── BashTool/ -│ │ ├── index.ts -│ │ └── __tests__/ -│ │ └── BashTool.test.ts -│ └── FileEditTool/ -│ ├── index.ts -│ └── __tests__/ -│ └── FileEditTool.test.ts -tests/ # 集成测试:集中管理 -├── integration/ -│ ├── tool-chain.test.ts -│ ├── context-build.test.ts -│ └── message-pipeline.test.ts -├── mocks/ # 通用 mock / fixtures -│ ├── api-responses.ts # Claude API mock 响应 -│ ├── file-system.ts # 文件系统 mock 工具 -│ └── fixtures/ -│ ├── sample-claudemd.md -│ └── sample-messages.json -└── helpers/ # 测试辅助函数 - └── setup.ts +├── utils/__tests__/ # 纯函数单元测试 +├── tools//__tests__/ # Tool 单元测试 +├── services/mcp/__tests__/ # MCP 单元测试 +├── utils/permissions/__tests__/ +├── utils/model/__tests__/ +├── utils/settings/__tests__/ +├── utils/shell/__tests__/ +├── utils/git/__tests__/ +└── __tests__/ # 顶层模块测试 (Tool.ts, tools.ts) +tests/ +├── integration/ # 集成测试(尚未创建) +├── mocks/ # 共享 mock/fixture(尚未创建) +└── helpers/ # 测试辅助函数 ``` -### 命名规则 +- 测试文件:`.test.ts` +- 命名风格:`describe("functionName")` + `test("行为描述")`,英文 +- 编写原则:Arrange-Act-Assert、单一职责、独立性、边界覆盖 -| 项 | 规则 | -|----|------| -| 测试文件 | `.test.ts` | -| 测试目录 | `__tests__/`(单元)、`tests/integration/`(集成) | -| Fixture 文件 | `tests/mocks/fixtures/` 下按用途命名 | -| Helper 文件 | `tests/helpers/` 下按功能命名 | +## 4. 当前覆盖状态 -## 5. 命名与编写规范 +> 更新日期:2026-04-02 | **1177 tests, 64 files, 0 fail, 837ms** -### 5.1 命名风格 +### 4.1 可靠度评分 -使用 `describe` + `it`/`test` 英文描述: +每个测试文件按断言深度、边界覆盖、mock 质量、测试独立性综合评定: -```typescript -import { describe, expect, test } from "bun:test"; - -describe("findToolByName", () => { - test("returns the tool when name matches exactly", () => { - // ... - }); - - test("returns undefined when no tool matches", () => { - // ... - }); - - test("is case-insensitive for tool name lookup", () => { - // ... - }); -}); -``` - -### 5.2 describe 块组织原则 - -- 顶层 `describe` 对应被测函数/类/模块名 -- 可嵌套 `describe` 对分支场景分组(如 `describe("when input is empty", ...)`) -- 每个 `test` 应测试一个行为,命名采用 **"动作 + 预期结果"** 格式 - -### 5.3 编写原则 - -| 原则 | 说明 | +| 等级 | 含义 | |------|------| -| **Arrange-Act-Assert** | 每个测试分三段:准备数据、执行操作、验证结果 | -| **单一职责** | 一个 `test` 只验证一个行为 | -| **独立性** | 测试之间无顺序依赖,无共享可变状态 | -| **可读性优先** | 测试代码是文档,宁可重复也不过度抽象 | -| **边界覆盖** | 空值、边界值、异常输入必须覆盖 | +| **GOOD** | 断言精确(exact match),边界充分,结构清晰 | +| **ACCEPTABLE** | 正常路径覆盖完整,部分边界或断言可加强 | +| **WEAK** | 存在明显缺陷:断言过弱、重要边界缺失、或有脆弱性风险 | -### 5.4 异步测试 +### 4.2 按模块分布 -```typescript -test("reads file content correctly", async () => { - const content = await readFile("/tmp/test.txt"); - expect(content).toContain("expected"); -}); -``` +#### P0 — 核心模块 -## 6. Mock 策略 +| 文件 | Tests | 评分 | 覆盖范围 | 主要不足 | +|------|-------|------|----------|----------| +| `src/__tests__/Tool.test.ts` | 20 | GOOD | buildTool, toolMatchesName, findToolByName, filterToolProgressMessages | — | +| `src/__tests__/tools.test.ts` | 9 | ACCEPTABLE | parseToolPreset, filterToolsByDenyRules | 预设覆盖仅测 "default";有冗余用例 | +| `src/tools/FileEditTool/__tests__/utils.test.ts` | 22 | ACCEPTABLE | normalizeQuotes, applyEditToFile, preserveQuoteStyle | `findActualString` 断言过弱(`not.toBeNull`);`preserveQuoteStyle` 仅 2 用例 | +| `src/tools/shared/__tests__/gitOperationTracking.test.ts` | 14 | WEAK | parseGitCommitId, detectGitOperation | **未 mock analytics 依赖**,测试产生副作用;6 个 GH PR action 仅测 2 个 | +| `src/tools/BashTool/__tests__/destructiveCommandWarning.test.ts` | 21 | ACCEPTABLE | git/rm/SQL/k8s/terraform 危险模式 | safe commands 4 断言合一;缺少 `rm -rf /`、`DROP DATABASE`、管道命令 | +| `src/tools/BashTool/__tests__/commandSemantics.test.ts` | 10 | ACCEPTABLE | grep/diff/test/rg/find 退出码语义 | mock `splitCommand_DEPRECATED` 与实现可能分歧;覆盖可更全面 | -采用 **混合管理**:通用 mock 集中于 `tests/mocks/`,专用 mock 就近定义。 +**Utils 纯函数(19 文件):** -### 6.1 Claude API Mock(集中管理) +| 文件 | Tests | 评分 | 覆盖范围 | 主要不足 | +|------|-------|------|----------|----------| +| `utils/__tests__/array.test.ts` | 12 | GOOD | intersperse, count, uniq | — | +| `utils/__tests__/set.test.ts` | 11 | GOOD | difference, intersects, every, union | — | +| `utils/__tests__/xml.test.ts` | 9 | GOOD | escapeXml, escapeXmlAttr | 缺 null/undefined 输入测试 | +| `utils/__tests__/hash.test.ts` | 12 | ACCEPTABLE | djb2Hash, hashContent, hashPair | `hashContent`/`hashPair` 无已知答案断言(仅测确定性) | +| `utils/__tests__/stringUtils.test.ts` | 30 | GOOD | 10 个函数全覆盖,含 Unicode 边界 | — | +| `utils/__tests__/semver.test.ts` | 16 | ACCEPTABLE | gt/gte/lt/lte/satisfies/order | 缺 pre-release、tilde range、畸形版本串 | +| `utils/__tests__/uuid.test.ts` | 6 | ACCEPTABLE | validateUuid | 大写测试仅 `not.toBeNull`,未验证标准化输出 | +| `utils/__tests__/format.test.ts` | 20 | WEAK | formatFileSize, formatDuration, formatNumber 等 | **多处 `toContain` 应为 `toBe`**:formatNumber/formatTokens/formatRelativeTime 仅检查子串 | +| `utils/__tests__/frontmatterParser.test.ts` | 22 | GOOD | parseFrontmatter, splitPathInFrontmatter, parsePositiveIntFromFrontmatter | — | +| `utils/__tests__/file.test.ts` | 13 | ACCEPTABLE | convertLeadingTabsToSpaces, addLineNumbers, stripLineNumberPrefix | `addLineNumbers` 仅 `toContain`;缺 Windows 路径分隔符测试 | +| `utils/__tests__/glob.test.ts` | 6 | ACCEPTABLE | extractGlobBaseDirectory | 缺绝对路径、根 `/`、Windows 路径 | +| `utils/__tests__/diff.test.ts` | 8 | ACCEPTABLE | adjustHunkLineNumbers, getPatchFromContents | `getPatchFromContents` 仅检查结构,未验证 diff 内容正确性 | +| `utils/__tests__/json.test.ts` | 15 | GOOD | safeParseJSON, parseJSONL, addItemToJSONCArray | — | +| `utils/__tests__/truncate.test.ts` | 18 | ACCEPTABLE | truncateToWidth, wrapText, truncatePathMiddle | **缺 CJK/emoji/wide-char 测试**(这是宽度感知实现的核心场景) | +| `utils/__tests__/path.test.ts` | 15 | ACCEPTABLE | containsPathTraversal, normalizePathForConfigKey | 仅覆盖 2/5+ 导出函数 | +| `utils/__tests__/tokens.test.ts` | 18 | GOOD | getTokenCountFromUsage, doesMostRecentAssistantMessageExceed200k 等 | — | -所有 API 测试全部使用 mock,不调用真实 API。 +**Context 构建(2 文件):** -```typescript -// tests/mocks/api-responses.ts -export const mockStreamResponse = { - type: "message_start", - message: { - id: "msg_mock_001", - type: "message", - role: "assistant", - content: [], - model: "claude-sonnet-4-20250514", - // ... - }, -}; +| 文件 | Tests | 评分 | 覆盖范围 | 主要不足 | +|------|-------|------|----------|----------| +| `utils/__tests__/claudemd.test.ts` | 14 | ACCEPTABLE | stripHtmlComments, isMemoryFilePath, getLargeMemoryFiles | **仅测 3 个辅助函数**,核心发现/加载/`@include` 指令/memoization 未覆盖 | +| `utils/__tests__/systemPrompt.test.ts` | 8 | GOOD | buildEffectiveSystemPrompt | — | -export const mockToolUseResponse = { - type: "content_block_start", - content_block: { - type: "tool_use", - id: "toolu_mock_001", - name: "Read", - input: { file_path: "/tmp/test.txt" }, - }, -}; -``` +#### P1 — 重要模块 -### 6.2 模块级 Mock(就近定义) +| 文件 | Tests | 评分 | 覆盖范围 | 主要不足 | +|------|-------|------|----------|----------| +| `permissions/__tests__/permissionRuleParser.test.ts` | 16 | GOOD | escape/unescape 规则,roundtrip 完整性 | — | +| `permissions/__tests__/permissions.test.ts` | 12 | ACCEPTABLE | getDenyRuleForTool, getAskRuleForTool, filterDeniedAgents | `as any` cast;缺 MCP tool deny 测试 | +| `permissions/__tests__/shellRuleMatching.test.ts` | 19 | GOOD | 通配符、转义、正则特殊字符 | — | +| `permissions/__tests__/PermissionMode.test.ts` | 18 | WEAK | permissionModeFromString, isExternalPermissionMode 等 | **`isExternalPermissionMode` false 路径从未执行**;mode 覆盖不完整(5 选 3) | +| `permissions/__tests__/dangerousPatterns.test.ts` | 7 | WEAK | CROSS_PLATFORM_CODE_EXEC, DANGEROUS_BASH_PATTERNS | 纯数据 smoke test,无行为测试;不验证数组无重复 | +| `model/__tests__/aliases.test.ts` | 15 | ACCEPTABLE | isModelAlias, isModelFamilyAlias | 缺 null/undefined/空串输入 | +| `model/__tests__/model.test.ts` | 13 | ACCEPTABLE | firstPartyNameToCanonical | 缺空串、非标准日期后缀 | +| `model/__tests__/providers.test.ts` | 9 | ACCEPTABLE | getAPIProvider, isFirstPartyAnthropicBaseUrl | `originalEnv` 声明未使用;env 恢复不完整 | +| `utils/__tests__/messages.test.ts` | 36 | GOOD | createAssistantMessage, createUserMessage, extractTag 等 16 个 describe | `normalizeMessages` 仅检查长度未验证内容 | -```typescript -import { mock } from "bun:test"; +#### P2 — 补充模块 -// mock 整个模块 -mock.module("src/services/api/claude.ts", () => ({ - createApiClient: () => ({ - stream: mock(() => mockStreamResponse), - }), -})); -``` +| 文件 | Tests | 评分 | 覆盖范围 | 主要不足 | +|------|-------|------|----------|----------| +| `utils/__tests__/cron.test.ts` | 31 | GOOD | parseCronExpression, computeNextCronRun, cronToHuman | 缺月边界、闰年 | +| `utils/__tests__/git.test.ts` | 15 | ACCEPTABLE | normalizeGitRemoteUrl (SSH/HTTPS/ssh://) | 缺 git://、file://、端口号 | +| `settings/__tests__/config.test.ts` | 38 | GOOD | SettingsSchema, type guards, validateSettingsFileContent, formatZodError | 缺 DeniedMcpServerEntrySchema | -### 6.3 文件系统 Mock +#### P3-P6 — 扩展覆盖(27 文件) -对于需要文件系统交互的测试,使用临时目录: +| 文件 | Tests | 评分 | 备注 | +|------|-------|------|------| +| `utils/__tests__/errors.test.ts` | 33 | GOOD | — | +| `utils/__tests__/envUtils.test.ts` | 33 | GOOD | env 保存/恢复规范 | +| `utils/__tests__/effort.test.ts` | 30 | GOOD | 5 个 mock 模块,边界完整 | +| `utils/__tests__/argumentSubstitution.test.ts` | 22 | ACCEPTABLE | 缺转义引号、越界索引 | +| `utils/__tests__/sanitization.test.ts` | 14 | ACCEPTABLE | — | +| `utils/__tests__/sleep.test.ts` | 14 | GOOD | 时间相关测试,margin 充足 | +| `utils/__tests__/CircularBuffer.test.ts` | 11 | ACCEPTABLE | 缺 capacity=1、空 buffer getRecent | +| `utils/__tests__/memoize.test.ts` | 18 | GOOD | 缓存 hit/stale/LRU 全覆盖 | +| `utils/__tests__/tokenBudget.test.ts` | 21 | GOOD | — | +| `utils/__tests__/displayTags.test.ts` | 17 | GOOD | — | +| `utils/__tests__/taggedId.test.ts` | 10 | GOOD | — | +| `utils/__tests__/controlMessageCompat.test.ts` | 15 | GOOD | — | +| `utils/__tests__/gitConfigParser.test.ts` | 21 | GOOD | — | +| `utils/__tests__/windowsPaths.test.ts` | 19 | GOOD | 双向 round-trip 测试 | +| `utils/__tests__/envExpansion.test.ts` | 15 | GOOD | — | +| `utils/__tests__/formatBriefTimestamp.test.ts` | 10 | GOOD | 固定 now 时间戳,确定性 | +| `utils/__tests__/notebook.test.ts` | 9 | ACCEPTABLE | 合并断言偏弱 | +| `utils/__tests__/hyperlink.test.ts` | 10 | ACCEPTABLE | 空串测试行为注释混乱 | +| `utils/__tests__/zodToJsonSchema.test.ts` | 9 | WEAK | **object 属性仅 `toBeDefined` 未验证类型**;optional 字段未验证 absence | +| `utils/__tests__/objectGroupBy.test.ts` | 5 | ACCEPTABLE | 极简,缺 undefined key 测试 | +| `utils/__tests__/contentArray.test.ts` | 6 | ACCEPTABLE | 缺混合 tool_result+text 交替 | +| `utils/__tests__/slashCommandParsing.test.ts` | 8 | GOOD | — | +| `utils/__tests__/groupToolUses.test.ts` | 10 | GOOD | — | +| `utils/__tests__/shell/__tests__/outputLimits.test.ts` | 7 | ACCEPTABLE | — | +| `utils/__tests__/envValidation.test.ts` | 9 | ACCEPTABLE | **可能存在 bug**:lower bound=100 但 value=1 报 valid | +| `utils/git/__tests__/gitConfigParser.test.ts` | 20 | GOOD | — | +| `services/mcp/__tests__/mcpStringUtils.test.ts` | 16 | GOOD | — | +| `services/mcp/__tests__/normalization.test.ts` | 10 | GOOD | — | -```typescript -import { mkdtemp, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterAll, beforeAll } from "bun:test"; +### 4.3 评分汇总 -let tempDir: string; +| 等级 | 文件数 | 占比 | +|------|--------|------| +| **GOOD** | 30 | 47% | +| **ACCEPTABLE** | 26 | 41% | +| **WEAK** | 8 | 12% | -beforeAll(async () => { - tempDir = await mkdtemp(join(tmpdir(), "claude-test-")); -}); +## 5. 系统性问题 -afterAll(async () => { - await rm(tempDir, { recursive: true }); -}); -``` +### 5.1 断言过弱(Smell: `toContain` 代替精确匹配) -## 7. 优先测试模块 +以下文件的部分测试使用 `toContain` 或 `not.toBeNull` 检查结果,当实现返回包含目标子串的任何字符串时测试仍通过,无法检测格式错误: -按优先级从高到低排列,括号内为目标覆盖率: +| 文件 | 受影响函数 | 建议 | +|------|-----------|------| +| `format.test.ts` | formatNumber, formatTokens, formatRelativeTime | 改为 `toBe` 精确匹配 | +| `file.test.ts` | addLineNumbers | 断言完整输出格式 | +| `diff.test.ts` | getPatchFromContents | 验证 hunk 内容正确性 | +| `notebook.test.ts` | mapNotebookCellsToToolResult | 验证合并后内容 | +| `uuid.test.ts` | validateUuid (uppercase) | 断言标准化后的精确值 | -### P0 — 核心(行覆盖率 >= 80%) +### 5.2 集成测试空白 -| 模块 | 路径 | 测试重点 | -|------|------|----------| -| **Tool 系统** | `src/tools/`, `src/Tool.ts`, `src/tools.ts` | tool 注册/发现、inputSchema 校验、call() 执行与错误处理 | -| **工具函数** | `src/utils/` 下纯函数 | 各种 utility 的正确性与边界情况 | -| **Context 构建** | `src/context.ts`, `src/utils/claudemd.ts` | 系统提示拼装、CLAUDE.md 发现与加载、context 内容完整性 | +Spec 定义的三个集成测试均未创建: -### P1 — 重要(行覆盖率 >= 60%) - -| 模块 | 路径 | 测试重点 | -|------|------|----------| -| **权限系统** | `src/utils/permissions/` | 权限模式判断、tool 许可/拒绝逻辑 | -| **模型路由** | `src/utils/model/` | provider 选择、模型名映射、fallback 逻辑 | -| **消息处理** | `src/types/message.ts`, `src/utils/messages.ts` | 消息类型构造、格式化、过滤 | -| **CLI 参数** | `src/main.tsx` 中的 Commander 配置 | 参数解析、模式切换(REPL/pipe) | - -### P2 — 补充 - -| 模块 | 路径 | 测试重点 | -|------|------|----------| -| **Cron 调度** | `src/utils/cron*.ts` | cron 表达式解析、任务调度逻辑 | -| **Git 工具** | `src/utils/git.ts` | git 命令构造、输出解析 | -| **Config** | `src/utils/config.ts`, `src/utils/settings/` | 配置加载、合并、默认值 | - -## 8. 覆盖率要求 - -| 范围 | 目标 | 说明 | +| 计划 | 状态 | 依赖 | |------|------|------| -| P0 核心模块 | **>= 80%** 行覆盖率 | Tool 系统、工具函数、Context 构建 | -| P1 重要模块 | **>= 60%** 行覆盖率 | 权限、模型路由、消息处理 | -| 整体 | 不设强制指标 | 逐步提升,不追求数字 | +| `tests/integration/tool-chain.test.ts` | 未创建 | 需 mock tools.ts 完整注册链 | +| `tests/integration/context-build.test.ts` | 未创建 | 需 mock context.ts 重依赖链 | +| `tests/integration/message-pipeline.test.ts` | 未创建 | 需 mock API 层 | -运行覆盖率报告: +`tests/mocks/` 目录也不存在,无共享 mock/fixture 基础设施。 -```bash -bun test --coverage -``` +### 5.3 Mock 相关 -## 9. CI 集成 +| 问题 | 影响文件 | 说明 | +|------|----------|------| +| 未 mock 重依赖 | `gitOperationTracking.test.ts` | `detectGitOperation` 内部调用 analytics,测试产生副作用 | +| `isExternalPermissionMode` 永远 true | `PermissionMode.test.ts` | false 路径从未被执行,测试形同虚设 | +| env 恢复不完整 | `providers.test.ts` | 仅删除已知 key,新增 env var 会导致测试泄漏 | -已有 GitHub Actions 配置(`.github/workflows/ci.yml`),`bun test` 步骤已就位。 +### 5.4 潜在 Bug -### CI 中测试的运行条件 - -- **push** 到 `main` 或 `feature/*` 分支时自动运行 -- **pull_request** 到 `main` 分支时自动运行 -- 测试失败将阻止合并 - -### 本地运行 - -```bash -# 运行全部测试 -bun test - -# 运行特定文件 -bun test src/utils/__tests__/array.test.ts - -# 运行匹配模式 -bun test --filter "findToolByName" - -# 带覆盖率 -bun test --coverage - -# watch 模式(开发时) -bun test --watch -``` - -## 10. 编写测试 Checklist - -每次新增或修改测试时,确认以下事项: - -- [ ] 测试文件位置正确(单元 → `__tests__/`,集成 → `tests/integration/`) -- [ ] 命名遵循 `describe` + `test` 英文格式 -- [ ] 每个 test 只验证一个行为 -- [ ] 覆盖了正常路径、边界情况和错误情况 -- [ ] 无硬编码的绝对路径或系统特定值 -- [ ] Mock 使用得当(通用 → `tests/mocks/`,专用 → 就近) -- [ ] 测试可独立运行,无顺序依赖 -- [ ] `bun test` 本地全部通过后再提交 - -## 11. 当前测试覆盖状态 - -> 更新日期:2026-04-02 | 总计:**1177 tests, 64 files, 0 failures** - -### P0 — 核心模块 - -| 测试计划 | 测试文件 | 测试数 | 覆盖范围 | -|----------|----------|--------|----------| -| 01 - Tool 系统 | `src/__tests__/Tool.test.ts` | 25 | buildTool, toolMatchesName, findToolByName, getEmptyToolPermissionContext, filterToolProgressMessages | -| | `src/__tests__/tools.test.ts` | 10 | parseToolPreset, filterToolsByDenyRules | -| | `src/tools/shared/__tests__/gitOperationTracking.test.ts` | 16 | parseGitCommitId, detectGitOperation | -| | `src/tools/FileEditTool/__tests__/utils.test.ts` | 24 | normalizeQuotes, stripTrailingWhitespace, findActualString, preserveQuoteStyle, applyEditToFile | -| 02 - Utils 纯函数 | `src/utils/__tests__/array.test.ts` | 12 | intersperse, count, uniq | -| | `src/utils/__tests__/set.test.ts` | 12 | difference, intersects, every, union | -| | `src/utils/__tests__/xml.test.ts` | 9 | escapeXml, escapeXmlAttr | -| | `src/utils/__tests__/hash.test.ts` | 12 | djb2Hash, hashContent, hashPair | -| | `src/utils/__tests__/stringUtils.test.ts` | 35 | escapeRegExp, capitalize, plural, firstLineOf, countCharInString, normalizeFullWidthDigits/Space, safeJoinLines, EndTruncatingAccumulator, truncateToLines | -| | `src/utils/__tests__/semver.test.ts` | 21 | gt, gte, lt, lte, satisfies, order | -| | `src/utils/__tests__/uuid.test.ts` | 6 | validateUuid | -| | `src/utils/__tests__/format.test.ts` | 24 | formatFileSize, formatSecondsShort, formatDuration, formatNumber, formatTokens, formatRelativeTime | -| | `src/utils/__tests__/frontmatterParser.test.ts` | 28 | parseFrontmatter, splitPathInFrontmatter, parsePositiveIntFromFrontmatter, parseBooleanFrontmatter, parseShellFrontmatter | -| | `src/utils/__tests__/file.test.ts` | 17 | convertLeadingTabsToSpaces, addLineNumbers, stripLineNumberPrefix, normalizePathForComparison, pathsEqual | -| | `src/utils/__tests__/glob.test.ts` | 6 | extractGlobBaseDirectory | -| | `src/utils/__tests__/diff.test.ts` | 8 | adjustHunkLineNumbers, getPatchFromContents | -| | `src/utils/__tests__/json.test.ts` | 27 | safeParseJSON, safeParseJSONC, parseJSONL, addItemToJSONCArray (mock log.ts) | -| | `src/utils/__tests__/truncate.test.ts` | 24 | truncateToWidth, truncateStartToWidth, truncateToWidthNoEllipsis, truncatePathMiddle, truncate, wrapText | -| | `src/utils/__tests__/path.test.ts` | 15 | containsPathTraversal, normalizePathForConfigKey | -| | `src/utils/__tests__/tokens.test.ts` | 22 | getTokenCountFromUsage, getTokenUsage, tokenCountFromLastAPIResponse, messageTokenCountFromLastAPIResponse, getCurrentUsage, doesMostRecentAssistantMessageExceed200k, getAssistantMessageContentLength (mock log.ts, tokenEstimation, slowOperations) | -| 03 - Context 构建 | `src/utils/__tests__/claudemd.test.ts` | 16 | stripHtmlComments, isMemoryFilePath, getLargeMemoryFiles | -| | `src/utils/__tests__/systemPrompt.test.ts` | 9 | buildEffectiveSystemPrompt | - -### P1 — 重要模块 - -| 测试计划 | 测试文件 | 测试数 | 覆盖范围 | -|----------|----------|--------|----------| -| 04 - 权限系统 | `src/utils/permissions/__tests__/permissionRuleParser.test.ts` | 25 | escapeRuleContent, unescapeRuleContent, permissionRuleValueFromString, permissionRuleValueToString, normalizeLegacyToolName | -| | `src/utils/permissions/__tests__/permissions.test.ts` | 13 | getDenyRuleForTool, getAskRuleForTool, getDenyRuleForAgent, filterDeniedAgents (mock log.ts, slowOperations) | -| 05 - 模型路由 | `src/utils/model/__tests__/aliases.test.ts` | 16 | isModelAlias, isModelFamilyAlias | -| | `src/utils/model/__tests__/model.test.ts` | 14 | firstPartyNameToCanonical | -| | `src/utils/model/__tests__/providers.test.ts` | 10 | getAPIProvider, isFirstPartyAnthropicBaseUrl | -| 06 - 消息处理 | `src/utils/__tests__/messages.test.ts` | 56 | createAssistantMessage, createUserMessage, isSyntheticMessage, getLastAssistantMessage, hasToolCallsInLastAssistantTurn, extractTag, isNotEmptyMessage, normalizeMessages, deriveUUID, isClassifierDenial 等 | - -### P2 — 补充模块 - -| 测试计划 | 测试文件 | 测试数 | 覆盖范围 | -|----------|----------|--------|----------| -| 07 - Cron 调度 | `src/utils/__tests__/cron.test.ts` | 38 | parseCronExpression, computeNextCronRun, cronToHuman | -| 08 - Git 工具 | `src/utils/__tests__/git.test.ts` | 18 | normalizeGitRemoteUrl (SSH/HTTPS/ssh:///代理URL/大小写规范化) | -| 09 - 配置与设置 | `src/utils/settings/__tests__/config.test.ts` | 62 | SettingsSchema, PermissionsSchema, AllowedMcpServerEntrySchema, MCP 类型守卫, 设置常量函数, filterInvalidPermissionRules, validateSettingsFileContent, formatZodError | - -### P3 — Phase 1 纯函数扩展 - -| 测试文件 | 测试数 | 覆盖范围 | -|----------|--------|----------| -| `src/utils/__tests__/errors.test.ts` | 28 | ClaudeError, AbortError, ConfigParseError, ShellError, TelemetrySafeError, isAbortError, hasExactErrorMessage, toError, errorMessage, getErrnoCode, isENOENT, getErrnoPath, shortErrorStack, isFsInaccessible, classifyAxiosError | -| `src/utils/permissions/__tests__/shellRuleMatching.test.ts` | 22 | permissionRuleExtractPrefix, hasWildcards, matchWildcardPattern, parsePermissionRule, suggestionForExactCommand, suggestionForPrefix | -| `src/utils/__tests__/argumentSubstitution.test.ts` | 18 | parseArguments, parseArgumentNames, generateProgressiveArgumentHint, substituteArguments | -| `src/utils/__tests__/CircularBuffer.test.ts` | 12 | CircularBuffer class: add, addAll, getRecent, toArray, clear, length | -| `src/utils/__tests__/sanitization.test.ts` | 14 | partiallySanitizeUnicode, recursivelySanitizeUnicode | -| `src/utils/__tests__/slashCommandParsing.test.ts` | 8 | parseSlashCommand | -| `src/utils/__tests__/contentArray.test.ts` | 6 | insertBlockAfterToolResults | -| `src/utils/__tests__/objectGroupBy.test.ts` | 5 | objectGroupBy | - -### P4 — Phase 2 轻 Mock 扩展 - -| 测试文件 | 测试数 | 覆盖范围 | -|----------|--------|----------| -| `src/utils/__tests__/envUtils.test.ts` | 34 | isEnvTruthy, isEnvDefinedFalsy, parseEnvVars, hasNodeOption, getAWSRegion, getDefaultVertexRegion, getVertexRegionForModel, isBareMode, shouldMaintainProjectWorkingDir, getClaudeConfigHomeDir | -| `src/utils/__tests__/sleep.test.ts` | 14 | sleep (abort, throwOnAbort, abortError), withTimeout, sequential | -| `src/utils/__tests__/memoize.test.ts` | 16 | memoizeWithTTL, memoizeWithTTLAsync (dedup/cache/clear), memoizeWithLRU (eviction/cache methods) | -| `src/utils/__tests__/groupToolUses.test.ts` | 10 | applyGrouping (verbose, grouping, result collection, mixed messages) | -| `src/utils/permissions/__tests__/dangerousPatterns.test.ts` | 7 | CROSS_PLATFORM_CODE_EXEC, DANGEROUS_BASH_PATTERNS 常量验证 | -| `src/utils/shell/__tests__/outputLimits.test.ts` | 7 | getMaxOutputLength, BASH_MAX_OUTPUT_UPPER_LIMIT, BASH_MAX_OUTPUT_DEFAULT | - -### P5 — Phase 3 补全 + Phase 4 工具模块 - -| 测试文件 | 测试数 | 覆盖范围 | -|----------|--------|----------| -| `src/utils/__tests__/zodToJsonSchema.test.ts` | 9 | zodToJsonSchema (string/number/object/enum/optional/array/boolean + caching) | -| `src/utils/permissions/__tests__/PermissionMode.test.ts` | 19 | PERMISSION_MODES, permissionModeFromString, permissionModeTitle, permissionModeShortTitle, permissionModeSymbol, getModeColor, isDefaultMode, toExternalPermissionMode, isExternalPermissionMode | -| `src/utils/__tests__/envValidation.test.ts` | 9 | validateBoundedIntEnvVar (default/valid/capped/invalid/boundary) | -| `src/services/mcp/__tests__/mcpStringUtils.test.ts` | 18 | mcpInfoFromString, getMcpPrefix, buildMcpToolName, getMcpDisplayName, getToolNameForPermissionCheck, extractMcpToolDisplayName | -| `src/tools/BashTool/__tests__/destructiveCommandWarning.test.ts` | 22 | getDestructiveCommandWarning (git/rm/database/infrastructure patterns) | -| `src/tools/BashTool/__tests__/commandSemantics.test.ts` | 11 | interpretCommandResult (grep/diff/test/rg/find exit code semantics) | - -### P6 — Phase 5 扩展覆盖 - -| 测试文件 | 测试数 | 覆盖范围 | -|----------|--------|----------| -| `src/utils/__tests__/tokenBudget.test.ts` | 20 | parseTokenBudget, findTokenBudgetPositions, getBudgetContinuationMessage | -| `src/utils/__tests__/displayTags.test.ts` | 17 | stripDisplayTags, stripDisplayTagsAllowEmpty, stripIdeContextTags | -| `src/utils/__tests__/taggedId.test.ts` | 10 | toTaggedId (prefix/uniqueness/format) | -| `src/utils/__tests__/controlMessageCompat.test.ts` | 15 | normalizeControlMessageKeys (snake_case→camelCase 转换) | -| `src/services/mcp/__tests__/normalization.test.ts` | 11 | normalizeNameForMCP (特殊字符/截断/空字符串/Unicode) | -| `src/services/mcp/__tests__/envExpansion.test.ts` | 14 | expandEnvVarsInString ($VAR/${VAR}/嵌套/未定义/转义) | -| `src/utils/git/__tests__/gitConfigParser.test.ts` | 20 | parseConfigString (key=value/section/subsection/多行/注释/引号) | -| `src/utils/__tests__/formatBriefTimestamp.test.ts` | 10 | formatBriefTimestamp (秒/分/时/天/周/月/年) | -| `src/utils/__tests__/hyperlink.test.ts` | 10 | createHyperlink (OSC 8 序列/file:///path/fallback) | -| `src/utils/__tests__/windowsPaths.test.ts` | 20 | windowsPathToPosixPath, posixPathToWindowsPath (驱动器/UNC/相对路径) | -| `src/utils/__tests__/notebook.test.ts` | 14 | parseCellId, mapNotebookCellsToToolResult (code/markdown/output) | -| `src/utils/__tests__/effort.test.ts` | 38 | isEffortLevel, parseEffortValue, isValidNumericEffort, convertEffortValueToLevel, getEffortLevelDescription, resolvePickerEffortPersistence | - -### 已知限制 - -以下模块因 Bun 运行时限制或极重依赖链,暂时无法或不适合测试: - -| 模块 | 问题 | 说明 | +| 文件 | 函数 | 问题 | |------|------|------| -| `Bun.JSONL.parseChunk` | 处理畸形行时无限挂起 | Bun 1.3.10 bug,错误恢复循环卡死;已跳过 parseJSONL 畸形行测试 | -| `src/tools.ts` 部分函数 | `getAllBaseTools`/`getTools` 加载全量 tool | 导入链过重,mock 难度大 | -| `src/tools/shared/spawnMultiAgent.ts` | 依赖 bootstrap/state + AppState + 50+ 模块 | mock 成本极高,投入产出比低 | -| `src/utils/messages.ts` 部分函数 | `withMemoryCorrectionHint` 等 | 依赖 `getFeatureValue_CACHED_MAY_BE_STALE` | +| `envValidation.test.ts` | validateBoundedIntEnvVar | 测试断言 lower bound=100 时 value=1 返回 `status: "valid"`,与函数名语义矛盾,可能是源码 bug 或测试逻辑错误 | -### Mock 策略总结 +### 5.5 已知限制 -通过 `mock.module()` + `await import()` 模式成功解锁了以下重依赖模块的测试: +| 模块 | 问题 | +|------|------| +| `Bun.JSONL.parseChunk` | 畸形行时无限挂起(Bun 1.3.10 bug) | +| `context.ts` 核心逻辑 | 依赖 bootstrap/state + git + 50+ 模块,mock 不可行 | +| `tools.ts` (getAllBaseTools) | 导入链过重 | +| `spawnMultiAgent.ts` | 50+ 依赖 | +| `messages.ts` 部分函数 | 依赖 `getFeatureValue_CACHED_MAY_BE_STALE` | +| UI 组件 (`screens/`, `components/`) | 需 Ink 渲染测试环境 | + +### 5.6 Mock 模式 + +通过 `mock.module()` + `await import()` 解锁重依赖模块: | 被 Mock 模块 | 解锁的测试 | |-------------|-----------| -| `src/utils/log.ts` | json.ts, tokens.ts, FileEditTool/utils.ts, permissions.ts, memoize.ts, PermissionMode.ts | -| `src/services/tokenEstimation.ts` | tokens.ts | -| `src/utils/slowOperations.ts` | tokens.ts, permissions.ts, memoize.ts, PermissionMode.ts | -| `src/utils/debug.ts` | envValidation.ts, outputLimits.ts | -| `src/utils/bash/commands.ts` | commandSemantics.ts | -| `src/utils/thinking.js` | effort.ts | -| `src/utils/settings/settings.js` | effort.ts | -| `src/utils/auth.js` | effort.ts | -| `src/services/analytics/growthbook.js` | effort.ts, tokenBudget.ts | -| `src/utils/model/modelSupportOverrides.js` | effort.ts | +| `src/utils/log.ts` | json, tokens, FileEditTool/utils, permissions, memoize, PermissionMode | +| `src/services/tokenEstimation.ts` | tokens | +| `src/utils/slowOperations.ts` | tokens, permissions, memoize, PermissionMode | +| `src/utils/debug.ts` | envValidation, outputLimits | +| `src/utils/bash/commands.ts` | commandSemantics | +| `src/utils/thinking.js` | effort | +| `src/utils/settings/settings.js` | effort | +| `src/utils/auth.js` | effort | +| `src/services/analytics/growthbook.js` | effort, tokenBudget | -**关键约束**:`mock.module()` 必须在每个测试文件中内联调用,不能从共享 helper 导入(Bun 在 mock 生效前就解析了 helper 的导入)。 +**约束**:`mock.module()` 必须在每个测试文件内联调用,不能从共享 helper 导入。 -## 12. 后续测试覆盖计划 +## 6. 改进计划 -> **已完成** — Phase 1-4 增加 321 tests (647 → 968),Phase 5 增加 209 tests (968 → 1177) -> -> Phase 1-4 全部完成,详见上方 P3-P5 表格。 -> Phase 5 新增 12 个测试文件覆盖:effort、tokenBudget、displayTags、taggedId、controlMessageCompat、MCP normalization/envExpansion、gitConfigParser、formatBriefTimestamp、hyperlink、windowsPaths、notebook,详见 P6 表格。 -> 实际调整:Phase 3 中 `context.ts` 因极重依赖链(bootstrap/state + claudemd + git 等)且 `getGitStatus` 在 test 环境直接返回 null,替换为 `envValidation.ts`(更实用);Phase 4 中 GlobTool 纯函数不足,替换为 `commandSemantics.ts` + `destructiveCommandWarning.ts`。 +### 优先级排序 -### 不纳入计划的模块 +| 优先级 | 任务 | 预期效果 | +|--------|------|----------| +| **高** | 修复 8 个 WEAK 文件的断言缺陷 | 消除假阳性风险 | +| **高** | 补 `gitOperationTracking.test.ts` 的 analytics mock | 消除测试副作用 | +| **高** | 验证 `envValidation.test.ts` 潜在 bug | 排除源码缺陷 | +| **中** | 搭建 `tests/mocks/` 基础设施 | 为集成测试铺路 | +| **中** | 编写 `tests/integration/tool-chain.test.ts` | 覆盖 Tool 注册→发现→执行链路 | +| **中** | 补 `truncate.test.ts` CJK/emoji 测试 | 覆盖核心场景 | +| **低** | 补 `claudemd.test.ts` 核心逻辑 | 提升 P0 模块覆盖率 | +| **低** | 补 CLI 参数测试 (`main.tsx`) | 完成 P1 覆盖 | +| **低** | 运行 `bun test --coverage` 建立基线 | 量化覆盖率 | + +### 不纳入计划 | 模块 | 原因 | |------|------| -| `query.ts` / `QueryEngine.ts` | 核心循环,需集成测试环境 | +| `query.ts` / `QueryEngine.ts` | 核心循环,需完整集成环境 | | `services/api/claude.ts` | 需 mock SDK 流式响应 | -| `spawnMultiAgent.ts` | 50+ 依赖,mock 不可行 | +| `spawnMultiAgent.ts` | 50+ 依赖 | | `modelCost.ts` | 依赖 bootstrap/state + analytics | | `mcp/dateTimeParser.ts` | 调用 Haiku API | -| `screens/` / `components/` | UI 组件,需 Ink 渲染测试 | - -## 13. 参考 - -- [Bun Test 文档](https://bun.sh/docs/cli/test) -- 现有测试示例:`src/utils/__tests__/set.test.ts`, `src/utils/__tests__/array.test.ts` +| `screens/` / `components/` | 需 Ink 渲染测试 |