docs: 指定测试计划

This commit is contained in:
claude-code-best 2026-04-02 14:14:35 +08:00
parent 5fda87246d
commit 9c3803d16b
7 changed files with 1204 additions and 391 deletions

View File

@ -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 环境永远返回 truefalse 路径从未执行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('<cell id="cell-0">');
expect(result).toContain("</cell>");
```
#### 新增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 并更新测试)

View File

@ -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` | `"<!-- no close some text"` → 原样返回 |
| 跨行注释 | `stripHtmlComments` | `"<!--\nmulti\nline\n-->text"``"text"` |
| 同行注释+内容 | `stripHtmlComments` | `"<!-- note -->some text"``"some text"` |
| 内联代码中的注释 | `stripHtmlComments` | `` `<!-- kept -->` `` → 保留 |
| 大小写不敏感 | `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` 用于精确值检查的场景

View File

@ -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<string, string | undefined>;
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 | 增大 marginTTL=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` 全部通过

View File

@ -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` 全部通过

View File

@ -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<string> {
const dir = await mkdtemp(join(tmpdir(), prefix));
return dir;
}
export async function cleanupTempDir(dir: string): Promise<void> {
await rm(dir, { recursive: true, force: true });
}
export async function writeTempFile(dir: string, name: string, content: string): Promise<string> {
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` 全部通过

View File

@ -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` 全部通过

View File

@ -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 Actionspush/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/<Tool>/__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/ # 测试辅助函数
```
### 命名规则
- 测试文件:`<module>.test.ts`
- 命名风格:`describe("functionName")` + `test("行为描述")`,英文
- 编写原则Arrange-Act-Assert、单一职责、独立性、边界覆盖
| 项 | 规则 |
|----|------|
| 测试文件 | `<module-name>.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 渲染测试 |