claude-code/docs/test-plans/phase-16-zero-dep-pure-functions.md
2026-04-02 15:06:51 +08:00

189 lines
7.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Phase 16 — 零依赖纯函数测试
> 创建日期2026-04-02
> 预计:+120 tests / 8 files
> 目标:覆盖所有零外部依赖的纯函数/类模块
所有模块均为纯函数或零外部依赖类mock 成本为零ROI 最高。
---
## 16.1 `src/utils/__tests__/stream.test.ts`~15 tests
**目标模块**: `src/utils/stream.ts`76 行)
**导出**: `Stream<T>` class — 手动异步队列,实现 `AsyncIterator<T>`
| 测试用例 | 验证点 |
|---------|--------|
| enqueue then read | 单条消息正确传递 |
| enqueue multiple then drain | 多条消息顺序消费 |
| done resolves pending readers | `done()` 后迭代结束 |
| done with no pending readers | 无等待时安全关闭 |
| error rejects pending readers | `error(e)` 传播异常 |
| error after done | 后续操作安全处理 |
| single-iteration guard | `return()` 后不可再迭代 |
| empty stream done immediately | 无数据时 done 返回 `{ done: true }` |
| concurrent enqueue | 多次 enqueue 不丢失 |
| backpressure | reader 慢于 writer 时不丢数据 |
---
## 16.2 `src/utils/__tests__/abortController.test.ts`~12 tests
**目标模块**: `src/utils/abortController.ts`99 行)
**导出**: `createAbortController()`, `createChildAbortController()`
| 测试用例 | 验证点 |
|---------|--------|
| parent abort propagates to child | `parent.abort()` → child aborted |
| child abort does NOT propagate to parent | `child.abort()` → parent still active |
| already-aborted parent → child immediately aborted | 创建时即继承 abort 状态 |
| child listener cleanup after parent abort | WeakRef 回收后无泄漏 |
| multiple children of same parent | 独立 abort 传播 |
| child abort then parent abort | 顺序无关 |
| signal.maxListeners raised | MaxListenersExceededWarning 不触发 |
---
## 16.3 `src/utils/__tests__/bufferedWriter.test.ts`~14 tests
**目标模块**: `src/utils/bufferedWriter.ts`100 行)
**导出**: `createBufferedWriter()`
| 测试用例 | 验证点 |
|---------|--------|
| single write buffered | write → buffer 累积 |
| flush on size threshold | 超过 maxSize 时自动 flush |
| flush on timer | 定时器触发 flush |
| immediate mode | `{ immediate: true }` 跳过缓冲 |
| overflow coalescing | overflow 内容合并到下次 flush |
| empty buffer flush | 无数据时 flush 无副作用 |
| close flushes remaining | close 触发最终 flush |
| multiple writes before flush | 批量写入合并 |
| flush callback receives concatenated data | writeFn 参数正确 |
**Mock**: 注入 `writeFn` 回调,可选 fake timers
---
## 16.4 `src/utils/__tests__/gitDiff.test.ts`~20 tests
**目标模块**: `src/utils/gitDiff.ts`532 行)
**可测函数**: `parseGitNumstat()`, `parseGitDiff()`, `parseShortstat()`
| 测试用例 | 验证点 |
|---------|--------|
| parseGitNumstat — single file | `1\t2\tpath` → { added: 1, deleted: 2, file: "path" } |
| parseGitNumstat — binary file | `-\t-\timage.png` → binary flag |
| parseGitNumstat — rename | `{ old => new }` 格式解析 |
| parseGitNumstat — empty diff | 空字符串 → [] |
| parseGitNumstat — multiple files | 多行正确分割 |
| parseGitDiff — added lines | `+` 开头行计数 |
| parseGitDiff — deleted lines | `-` 开头行计数 |
| parseGitDiff — hunk header | `@@ -a,b +c,d @@` 解析 |
| parseGitDiff — new file mode | `new file mode 100644` 检测 |
| parseGitDiff — deleted file mode | `deleted file mode` 检测 |
| parseGitDiff — binary diff | Binary files differ 处理 |
| parseShortstat — all components | `1 file changed, 5 insertions(+), 3 deletions(-)` |
| parseShortstat — insertions only | 无 deletions |
| parseShortstat — deletions only | 无 insertions |
| parseShortstat — files only | 仅 file changed |
| parseShortstat — empty | 空字符串 → 默认值 |
| parseShortstat — rename | `1 file changed, ...` 重命名 |
**Mock**: 无需 mock — 全部是纯字符串解析
---
## 16.5 `src/__tests__/history.test.ts`~18 tests
**目标模块**: `src/history.ts`464 行)
**可测函数**: `parseReferences()`, `expandPastedTextRefs()`, `formatPastedTextRef()`, `formatImageRef()`, `getPastedTextRefNumLines()`
| 测试用例 | 验证点 |
|---------|--------|
| parseReferences — text ref | `#1` → [{ type: "text", ref: 1 }] |
| parseReferences — image ref | `@1` → [{ type: "image", ref: 1 }] |
| parseReferences — multiple refs | `#1 #2 @3` → 3 refs |
| parseReferences — no refs | `"hello"` → [] |
| parseReferences — duplicate refs | `#1 #1` → 去重或保留 |
| parseReferences — zero ref | `#0` → 边界 |
| parseReferences — large ref | `#999` → 正常 |
| formatPastedTextRef — basic | 输出格式验证 |
| formatPastedTextRef — multiline | 多行内容格式 |
| getPastedTextRefNumLines — 1 line | 返回 1 |
| getPastedTextRefNumLines — multiple lines | 换行计数 |
| expandPastedTextRefs — single ref | 替换单个引用 |
| expandPastedTextRefs — multiple refs | 替换多个引用 |
| expandPastedTextRefs — no refs | 原样返回 |
| expandPastedTextRefs — mixed content | 文本 + 引用混合 |
| formatImageRef — basic | 输出格式 |
**Mock**: `mock.module("src/bootstrap/state.ts", ...)` 解锁模块
---
## 16.6 `src/utils/__tests__/sliceAnsi.test.ts`~16 tests
**目标模块**: `src/utils/sliceAnsi.ts`91 行)
**导出**: `sliceAnsi()` — ANSI 感知的字符串切片
| 测试用例 | 验证点 |
|---------|--------|
| plain text slice | `"hello".slice(1,3)` 等价 |
| preserve ANSI codes | `\x1b[31mhello\x1b[0m` 切片后保留颜色 |
| close opened styles | 切片点在 ANSI 样式中间时正确关闭 |
| hyperlink handling | OSC 8 超链接不被切断 |
| combining marks (diacritics) | `é` = `e\u0301` 不被切开 |
| Devanagari matras | 零宽字符不被切断 |
| full-width characters | CJK 字符宽度 = 2 |
| empty slice | 返回空字符串 |
| full slice | 返回完整字符串 |
| boundary at ANSI code | 边界恰好在 escape 序列上 |
| nested ANSI styles | 多层嵌套时正确处理 |
| slice start > end | 空结果 |
**Mock**: `mock.module("@alcalzone/ansi-tokenize", ...)`, `mock.module("ink/stringWidth", ...)`
---
## 16.7 `src/utils/__tests__/treeify.test.ts`~15 tests
**目标模块**: `src/utils/treeify.ts`170 行)
**导出**: `treeify()` — 递归树渲染
| 测试用例 | 验证点 |
|---------|--------|
| simple flat tree | `{ a: {}, b: {} }` → 2 行 |
| nested tree | `{ a: { b: { c: {} } } }` → 3 行缩进 |
| array values | `[1, 2, 3]` 渲染为列表 |
| circular reference | 不无限递归 |
| empty object | `{}` 处理 |
| single key | 布局适配 |
| branch vs last-branch character | ├─ vs └─ |
| custom prefix | options 前缀传递 |
| deep nesting | 5+ 层缩进正确 |
| mixed object/array | 混合结构 |
**Mock**: `mock.module("figures", ...)`, color 模块 mock
---
## 16.8 `src/utils/__tests__/words.test.ts`~10 tests
**目标模块**: `src/utils/words.ts`800 行,大部分是词表数据)
**导出**: `generateWordSlug()`, `generateShortWordSlug()`
| 测试用例 | 验证点 |
|---------|--------|
| generateWordSlug format | `adjective-verb-noun` 三段式 |
| generateShortWordSlug format | `adjective-noun` 两段式 |
| all parts non-empty | 无空段 |
| hyphen separator | `-` 分隔 |
| all parts from word lists | 成分来自预定义词表 |
| multiple calls uniqueness | 连续调用不总是相同 |
| no consecutive hyphens | 无 `--` |
| lowercase only | 全小写 |
**Mock**: `mock.module("crypto", ...)` 控制 `randomBytes` 实现确定性测试