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 渲染测试 |