docs: 清理垃圾文档

This commit is contained in:
claude-code-best
2026-04-12 11:38:58 +08:00
parent e0e4ee41c2
commit 14c46df881
31 changed files with 0 additions and 8343 deletions

View File

@@ -1,147 +0,0 @@
# Tool 系统测试计划
## 概述
Tool 系统是 Claude Code 的核心,负责工具的定义、注册、发现和过滤。本计划覆盖 `src/Tool.ts` 中的工具接口与工具函数、`src/tools.ts` 中的注册/过滤逻辑,以及各工具目录下可独立测试的纯函数。
## 被测文件
| 文件 | 关键导出 |
|------|----------|
| `src/Tool.ts` | `buildTool`, `toolMatchesName`, `findToolByName`, `getEmptyToolPermissionContext`, `filterToolProgressMessages` |
| `src/tools.ts` | `parseToolPreset`, `filterToolsByDenyRules`, `getAllBaseTools`, `getTools`, `assembleToolPool` |
| `src/tools/shared/gitOperationTracking.ts` | `parseGitCommitId`, `detectGitOperation` |
| `src/tools/shared/spawnMultiAgent.ts` | `resolveTeammateModel`, `generateUniqueTeammateName` |
| `src/tools/GrepTool/GrepTool.ts` | `applyHeadLimit`, `formatLimitInfo`(内部辅助函数) |
| `src/tools/FileEditTool/utils.ts` | 字符串匹配/补丁相关纯函数 |
---
## 测试用例
### src/Tool.ts
#### describe('buildTool')
- test('fills in default isEnabled as true') — 不传 isEnabled 时,构建的 tool.isEnabled() 应返回 true
- test('fills in default isConcurrencySafe as false') — 默认值应为 falsefail-closed
- test('fills in default isReadOnly as false') — 默认假设有写操作
- test('fills in default isDestructive as false') — 默认非破坏性
- test('fills in default checkPermissions as allow') — 默认 checkPermissions 应返回 `{ behavior: 'allow', updatedInput }`
- test('fills in default userFacingName from tool name') — userFacingName 默认应返回 tool.name
- test('preserves explicitly provided methods') — 传入自定义 isEnabled 等方法时应覆盖默认值
- test('preserves all non-defaultable properties') — name, inputSchema, call, description 等属性原样保留
#### describe('toolMatchesName')
- test('returns true for exact name match') — `{ name: 'Bash' }` 匹配 'Bash'
- test('returns false for non-matching name') — `{ name: 'Bash' }` 不匹配 'Read'
- test('returns true when name matches an alias') — `{ name: 'Bash', aliases: ['BashTool'] }` 匹配 'BashTool'
- test('returns false when aliases is undefined') — `{ name: 'Bash' }` 不匹配 'BashTool'
- test('returns false when aliases is empty') — `{ name: 'Bash', aliases: [] }` 不匹配 'BashTool'
#### describe('findToolByName')
- test('finds tool by primary name') — 从 tools 列表中按 name 找到工具
- test('finds tool by alias') — 从 tools 列表中按 alias 找到工具
- test('returns undefined when no match') — 找不到时返回 undefined
- test('returns first match when duplicates exist') — 多个同名工具时返回第一个
#### describe('getEmptyToolPermissionContext')
- test('returns default permission mode') — mode 应为 'default'
- test('returns empty maps and arrays') — additionalWorkingDirectories 为空 Maprules 为空对象
- test('returns isBypassPermissionsModeAvailable as false')
#### describe('filterToolProgressMessages')
- test('filters out hook_progress messages') — 移除 type 为 hook_progress 的消息
- test('keeps tool progress messages') — 保留非 hook_progress 的消息
- test('returns empty array for empty input')
- test('handles messages without type field') — data 不含 type 时应保留
---
### src/tools.ts
#### describe('parseToolPreset')
- test('returns "default" for "default" input') — 精确匹配
- test('returns "default" for "Default" input') — 大小写不敏感
- test('returns null for unknown preset') — 未知字符串返回 null
- test('returns null for empty string')
#### describe('filterToolsByDenyRules')
- test('returns all tools when no deny rules') — 空 deny 规则不过滤任何工具
- test('filters out tools matching blanket deny rule') — deny rule `{ toolName: 'Bash' }` 应移除 Bash
- test('does not filter tools with content-specific deny rules') — deny rule `{ toolName: 'Bash', ruleContent: 'rm -rf' }` 不移除 Bash只在运行时阻止特定命令
- test('filters MCP tools by server name prefix') — deny rule `mcp__server` 应移除该 server 下所有工具
- test('preserves tools not matching any deny rule')
#### describe('getAllBaseTools')
- test('returns a non-empty array of tools') — 至少包含核心工具
- test('each tool has required properties') — 每个工具应有 name, inputSchema, call 等属性
- test('includes BashTool, FileReadTool, FileEditTool') — 核心工具始终存在
- test('includes TestingPermissionTool when NODE_ENV is test') — 需设置 env
#### describe('getTools')
- test('returns filtered tools based on permission context') — 根据 deny rules 过滤
- test('returns simple tools in CLAUDE_CODE_SIMPLE mode') — 仅返回 Bash/Read/Edit
- test('filters disabled tools via isEnabled') — isEnabled 返回 false 的工具被排除
---
### src/tools/shared/gitOperationTracking.ts
#### describe('parseGitCommitId')
- test('extracts commit hash from git commit output') — 从 `[main abc1234] message` 中提取 `abc1234`
- test('returns null for non-commit output') — 无法解析时返回 null
- test('handles various branch name formats') — `[feature/foo abc1234]`
#### describe('detectGitOperation')
- test('detects git commit operation') — 命令含 `git commit` 时识别为 commit
- test('detects git push operation') — 命令含 `git push` 时识别
- test('returns null for non-git commands') — 非 git 命令返回 null
- test('detects git merge operation')
- test('detects git rebase operation')
---
### src/tools/shared/spawnMultiAgent.ts
#### describe('resolveTeammateModel')
- test('returns specified model when provided')
- test('falls back to default model when not specified')
#### describe('generateUniqueTeammateName')
- test('generates a name when no existing names') — 无冲突时返回基础名
- test('appends suffix when name conflicts') — 与已有名称冲突时添加后缀
- test('handles multiple conflicts') — 多次冲突时递增后缀
---
## Mock 需求
| 依赖 | Mock 方式 | 说明 |
|------|-----------|------|
| `bun:bundle` (feature) | 已 polyfill 为 `() => false` | 不需额外 mock |
| `process.env` | `bun:test` mock | 测试 `USER_TYPE``NODE_ENV``CLAUDE_CODE_SIMPLE` |
| `getDenyRuleForTool` | mock module | `filterToolsByDenyRules` 测试中需控制返回值 |
| `isToolSearchEnabledOptimistic` | mock module | `getAllBaseTools` 中条件加载 |
## 集成测试场景
放在 `tests/integration/tool-chain.test.ts`
### describe('Tool registration and discovery')
- test('getAllBaseTools returns tools that can be found by findToolByName') — 注册 → 查找完整链路
- test('filterToolsByDenyRules + getTools produces consistent results') — 过滤管线一致性
- test('assembleToolPool deduplicates built-in and MCP tools') — 合并去重逻辑

View File

@@ -1,416 +0,0 @@
# 工具函数(纯函数)测试计划
## 概述
覆盖 `src/utils/` 下所有可独立单元测试的纯函数。这些函数无外部依赖,输入输出确定性强,是测试金字塔的底层基石。
## 被测文件
| 文件 | 状态 | 关键导出 |
|------|------|----------|
| `src/utils/array.ts` | **已有测试** | intersperse, count, uniq |
| `src/utils/set.ts` | **已有测试** | difference, intersects, every, union |
| `src/utils/xml.ts` | 待测 | escapeXml, escapeXmlAttr |
| `src/utils/hash.ts` | 待测 | djb2Hash, hashContent, hashPair |
| `src/utils/stringUtils.ts` | 待测 | escapeRegExp, capitalize, plural, firstLineOf, countCharInString, normalizeFullWidthDigits, normalizeFullWidthSpace, safeJoinLines, truncateToLines, EndTruncatingAccumulator |
| `src/utils/semver.ts` | 待测 | gt, gte, lt, lte, satisfies, order |
| `src/utils/uuid.ts` | 待测 | validateUuid, createAgentId |
| `src/utils/format.ts` | 待测 | formatFileSize, formatSecondsShort, formatDuration, formatNumber, formatTokens, formatRelativeTime, formatRelativeTimeAgo |
| `src/utils/json.ts` | 待测 | safeParseJSON, safeParseJSONC, parseJSONL, addItemToJSONCArray |
| `src/utils/truncate.ts` | 待测 | truncatePathMiddle, truncateToWidth, truncateStartToWidth, truncateToWidthNoEllipsis, truncate, wrapText |
| `src/utils/diff.ts` | 待测 | adjustHunkLineNumbers, getPatchFromContents |
| `src/utils/frontmatterParser.ts` | 待测 | parseFrontmatter, splitPathInFrontmatter, parsePositiveIntFromFrontmatter, parseBooleanFrontmatter, parseShellFrontmatter |
| `src/utils/file.ts` | 待测(纯函数部分) | convertLeadingTabsToSpaces, addLineNumbers, stripLineNumberPrefix, pathsEqual, normalizePathForComparison |
| `src/utils/glob.ts` | 待测(纯函数部分) | extractGlobBaseDirectory |
| `src/utils/tokens.ts` | 待测 | getTokenCountFromUsage |
| `src/utils/path.ts` | 待测(纯函数部分) | containsPathTraversal, normalizePathForConfigKey |
---
## 测试用例
### src/utils/xml.ts — 测试文件: `src/utils/__tests__/xml.test.ts`
#### describe('escapeXml')
- test('escapes ampersand') — `&``&`
- test('escapes less-than') — `<``&lt;`
- test('escapes greater-than') — `>``&gt;`
- test('does not escape quotes') — `"``'` 保持原样
- test('handles empty string') — `""``""`
- test('handles string with no special chars') — `"hello"` 原样返回
- test('escapes multiple special chars in one string') — `<a & b>``&lt;a &amp; b&gt;`
#### describe('escapeXmlAttr')
- test('escapes all xml chars plus quotes') — `"``&quot;`, `'``&apos;`
- test('escapes double quotes') — `he said "hi"` 正确转义
- test('escapes single quotes') — `it's` 正确转义
---
### src/utils/hash.ts — 测试文件: `src/utils/__tests__/hash.test.ts`
#### describe('djb2Hash')
- test('returns consistent hash for same input') — 相同输入返回相同结果
- test('returns different hashes for different inputs') — 不同输入大概率不同
- test('returns a 32-bit integer') — 结果在 int32 范围内
- test('handles empty string') — 空字符串有确定的哈希值
- test('handles unicode strings') — 中文/emoji 等正确处理
#### describe('hashContent')
- test('returns consistent hash for same content') — 确定性
- test('returns string result') — 返回值为字符串
#### describe('hashPair')
- test('returns consistent hash for same pair') — 确定性
- test('order matters') — hashPair(a, b) ≠ hashPair(b, a)
- test('handles empty strings')
---
### src/utils/stringUtils.ts — 测试文件: `src/utils/__tests__/stringUtils.test.ts`
#### describe('escapeRegExp')
- test('escapes dots') — `.``\\.`
- test('escapes asterisks') — `*``\\*`
- test('escapes brackets') — `[``\\[`
- test('escapes all special chars') — `.*+?^${}()|[]\` 全部转义
- test('leaves normal chars unchanged') — `hello` 原样
- test('escaped string works in RegExp') — `new RegExp(escapeRegExp('a.b'))` 精确匹配 `a.b`
#### describe('capitalize')
- test('uppercases first char') — `"foo"``"Foo"`
- test('does NOT lowercase rest') — `"fooBar"``"FooBar"`(区别于 lodash capitalize
- test('handles single char') — `"a"``"A"`
- test('handles empty string') — `""``""`
- test('handles already capitalized') — `"Foo"``"Foo"`
#### describe('plural')
- test('returns singular for n=1') — `plural(1, 'file')``'file'`
- test('returns plural for n=0') — `plural(0, 'file')``'files'`
- test('returns plural for n>1') — `plural(3, 'file')``'files'`
- test('uses custom plural form') — `plural(2, 'entry', 'entries')``'entries'`
#### describe('firstLineOf')
- test('returns first line of multi-line string') — `"a\nb\nc"``"a"`
- test('returns full string when no newline') — `"hello"``"hello"`
- test('handles empty string') — `""``""`
- test('handles string starting with newline') — `"\nhello"``""`
#### describe('countCharInString')
- test('counts occurrences') — `countCharInString("aabac", "a")``3`
- test('returns 0 when char not found') — `countCharInString("hello", "x")``0`
- test('handles empty string') — `countCharInString("", "a")``0`
- test('respects start position') — `countCharInString("aaba", "a", 2)``1`
#### describe('normalizeFullWidthDigits')
- test('converts full-width digits to half-width') — `""``"0123"`
- test('leaves half-width digits unchanged') — `"0123"``"0123"`
- test('mixed content') — `"port "``"port 8080"`
#### describe('normalizeFullWidthSpace')
- test('converts ideographic space to regular space') — `"\u3000"``" "`
- test('converts multiple spaces') — `"a\u3000b\u3000c"``"a b c"`
#### describe('safeJoinLines')
- test('joins lines with default delimiter') — `["a","b"]``"a,b"`
- test('truncates when exceeding maxSize') — 超限时截断并添加 `...[truncated]`
- test('handles empty array') — `[]``""`
- test('uses custom delimiter') — delimiter 为 `"\n"` 时按行连接
#### describe('truncateToLines')
- test('returns full text when within limit') — 行数不超限时原样返回
- test('truncates and adds ellipsis') — 超限时截断并加 `…`
- test('handles exact limit') — 刚好等于 maxLines 时不截断
- test('handles single line') — 单行文本不截断
#### describe('EndTruncatingAccumulator')
- test('accumulates strings normally within limit')
- test('truncates when exceeding maxSize')
- test('reports truncated status correctly')
- test('reports totalBytes including truncated content')
- test('toString includes truncation marker')
- test('clear resets all state')
- test('append with Buffer works') — 接受 Buffer 类型
---
### src/utils/semver.ts — 测试文件: `src/utils/__tests__/semver.test.ts`
#### describe('gt / gte / lt / lte')
- test('gt: 2.0.0 > 1.0.0') → true
- test('gt: 1.0.0 > 1.0.0') → false
- test('gte: 1.0.0 >= 1.0.0') → true
- test('lt: 1.0.0 < 2.0.0') → true
- test('lte: 1.0.0 <= 1.0.0') → true
- test('handles pre-release versions') — `1.0.0-beta < 1.0.0`
#### describe('satisfies')
- test('version satisfies caret range') — `satisfies('1.2.3', '^1.0.0')` → true
- test('version does not satisfy range') — `satisfies('2.0.0', '^1.0.0')` → false
- test('exact match') — `satisfies('1.0.0', '1.0.0')` → true
#### describe('order')
- test('returns -1 for lesser') — `order('1.0.0', '2.0.0')` → -1
- test('returns 0 for equal') — `order('1.0.0', '1.0.0')` → 0
- test('returns 1 for greater') — `order('2.0.0', '1.0.0')` → 1
---
### src/utils/uuid.ts — 测试文件: `src/utils/__tests__/uuid.test.ts`
#### describe('validateUuid')
- test('accepts valid v4 UUID') — `'550e8400-e29b-41d4-a716-446655440000'` → 返回 UUID
- test('returns null for invalid format') — `'not-a-uuid'` → null
- test('returns null for empty string') — `''` → null
- test('returns null for null/undefined input')
- test('accepts uppercase UUIDs') — 大写字母有效
#### describe('createAgentId')
- test('returns string starting with "a"') — 前缀为 `a`
- test('has correct length') — 前缀 + 16 hex 字符
- test('generates unique ids') — 连续两次调用结果不同
---
### src/utils/format.ts — 测试文件: `src/utils/__tests__/format.test.ts`
#### describe('formatFileSize')
- test('formats bytes') — `500``"500 bytes"`
- test('formats kilobytes') — `1536``"1.5KB"`
- test('formats megabytes') — `1572864``"1.5MB"`
- test('formats gigabytes') — `1610612736``"1.5GB"`
- test('removes trailing .0') — `1024``"1KB"` (不是 `"1.0KB"`)
#### describe('formatSecondsShort')
- test('formats milliseconds to seconds') — `1234``"1.2s"`
- test('formats zero') — `0``"0.0s"`
#### describe('formatDuration')
- test('formats seconds') — `5000``"5s"`
- test('formats minutes and seconds') — `65000``"1m 5s"`
- test('formats hours') — `3661000``"1h 1m 1s"`
- test('formats days') — `90061000``"1d 1h 1m"`
- test('returns "0s" for zero') — `0``"0s"`
- test('hideTrailingZeros omits zero components') — `3600000` + `hideTrailingZeros``"1h"`
- test('mostSignificantOnly returns largest unit') — `3661000` + `mostSignificantOnly``"1h"`
#### describe('formatNumber')
- test('formats thousands') — `1321``"1.3k"`
- test('formats small numbers as-is') — `900``"900"`
- test('lowercase output') — `1500``"1.5k"` (不是 `"1.5K"`)
#### describe('formatTokens')
- test('strips .0 suffix') — `1000``"1k"` (不是 `"1.0k"`)
- test('keeps non-zero decimal') — `1500``"1.5k"`
#### describe('formatRelativeTime')
- test('formats past time') — now - 3600s → `"1h ago"` (narrow style)
- test('formats future time') — now + 3600s → `"in 1h"` (narrow style)
- test('formats less than 1 second') — now → `"0s ago"`
- test('uses custom now parameter for deterministic output')
---
### src/utils/json.ts — 测试文件: `src/utils/__tests__/json.test.ts`
#### describe('safeParseJSON')
- test('parses valid JSON') — `'{"a":1}'``{ a: 1 }`
- test('returns null for invalid JSON') — `'not json'` → null
- test('returns null for null input') — `null` → null
- test('returns null for undefined input') — `undefined` → null
- test('returns null for empty string') — `""` → null
- test('handles JSON with BOM') — BOM 前缀不影响解析
- test('caches results for repeated calls') — 同一输入不重复解析
#### describe('safeParseJSONC')
- test('parses JSON with comments') — 含 `//` 注释的 JSON 正确解析
- test('parses JSON with trailing commas') — 宽松模式
- test('returns null for invalid input')
- test('returns null for null input')
#### describe('parseJSONL')
- test('parses multiple JSON lines') — `'{"a":1}\n{"b":2}'``[{a:1}, {b:2}]`
- test('skips malformed lines') — 含错误行时跳过该行
- test('handles empty input') — `""``[]`
- test('handles trailing newline') — 尾部换行不产生空元素
- test('accepts Buffer input') — Buffer 类型同样工作
- test('handles BOM prefix')
#### describe('addItemToJSONCArray')
- test('adds item to existing array') — `[1, 2]` + 3 → `[1, 2, 3]`
- test('creates new array for empty content') — `""` + item → `[item]`
- test('creates new array for non-array content') — `'"hello"'` + item → `[item]`
- test('preserves comments in JSONC') — 注释不被丢弃
- test('handles empty array') — `"[]"` + item → `[item]`
---
### src/utils/diff.ts — 测试文件: `src/utils/__tests__/diff.test.ts`
#### describe('adjustHunkLineNumbers')
- test('shifts line numbers by positive offset') — 所有 hunk 的 oldStart/newStart 增加 offset
- test('shifts by negative offset') — 负 offset 减少行号
- test('handles empty hunk array') — `[]``[]`
#### describe('getPatchFromContents')
- test('returns empty array for identical content') — 相同内容无差异
- test('detects added lines') — 新内容多出行
- test('detects removed lines') — 旧内容缺少行
- test('detects modified lines') — 行内容变化
- test('handles empty old content') — 从空文件到有内容
- test('handles empty new content') — 删除所有内容
---
### src/utils/frontmatterParser.ts — 测试文件: `src/utils/__tests__/frontmatterParser.test.ts`
#### describe('parseFrontmatter')
- test('extracts YAML frontmatter between --- delimiters') — 正确提取 frontmatter 并返回 body
- test('returns empty frontmatter for content without ---') — 无 frontmatter 时 data 为空
- test('handles empty content') — `""` 正确处理
- test('handles frontmatter-only content') — 只有 frontmatter 无 body
- test('falls back to quoting on YAML parse error') — 无效 YAML 不崩溃
#### describe('splitPathInFrontmatter')
- test('splits comma-separated paths') — `"a.ts, b.ts"``["a.ts", "b.ts"]`
- test('expands brace patterns') — `"*.{ts,tsx}"``["*.ts", "*.tsx"]`
- test('handles string array input') — `["a.ts", "b.ts"]``["a.ts", "b.ts"]`
- test('respects braces in comma splitting') — 大括号内的逗号不作为分隔符
#### describe('parsePositiveIntFromFrontmatter')
- test('returns number for valid positive int') — `5``5`
- test('returns undefined for negative') — `-1` → undefined
- test('returns undefined for non-number') — `"abc"` → undefined
- test('returns undefined for float') — `1.5` → undefined
#### describe('parseBooleanFrontmatter')
- test('returns true for true') — `true` → true
- test('returns true for "true"') — `"true"` → true
- test('returns false for false') — `false` → false
- test('returns false for other values') — `"yes"`, `1` → false
#### describe('parseShellFrontmatter')
- test('returns bash for "bash"') — 正确识别
- test('returns powershell for "powershell"')
- test('returns undefined for invalid value') — `"zsh"` → undefined
---
### src/utils/file.ts纯函数部分— 测试文件: `src/utils/__tests__/file.test.ts`
#### describe('convertLeadingTabsToSpaces')
- test('converts single tab to 2 spaces') — `"\thello"``" hello"`
- test('converts multiple leading tabs') — `"\t\thello"``" hello"`
- test('does not convert tabs within line') — `"a\tb"` 保持原样
- test('handles mixed content')
#### describe('addLineNumbers')
- test('adds line numbers starting from 1') — 每行添加 `N\t` 前缀
- test('respects startLine parameter') — 从指定行号开始
- test('handles empty content')
#### describe('stripLineNumberPrefix')
- test('strips tab-prefixed line number') — `"1\thello"``"hello"`
- test('strips padded line number') — `" 1\thello"``"hello"`
- test('returns line unchanged when no prefix')
#### describe('pathsEqual')
- test('returns true for identical paths')
- test('handles trailing slashes') — 带/不带尾部斜杠视为相同
- test('handles case sensitivity based on platform')
#### describe('normalizePathForComparison')
- test('normalizes forward slashes')
- test('resolves path for comparison')
---
### src/utils/glob.ts纯函数部分— 测试文件: `src/utils/__tests__/glob.test.ts`
#### describe('extractGlobBaseDirectory')
- test('extracts static prefix from glob') — `"src/**/*.ts"``{ baseDir: "src", relativePattern: "**/*.ts" }`
- test('handles root-level glob') — `"*.ts"``{ baseDir: ".", relativePattern: "*.ts" }`
- test('handles deep static path') — `"src/utils/model/*.ts"` → baseDir 为 `"src/utils/model"`
- test('handles Windows drive root') — `"C:\\Users\\**\\*.ts"` 正确分割
---
### src/utils/tokens.ts纯函数部分— 测试文件: `src/utils/__tests__/tokens.test.ts`
#### describe('getTokenCountFromUsage')
- test('sums input and output tokens') — `{ input_tokens: 100, output_tokens: 50 }` → 150
- test('includes cache tokens') — cache_creation + cache_read 加入总数
- test('handles zero values') — 全 0 时返回 0
---
### src/utils/path.ts纯函数部分— 测试文件: `src/utils/__tests__/path.test.ts`
#### describe('containsPathTraversal')
- test('detects ../ traversal') — `"../etc/passwd"` → true
- test('detects mid-path traversal') — `"foo/../../bar"` → true
- test('returns false for safe paths') — `"src/utils/file.ts"` → false
- test('returns false for paths containing .. in names') — `"foo..bar"` → false
#### describe('normalizePathForConfigKey')
- test('converts backslashes to forward slashes') — `"src\\utils"``"src/utils"`
- test('leaves forward slashes unchanged')
---
## Mock 需求
本计划中的函数大部分为纯函数,**不需要 mock**。少数例外:
| 函数 | 依赖 | 处理 |
|------|------|------|
| `hashContent` / `hashPair` | `Bun.hash` | Bun 运行时下自动可用 |
| `formatRelativeTime` | `Date` | 使用 `now` 参数注入确定性时间 |
| `safeParseJSON` | `logError` | 可通过 `shouldLogError: false` 跳过 |
| `safeParseJSONC` | `logError` | mock `logError` 避免测试输出噪音 |

View File

@@ -1,134 +0,0 @@
# Context 构建测试计划
## 概述
Context 构建系统负责组装发送给 Claude API 的系统提示和用户上下文。包括 git 状态获取、CLAUDE.md 文件发现与加载、系统提示拼装三部分。
## 被测文件
| 文件 | 关键导出 |
|------|----------|
| `src/context.ts` | `getSystemContext`, `getUserContext`, `getGitStatus`, `setSystemPromptInjection` |
| `src/utils/claudemd.ts` | `stripHtmlComments`, `getClaudeMds`, `isMemoryFilePath`, `getLargeMemoryFiles`, `filterInjectedMemoryFiles`, `getExternalClaudeMdIncludes`, `hasExternalClaudeMdIncludes`, `processMemoryFile`, `getMemoryFiles` |
| `src/utils/systemPrompt.ts` | `buildEffectiveSystemPrompt` |
---
## 测试用例
### src/utils/claudemd.ts — 纯函数部分
#### describe('stripHtmlComments')
- test('strips block-level HTML comments') — `"text <!-- comment --> more"` → content 不含注释
- test('preserves inline content') — 行内文本保留
- test('preserves code block content') — ` ```html\n<!-- not stripped -->\n``` ` 内的注释不移除
- test('returns stripped: false when no comments') — 无注释时 stripped 为 false
- test('returns stripped: true when comments exist')
- test('handles empty string') — `""``{ content: "", stripped: false }`
- test('handles multiple comments') — 多个注释全部移除
#### describe('getClaudeMds')
- test('assembles memory files with type descriptions') — 不同 type 的文件有不同前缀描述
- test('includes instruction prompt prefix') — 输出包含指令前缀
- test('handles empty memory files array') — 空数组返回空字符串或最小前缀
- test('respects filter parameter') — filter 函数可过滤特定类型
- test('concatenates multiple files with separators')
#### describe('isMemoryFilePath')
- test('returns true for CLAUDE.md path') — `"/project/CLAUDE.md"` → true
- test('returns true for .claude/rules/ path') — `"/project/.claude/rules/foo.md"` → true
- test('returns true for memory file path') — `"~/.claude/memory/foo.md"` → true
- test('returns false for regular file') — `"/project/src/main.ts"` → false
- test('returns false for unrelated .md file') — `"/project/README.md"` → false
#### describe('getLargeMemoryFiles')
- test('returns files exceeding 40K chars') — 内容 > MAX_MEMORY_CHARACTER_COUNT 的文件被返回
- test('returns empty array when all files are small')
- test('correctly identifies threshold boundary')
#### describe('filterInjectedMemoryFiles')
- test('filters out AutoMem type files') — feature flag 开启时移除自动记忆
- test('filters out TeamMem type files')
- test('preserves other types') — 非 AutoMem/TeamMem 的文件保留
#### describe('getExternalClaudeMdIncludes')
- test('returns includes from outside CWD') — 外部 @include 路径被识别
- test('returns empty array when all includes are internal')
#### describe('hasExternalClaudeMdIncludes')
- test('returns true when external includes exist')
- test('returns false when no external includes')
---
### src/utils/systemPrompt.ts
#### describe('buildEffectiveSystemPrompt')
- test('returns default system prompt when no overrides') — 无任何覆盖时使用默认提示
- test('overrideSystemPrompt replaces everything') — override 模式替换全部内容
- test('customSystemPrompt replaces default') — `--system-prompt` 参数替换默认
- test('appendSystemPrompt is appended after main prompt') — append 在主提示之后
- test('agent definition replaces default prompt') — agent 模式使用 agent prompt
- test('agent definition with append combines both') — agent prompt + append
- test('override takes precedence over agent and custom') — 优先级最高
- test('returns array of strings') — 返回值为 SystemPrompt 类型(字符串数组)
---
### src/context.ts — 需 Mock 的部分
#### describe('getGitStatus')
- test('returns formatted git status string') — 包含 branch、status、log、user
- test('truncates status at 2000 chars') — 超长 status 被截断
- test('returns null in test environment') — `NODE_ENV=test` 时返回 null
- test('returns null in non-git directory') — 非 git 仓库返回 null
- test('runs git commands in parallel') — 多个 git 命令并行执行
#### describe('getSystemContext')
- test('includes gitStatus key') — 返回对象包含 gitStatus
- test('returns memoized result on subsequent calls') — 多次调用返回同一结果
- test('skips git when instructions disabled')
#### describe('getUserContext')
- test('includes currentDate key') — 返回对象包含当前日期
- test('includes claudeMd key when CLAUDE.md exists') — 加载 CLAUDE.md 内容
- test('respects CLAUDE_CODE_DISABLE_CLAUDE_MDS env') — 设置后不加载 CLAUDE.md
- test('returns memoized result')
#### describe('setSystemPromptInjection')
- test('clears memoized context caches') — 调用后下次 getSystemContext/getUserContext 重新计算
- test('injection value is accessible via getSystemPromptInjection')
---
## Mock 需求
| 依赖 | Mock 方式 | 用途 |
|------|-----------|------|
| `execFileNoThrow` | `mock.module` | `getGitStatus` 中的 git 命令 |
| `getMemoryFiles` | `mock.module` | `getUserContext` 中的 CLAUDE.md 加载 |
| `getCwd` | `mock.module` | 路径解析上下文 |
| `process.env.NODE_ENV` | 直接设置 | 测试环境检测 |
| `process.env.CLAUDE_CODE_DISABLE_CLAUDE_MDS` | 直接设置 | 禁用 CLAUDE.md |
## 集成测试场景
放在 `tests/integration/context-build.test.ts`
### describe('Context assembly pipeline')
- test('getUserContext produces claudeMd containing CLAUDE.md content') — 端到端验证 CLAUDE.md 被正确加载到 context
- test('buildEffectiveSystemPrompt + getUserContext produces complete prompt') — 系统提示 + 用户上下文完整性
- test('setSystemPromptInjection invalidates and rebuilds context') — 注入后重新构建上下文

View File

@@ -1,104 +0,0 @@
# 权限系统测试计划
## 概述
权限系统控制工具是否可以执行,包含规则解析器、权限检查管线和权限模式判断。测试重点是纯函数解析器和规则匹配逻辑。
## 被测文件
| 文件 | 关键导出 |
|------|----------|
| `src/utils/permissions/permissionRuleParser.ts` | `permissionRuleValueFromString`, `permissionRuleValueToString`, `escapeRuleContent`, `unescapeRuleContent`, `normalizeLegacyToolName`, `getLegacyToolNames` |
| `src/utils/permissions/PermissionMode.ts` | 权限模式常量和辅助函数 |
| `src/utils/permissions/permissions.ts` | `hasPermissionsToUseTool`, `getDenyRuleForTool`, `checkRuleBasedPermissions` |
| `src/types/permissions.ts` | `PermissionMode`, `PermissionBehavior`, `PermissionRule` 类型定义 |
---
## 测试用例
### src/utils/permissions/permissionRuleParser.ts
#### describe('escapeRuleContent')
- test('escapes backslashes first') — `'test\\value'``'test\\\\value'`
- test('escapes opening parentheses') — `'print(1)'``'print\\(1\\)'`
- test('escapes closing parentheses') — `'func()'``'func\\(\\)'`
- test('handles combined escape') — `'echo "test\\nvalue"'` 中的 `\\` 先转义
- test('handles empty string') — `''``''`
- test('no-op for string without special chars') — `'npm install'` 原样返回
#### describe('unescapeRuleContent')
- test('unescapes parentheses') — `'print\\(1\\)'``'print(1)'`
- test('unescapes backslashes last') — `'test\\\\nvalue'``'test\\nvalue'`
- test('handles empty string')
- test('roundtrip: escape then unescape returns original') — `unescapeRuleContent(escapeRuleContent(x)) === x`
#### describe('permissionRuleValueFromString')
- test('parses tool name only') — `'Bash'``{ toolName: 'Bash' }`
- test('parses tool name with content') — `'Bash(npm install)'``{ toolName: 'Bash', ruleContent: 'npm install' }`
- test('parses content with escaped parentheses') — `'Bash(python -c "print\\(1\\)")'` → ruleContent 为 `'python -c "print(1)"'`
- test('treats empty parens as tool-wide rule') — `'Bash()'``{ toolName: 'Bash' }`(无 ruleContent
- test('treats wildcard content as tool-wide rule') — `'Bash(*)'``{ toolName: 'Bash' }`
- test('normalizes legacy tool names') — `'Task'``{ toolName: 'Agent' }`(或对应的 AGENT_TOOL_NAME
- test('handles malformed input: no closing paren') — `'Bash(npm'` → 整个字符串作为 toolName
- test('handles malformed input: content after closing paren') — `'Bash(npm)extra'` → 整个字符串作为 toolName
- test('handles missing tool name') — `'(foo)'` → 整个字符串作为 toolName
#### describe('permissionRuleValueToString')
- test('serializes tool name only') — `{ toolName: 'Bash' }``'Bash'`
- test('serializes with content') — `{ toolName: 'Bash', ruleContent: 'npm install' }``'Bash(npm install)'`
- test('escapes content with parentheses') — ruleContent 含 `()` 时正确转义
- test('roundtrip: fromString then toString preserves value') — 往返一致
#### describe('normalizeLegacyToolName')
- test('maps Task to Agent tool name') — `'Task'` → AGENT_TOOL_NAME
- test('maps KillShell to TaskStop tool name') — `'KillShell'` → TASK_STOP_TOOL_NAME
- test('maps AgentOutputTool to TaskOutput tool name')
- test('returns unknown names unchanged') — `'UnknownTool'``'UnknownTool'`
#### describe('getLegacyToolNames')
- test('returns legacy names for canonical name') — 给定 AGENT_TOOL_NAME 返回包含 `'Task'`
- test('returns empty array for name with no legacy aliases')
---
### src/utils/permissions/permissions.ts — 需 Mock
#### describe('getDenyRuleForTool')
- test('returns deny rule matching tool name') — 匹配到 blanket deny 规则时返回
- test('returns null when no deny rules match') — 无匹配时返回 null
- test('matches MCP tools by server prefix') — `mcp__server` 规则匹配该 server 下的 MCP 工具
- test('does not match content-specific deny rules') — 有 ruleContent 的 deny 规则不作为 blanket deny
#### describe('checkRuleBasedPermissions')(集成级别)
- test('deny rule takes precedence over allow') — 同时有 allow 和 deny 时 deny 优先
- test('ask rule prompts user') — 匹配 ask 规则返回 `{ behavior: 'ask' }`
- test('allow rule permits execution') — 匹配 allow 规则返回 `{ behavior: 'allow' }`
- test('passthrough when no rules match') — 无匹配规则返回 passthrough
---
## Mock 需求
| 依赖 | Mock 方式 | 说明 |
|------|-----------|------|
| `bun:bundle` (feature) | 已 polyfill | BRIEF_TOOL_NAME 条件加载 |
| Tool 常量导入 | 实际值 | AGENT_TOOL_NAME 等从常量文件导入 |
| `appState` | mock object | `hasPermissionsToUseTool` 中的状态依赖 |
| Tool 对象 | mock object | 模拟 tool 的 name, checkPermissions 等 |
## 集成测试场景
### describe('Permission pipeline end-to-end')
- test('deny rule blocks tool before it runs') — deny 规则在 call 前拦截
- test('bypassPermissions mode allows all') — bypass 模式下 ask → allow
- test('dontAsk mode converts ask to deny') — dontAsk 模式下 ask → deny

View File

@@ -1,113 +0,0 @@
# 模型路由测试计划
## 概述
模型路由系统负责 API provider 选择、模型别名解析、模型名规范化和运行时模型决策。测试重点是纯函数和环境变量驱动的逻辑。
## 被测文件
| 文件 | 关键导出 |
|------|----------|
| `src/utils/model/aliases.ts` | `MODEL_ALIASES`, `MODEL_FAMILY_ALIASES`, `isModelAlias`, `isModelFamilyAlias` |
| `src/utils/model/providers.ts` | `APIProvider`, `getAPIProvider`, `isFirstPartyAnthropicBaseUrl` |
| `src/utils/model/model.ts` | `firstPartyNameToCanonical`, `getCanonicalName`, `parseUserSpecifiedModel`, `normalizeModelStringForAPI`, `getRuntimeMainLoopModel`, `getDefaultMainLoopModelSetting` |
---
## 测试用例
### src/utils/model/aliases.ts
#### describe('isModelAlias')
- test('returns true for "sonnet"') — 有效别名
- test('returns true for "opus"')
- test('returns true for "haiku"')
- test('returns true for "best"')
- test('returns true for "sonnet[1m]"')
- test('returns true for "opus[1m]"')
- test('returns true for "opusplan"')
- test('returns false for full model ID') — `'claude-sonnet-4-6-20250514'` → false
- test('returns false for unknown string') — `'gpt-4'` → false
- test('is case-sensitive') — `'Sonnet'` → false别名是小写
#### describe('isModelFamilyAlias')
- test('returns true for "sonnet"')
- test('returns true for "opus"')
- test('returns true for "haiku"')
- test('returns false for "best"') — best 不是 family alias
- test('returns false for "opusplan"')
- test('returns false for "sonnet[1m]"')
---
### src/utils/model/providers.ts
#### describe('getAPIProvider')
- test('returns "firstParty" by default') — 无相关 env 时返回 firstParty
- test('returns "bedrock" when CLAUDE_CODE_USE_BEDROCK is set') — env 为 truthy 值
- test('returns "vertex" when CLAUDE_CODE_USE_VERTEX is set')
- test('returns "foundry" when CLAUDE_CODE_USE_FOUNDRY is set')
- test('bedrock takes precedence over vertex') — 多个 env 同时设置时 bedrock 优先
#### describe('isFirstPartyAnthropicBaseUrl')
- test('returns true when ANTHROPIC_BASE_URL is not set') — 默认 API
- test('returns true for api.anthropic.com') — `'https://api.anthropic.com'` → true
- test('returns false for custom URL') — `'https://my-proxy.com'` → false
- test('returns false for invalid URL') — 非法 URL → false
- test('returns true for staging URL when USER_TYPE is ant') — `'https://api-staging.anthropic.com'` + ant → true
---
### src/utils/model/model.ts
#### describe('firstPartyNameToCanonical')
- test('maps opus-4-6 full name to canonical') — `'claude-opus-4-6-20250514'``'claude-opus-4-6'`
- test('maps sonnet-4-6 full name') — `'claude-sonnet-4-6-20250514'``'claude-sonnet-4-6'`
- test('maps haiku-4-5') — `'claude-haiku-4-5-20251001'``'claude-haiku-4-5'`
- test('maps 3P provider format') — `'us.anthropic.claude-opus-4-6-v1:0'``'claude-opus-4-6'`
- test('maps claude-3-7-sonnet') — `'claude-3-7-sonnet-20250219'``'claude-3-7-sonnet'`
- test('maps claude-3-5-sonnet') → `'claude-3-5-sonnet'`
- test('maps claude-3-5-haiku') → `'claude-3-5-haiku'`
- test('maps claude-3-opus') → `'claude-3-opus'`
- test('is case insensitive') — `'Claude-Opus-4-6'``'claude-opus-4-6'`
- test('falls back to input for unknown model') — `'unknown-model'``'unknown-model'`
- test('differentiates opus-4 vs opus-4-5 vs opus-4-6') — 更具体的版本优先匹配
#### describe('parseUserSpecifiedModel')
- test('resolves "sonnet" to default sonnet model')
- test('resolves "opus" to default opus model')
- test('resolves "haiku" to default haiku model')
- test('resolves "best" to best model')
- test('resolves "opusplan" to default sonnet model') — opusplan 默认用 sonnet
- test('appends [1m] suffix when alias has [1m]') — `'sonnet[1m]'` → 模型名 + `'[1m]'`
- test('preserves original case for custom model names') — `'my-Custom-Model'` 保留大小写
- test('handles [1m] suffix on non-alias models') — `'custom-model[1m]'``'custom-model[1m]'`
- test('trims whitespace') — `' sonnet '` → 正确解析
#### describe('getRuntimeMainLoopModel')
- test('returns mainLoopModel by default') — 无特殊条件时原样返回
- test('returns opus in plan mode when opusplan is set') — opusplan + plan mode → opus
- test('returns sonnet in plan mode when haiku is set') — haiku + plan mode → sonnet 升级
- test('returns mainLoopModel in non-plan mode') — 非 plan 模式不做替换
---
## Mock 需求
| 依赖 | Mock 方式 | 说明 |
|------|-----------|------|
| `process.env.CLAUDE_CODE_USE_BEDROCK/VERTEX/FOUNDRY` | 直接设置/恢复 | provider 选择 |
| `process.env.ANTHROPIC_BASE_URL` | 直接设置/恢复 | URL 检测 |
| `process.env.USER_TYPE` | 直接设置/恢复 | staging URL 和 ant 功能 |
| `getModelStrings()` | mock.module | 返回固定模型 ID |
| `getMainLoopModelOverride` | mock.module | 会话中模型覆盖 |
| `getSettings_DEPRECATED` | mock.module | 用户设置中的模型 |
| `getUserSpecifiedModelSetting` | mock.module | `getRuntimeMainLoopModel` 依赖 |
| `isModelAllowed` | mock.module | allowlist 检查 |

View File

@@ -1,165 +0,0 @@
# 消息处理测试计划
## 概述
消息处理系统负责消息的创建、查询、规范化和文本提取。覆盖消息类型定义、消息工厂函数、消息过滤/查询工具和 API 规范化管线。
## 被测文件
| 文件 | 关键导出 |
|------|----------|
| `src/types/message.ts` | `MessageType`, `Message`, `AssistantMessage`, `UserMessage`, `SystemMessage` 等类型 |
| `src/utils/messages.ts` | 消息创建、查询、规范化、文本提取等函数(~3100 行) |
| `src/utils/messages/mappers.ts` | 消息映射工具 |
---
## 测试用例
### src/utils/messages.ts — 消息创建
#### describe('createAssistantMessage')
- test('creates message with type "assistant"') — type 字段正确
- test('creates message with role "assistant"') — role 正确
- test('creates message with empty content array') — 默认 content 为空
- test('generates unique uuid') — 每次调用 uuid 不同
- test('includes costUsd as 0')
#### describe('createUserMessage')
- test('creates message with type "user"') — type 字段正确
- test('creates message with provided content') — content 正确传入
- test('generates unique uuid')
#### describe('createSystemMessage')
- test('creates system message with correct type')
- test('includes message content')
#### describe('createProgressMessage')
- test('creates progress message with data')
- test('has correct type "progress"')
---
### src/utils/messages.ts — 消息查询
#### describe('getLastAssistantMessage')
- test('returns last assistant message from array') — 多条消息中返回最后一条 assistant
- test('returns undefined for empty array')
- test('returns undefined when no assistant messages exist')
#### describe('hasToolCallsInLastAssistantTurn')
- test('returns true when last assistant has tool_use content') — content 含 tool_use block
- test('returns false when last assistant has only text')
- test('returns false for empty messages')
#### describe('isSyntheticMessage')
- test('identifies interrupt message as synthetic') — INTERRUPT_MESSAGE 标记
- test('identifies cancel message as synthetic')
- test('returns false for normal user messages')
#### describe('isNotEmptyMessage')
- test('returns true for message with content')
- test('returns false for message with empty content array')
- test('returns false for message with empty text content')
---
### src/utils/messages.ts — 文本提取
#### describe('getAssistantMessageText')
- test('extracts text from text blocks') — content 含 `{ type: 'text', text: 'hello' }` 时提取
- test('returns empty string for non-text content') — 仅含 tool_use 时返回空
- test('concatenates multiple text blocks')
#### describe('getUserMessageText')
- test('extracts text from string content') — content 为纯字符串
- test('extracts text from content array') — content 为数组时提取 text 块
- test('handles empty content')
#### describe('extractTextContent')
- test('extracts text items from mixed content') — 过滤出 type: 'text' 的项
- test('returns empty array for all non-text content')
---
### src/utils/messages.ts — 规范化
#### describe('normalizeMessages')
- test('converts raw messages to normalized format') — 消息数组规范化
- test('handles empty array') — `[]``[]`
- test('preserves message order')
- test('handles mixed message types')
#### describe('normalizeMessagesForAPI')
- test('filters out system messages') — 系统消息不发送给 API
- test('filters out progress messages')
- test('filters out attachment messages')
- test('preserves user and assistant messages')
- test('reorders tool results to match API expectations')
- test('handles empty array')
---
### src/utils/messages.ts — 合并
#### describe('mergeUserMessages')
- test('merges consecutive user messages') — 相邻用户消息合并
- test('does not merge non-consecutive user messages')
- test('preserves assistant messages between user messages')
#### describe('mergeAssistantMessages')
- test('merges consecutive assistant messages')
- test('combines content arrays')
---
### src/utils/messages.ts — 辅助函数
#### describe('buildMessageLookups')
- test('builds index by message uuid') — 按 uuid 建立查找表
- test('returns empty lookups for empty messages')
- test('handles duplicate uuids gracefully')
---
## Mock 需求
| 依赖 | Mock 方式 | 说明 |
|------|-----------|------|
| `crypto.randomUUID` | `mock` 或 spy | 消息创建中的 uuid 生成 |
| Message 对象 | 手动构造 | 创建符合类型的 mock 消息对象 |
### Mock 消息工厂(放在 `tests/mocks/messages.ts`
```typescript
// 通用 mock 消息构造器
export function mockAssistantMessage(overrides?: Partial<AssistantMessage>): AssistantMessage
export function mockUserMessage(content: string, overrides?: Partial<UserMessage>): UserMessage
export function mockSystemMessage(overrides?: Partial<SystemMessage>): SystemMessage
export function mockToolUseBlock(name: string, input: unknown): ToolUseBlock
export function mockToolResultMessage(toolUseId: string, content: string): UserMessage
```
## 集成测试场景
### describe('Message pipeline')
- test('create → normalize → API format produces valid request') — 创建消息 → normalizeMessagesForAPI → 验证输出结构
- test('tool use and tool result pairing is preserved through normalization')
- test('merge + normalize handles conversation with interruptions')

View File

@@ -1,112 +0,0 @@
# Cron 调度测试计划
## 概述
Cron 模块提供 cron 表达式解析、下次运行时间计算和人类可读描述。全部为纯函数,无外部依赖,是最适合单元测试的模块之一。
## 被测文件
| 文件 | 关键导出 |
|------|----------|
| `src/utils/cron.ts` | `CronFields`, `parseCronExpression`, `computeNextCronRun`, `cronToHuman` |
---
## 测试用例
### describe('parseCronExpression')
#### 有效表达式
- test('parses wildcard fields') — `'* * * * *'` → 每个字段为完整范围
- test('parses specific values') — `'30 14 1 6 3'` → minute=[30], hour=[14], dom=[1], month=[6], dow=[3]
- test('parses step syntax') — `'*/5 * * * *'` → minute=[0,5,10,...,55]
- test('parses range syntax') — `'1-5 * * * *'` → minute=[1,2,3,4,5]
- test('parses range with step') — `'1-10/3 * * * *'` → minute=[1,4,7,10]
- test('parses comma-separated list') — `'1,15,30 * * * *'` → minute=[1,15,30]
- test('parses day-of-week 7 as Sunday alias') — `'0 0 * * 7'` → dow=[0]
- test('parses range with day-of-week 7') — `'0 0 * * 5-7'` → dow=[0,5,6]
- test('parses complex combined expression') — `'0,30 9-17 * * 1-5'` → 工作日 9-17 每半小时
#### 无效表达式
- test('returns null for wrong field count') — `'* * *'` → null
- test('returns null for out-of-range values') — `'60 * * * *'` → nullminute max=59
- test('returns null for invalid step') — `'*/0 * * * *'` → nullstep=0
- test('returns null for reversed range') — `'10-5 * * * *'` → nulllo>hi
- test('returns null for empty string') — `''` → null
- test('returns null for non-numeric tokens') — `'abc * * * *'` → null
#### 字段范围验证
- test('minute: 0-59')
- test('hour: 0-23')
- test('dayOfMonth: 1-31')
- test('month: 1-12')
- test('dayOfWeek: 0-6 (plus 7 alias)')
---
### describe('computeNextCronRun')
#### 基本匹配
- test('finds next minute') — from 14:30:45, cron `'31 14 * * *'` → 14:31:00 同天
- test('finds next hour') — from 14:30, cron `'0 15 * * *'` → 15:00 同天
- test('rolls to next day') — from 14:30, cron `'0 10 * * *'` → 10:00 次日
- test('rolls to next month') — from 1月31日, cron `'0 0 1 * *'` → 2月1日
- test('is strictly after from date') — from 恰好匹配时应返回下一次而非当前时间
#### DOM/DOW 语义
- test('OR semantics when both dom and dow constrained') — dom=15, dow=3 → 匹配 15 号 OR 周三
- test('only dom constrained uses dom') — dom=15, dow=* → 只匹配 15 号
- test('only dow constrained uses dow') — dom=*, dow=3 → 只匹配周三
- test('both wildcarded matches every day') — dom=*, dow=* → 每天
#### 边界情况
- test('handles month boundary') — 从 2 月 28 日寻找 2 月 29 日或 3 月 1 日
- test('returns null after 366-day search') — 不可能匹配的表达式返回 null理论上不会发生
- test('handles step across midnight') — `'0 0 * * *'` 从 23:59 → 次日 0:00
#### 每 N 分钟
- test('every 5 minutes from arbitrary time') — `'*/5 * * * *'` from 14:32 → 14:35
- test('every minute') — `'* * * * *'` from 14:32:45 → 14:33:00
---
### describe('cronToHuman')
#### 常见模式
- test('every N minutes') — `'*/5 * * * *'``'Every 5 minutes'`
- test('every minute') — `'*/1 * * * *'``'Every minute'`
- test('every hour at :00') — `'0 * * * *'``'Every hour'`
- test('every hour at :30') — `'30 * * * *'``'Every hour at :30'`
- test('every N hours') — `'0 */2 * * *'``'Every 2 hours'`
- test('daily at specific time') — `'30 9 * * *'``'Every day at 9:30 AM'`
- test('specific day of week') — `'0 9 * * 3'``'Every Wednesday at 9:00 AM'`
- test('weekdays') — `'0 9 * * 1-5'``'Weekdays at 9:00 AM'`
#### Fallback
- test('returns raw cron for complex patterns') — 非常见模式返回原始 cron 字符串
- test('returns raw cron for wrong field count') — `'* * *'` → 原样返回
#### UTC 模式
- test('UTC option formats time in local timezone') — `{ utc: true }` 时 UTC 时间转本地显示
- test('UTC midnight crossing adjusts day name') — UTC 时间跨天时本地星期名正确
---
## Mock 需求
**无需 Mock**。所有函数为纯函数,唯一的外部依赖是 `Date` 构造器和 `toLocaleTimeString`,可通过传入确定性的 `from` 参数控制。
## 注意事项
- `cronToHuman` 的时间格式化依赖系统 locale测试中建议使用 `'en-US'` locale 或只验证部分输出
- `computeNextCronRun` 使用本地时区DST 相关测试需注意运行环境

View File

@@ -1,106 +0,0 @@
# Git 工具测试计划
## 概述
Git 工具模块提供 git 远程 URL 规范化、仓库根目录查找、裸仓库安全检测等功能。测试重点是纯函数的 URL 规范化和需要文件系统 mock 的仓库发现逻辑。
## 被测文件
| 文件 | 关键导出 |
|------|----------|
| `src/utils/git.ts` | `normalizeGitRemoteUrl`, `findGitRoot`, `findCanonicalGitRoot`, `getIsGit`, `isAtGitRoot`, `getRepoRemoteHash`, `isCurrentDirectoryBareGitRepo`, `gitExe`, `getGitState`, `stashToCleanState`, `preserveGitStateForIssue` |
---
## 测试用例
### describe('normalizeGitRemoteUrl')(纯函数)
#### SSH 格式
- test('normalizes SSH URL') — `'git@github.com:owner/repo.git'``'github.com/owner/repo'`
- test('normalizes SSH URL without .git suffix') — `'git@github.com:owner/repo'``'github.com/owner/repo'`
- test('handles GitLab SSH') — `'git@gitlab.com:group/subgroup/repo.git'``'gitlab.com/group/subgroup/repo'`
#### HTTPS 格式
- test('normalizes HTTPS URL') — `'https://github.com/owner/repo.git'``'github.com/owner/repo'`
- test('normalizes HTTPS URL without .git suffix') — `'https://github.com/owner/repo'``'github.com/owner/repo'`
- test('normalizes HTTP URL') — `'http://github.com/owner/repo.git'``'github.com/owner/repo'`
#### SSH:// 协议格式
- test('normalizes ssh:// URL') — `'ssh://git@github.com/owner/repo'``'github.com/owner/repo'`
- test('handles user prefix in ssh://') — `'ssh://user@host/path'``'host/path'`
#### 代理 URLCCR git proxy
- test('normalizes legacy proxy URL') — `'http://local_proxy@127.0.0.1:16583/git/owner/repo'``'github.com/owner/repo'`
- test('normalizes GHE proxy URL') — `'http://user@127.0.0.1:8080/git/ghe.company.com/owner/repo'``'ghe.company.com/owner/repo'`
#### 边界情况
- test('returns null for empty string') — `''` → null
- test('returns null for whitespace') — `' '` → null
- test('returns null for unrecognized format') — `'not-a-url'` → null
- test('output is lowercase') — `'git@GitHub.com:Owner/Repo.git'``'github.com/owner/repo'`
- test('SSH and HTTPS for same repo produce same result') — 相同仓库不同协议 → 相同输出
---
### describe('findGitRoot')(需文件系统 Mock
- test('finds git root from nested directory') — `/project/src/utils/``/project/`(假设 `/project/.git` 存在)
- test('finds git root from root directory') — `/project/``/project/`
- test('returns null for non-git directory') — 无 `.git` → null
- test('handles worktree .git file') — `.git` 为文件时也识别
- test('memoizes results') — 同一路径不重复查找
### describe('findCanonicalGitRoot')
- test('returns same as findGitRoot for regular repo')
- test('resolves worktree to main repo root') — worktree 路径 → 主仓库根目录
- test('returns null for non-git directory')
### describe('gitExe')
- test('returns git path string') — 返回字符串
- test('memoizes the result') — 多次调用返回同一值
---
### describe('getRepoRemoteHash')(需 Mock
- test('returns 16-char hex hash') — 返回值为 16 位十六进制字符串
- test('returns null when no remote') — 无 remote URL 时返回 null
- test('same repo SSH/HTTPS produce same hash') — 不同协议同一仓库 hash 相同
---
### describe('isCurrentDirectoryBareGitRepo')(需文件系统 Mock
- test('detects bare git repo attack vector') — 目录含 HEAD + objects/ + refs/ 但无有效 .git/HEAD → true
- test('returns false for normal directory') — 普通目录 → false
- test('returns false for regular git repo') — 有效 .git 目录 → false
---
## Mock 需求
| 依赖 | Mock 方式 | 说明 |
|------|-----------|------|
| `statSync` | mock module | `findGitRoot` 中的 .git 检测 |
| `readFileSync` | mock module | worktree .git 文件读取 |
| `realpathSync` | mock module | 路径解析 |
| `execFileNoThrow` | mock module | git 命令执行 |
| `whichSync` | mock module | `gitExe` 中的 git 路径查找 |
| `getCwd` | mock module | 当前工作目录 |
| `getRemoteUrl` | mock module | `getRepoRemoteHash` 依赖 |
| 临时目录 | `mkdtemp` | 集成测试中创建临时 git 仓库 |
## 集成测试场景
### describe('Git repo discovery')(放在 tests/integration/
- test('findGitRoot works in actual git repo') — 在临时 git init 的目录中验证
- test('normalizeGitRemoteUrl + getRepoRemoteHash produces stable hash') — URL → hash 端到端验证

View File

@@ -1,161 +0,0 @@
# 配置系统测试计划
## 概述
配置系统包含全局配置GlobalConfig、项目配置ProjectConfig和设置Settings三层。测试重点是纯函数校验逻辑、Zod schema 验证和配置合并策略。
## 被测文件
| 文件 | 关键导出 |
|------|----------|
| `src/utils/config.ts` | `getGlobalConfig`, `saveGlobalConfig`, `getCurrentProjectConfig`, `checkHasTrustDialogAccepted`, `isPathTrusted`, `getOrCreateUserID`, `isAutoUpdaterDisabled` |
| `src/utils/settings/settings.ts` | `getSettingsForSource`, `parseSettingsFile`, `getSettingsFilePathForSource`, `getInitialSettings` |
| `src/utils/settings/types.ts` | `SettingsSchema`Zod schema |
| `src/utils/settings/validation.ts` | 设置验证函数 |
| `src/utils/settings/constants.ts` | 设置常量 |
---
## 测试用例
### src/utils/config.ts — 纯函数/常量
#### describe('DEFAULT_GLOBAL_CONFIG')
- test('has all required fields') — 默认配置对象包含所有必需字段
- test('has null auth fields by default') — oauthAccount 等为 null
#### describe('DEFAULT_PROJECT_CONFIG')
- test('has empty allowedTools') — 默认为空数组
- test('has empty mcpServers') — 默认为空对象
#### describe('isAutoUpdaterDisabled')
- test('returns true when CLAUDE_CODE_DISABLE_AUTOUPDATER is set') — env 设置时禁用
- test('returns true when disableAutoUpdater config is true')
- test('returns false by default')
---
### src/utils/config.ts — 需 Mock
#### describe('getGlobalConfig')
- test('returns cached config on subsequent calls') — 缓存机制
- test('returns TEST_GLOBAL_CONFIG_FOR_TESTING in test mode')
- test('reads config from ~/.claude.json')
- test('returns default config when file does not exist')
#### describe('saveGlobalConfig')
- test('applies updater function to current config') — updater 修改被保存
- test('creates backup before writing') — 写入前备份
- test('prevents auth state loss') — `wouldLoseAuthState` 检查
#### describe('getCurrentProjectConfig')
- test('returns project config for current directory')
- test('returns default config when no project config exists')
#### describe('checkHasTrustDialogAccepted')
- test('returns true when trust is accepted in current directory')
- test('returns true when parent directory is trusted') — 父目录信任传递
- test('returns false when no trust accepted')
- test('caches positive results')
#### describe('isPathTrusted')
- test('returns true for trusted path')
- test('returns false for untrusted path')
#### describe('getOrCreateUserID')
- test('returns existing user ID from config')
- test('creates and persists new ID when none exists')
- test('returns consistent ID across calls')
---
### src/utils/settings/settings.ts
#### describe('getSettingsFilePathForSource')
- test('returns ~/.claude/settings.json for userSettings') — 全局用户设置路径
- test('returns .claude/settings.json for projectSettings') — 项目设置路径
- test('returns .claude/settings.local.json for localSettings') — 本地设置路径
#### describe('parseSettingsFile')(需 Mock 文件读取)
- test('parses valid settings JSON') — 有效 JSON → `{ settings, errors: [] }`
- test('returns errors for invalid fields') — 无效字段 → errors 非空
- test('returns empty settings for non-existent file')
- test('handles JSON with comments') — JSONC 格式支持
#### describe('getInitialSettings')
- test('merges settings from all sources') — user + project + local 合并
- test('later sources override earlier ones') — 优先级policy > user > project > local
---
### src/utils/settings/types.ts — Zod Schema 验证
#### describe('SettingsSchema validation')
- test('accepts valid minimal settings') — `{}` → 有效
- test('accepts permissions block') — `{ permissions: { allow: ['Bash(*)'] } }` → 有效
- test('accepts model setting') — `{ model: 'sonnet' }` → 有效
- test('accepts hooks configuration') — 有效的 hooks 对象被接受
- test('accepts env variables') — `{ env: { FOO: 'bar' } }` → 有效
- test('rejects unknown top-level keys') — 未知字段被拒绝或忽略(取决于 schema 配置)
- test('rejects invalid permission mode') — `{ permissions: { defaultMode: 'invalid' } }` → 错误
- test('rejects non-string model') — `{ model: 123 }` → 错误
- test('accepts mcpServers configuration') — MCP server 配置有效
- test('accepts sandbox configuration')
---
### src/utils/settings/validation.ts
#### describe('settings validation')
- test('validates permission rules format') — `'Bash(npm install)'` 格式正确
- test('rejects malformed permission rules')
- test('validates hook configuration structure')
- test('provides helpful error messages') — 错误信息包含字段路径
---
## Mock 需求
| 依赖 | Mock 方式 | 说明 |
|------|-----------|------|
| 文件系统 | 临时目录 + mock | config 文件读写 |
| `lockfile` | mock module | 文件锁 |
| `getCwd` | mock module | 项目路径判断 |
| `findGitRoot` | mock module | 项目根目录 |
| `process.env` | 直接设置/恢复 | `CLAUDE_CODE_DISABLE_AUTOUPDATER` 等 |
### 测试用临时文件结构
```
/tmp/claude-test-xxx/
├── .claude/
│ ├── settings.json # projectSettings
│ └── settings.local.json # localSettings
├── home/
│ └── .claude/
│ └── settings.json # userSettingsmock HOME
└── project/
└── .git/
```
## 集成测试场景
### describe('Config + Settings merge pipeline')
- test('user settings + project settings merge correctly') — 验证合并优先级
- test('deny rules from settings are reflected in tool permission context')
- test('trust dialog state persists across config reads')

View File

@@ -1,361 +0,0 @@
# 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

@@ -1,177 +0,0 @@
# 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

@@ -1,145 +0,0 @@
# 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

@@ -1,71 +0,0 @@
# 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

@@ -1,191 +0,0 @@
# 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

@@ -1,67 +0,0 @@
# 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,188 +0,0 @@
# 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` 实现确定性测试

View File

@@ -1,203 +0,0 @@
# Phase 17 — Tool 子模块纯逻辑测试
> 创建日期2026-04-02
> 预计:+150 tests / 11 files
> 目标:覆盖 Tool 目录下有丰富纯逻辑但零测试的子模块
---
## 17.1 `src/tools/PowerShellTool/__tests__/powershellSecurity.test.ts`~25 tests
**目标模块**: `src/tools/PowerShellTool/powershellSecurity.ts`1091 行)
**安全关键** — 检测 ~20 种攻击向量。
| 测试分组 | 测试数 | 验证点 |
|---------|-------|--------|
| Invoke-Expression 检测 | 3 | `IEX`, `Invoke-Expression`, 变形 |
| Download cradle 检测 | 3 | `Net.WebClient`, `Invoke-WebRequest`, pipe |
| Privilege escalation | 3 | `Start-Process -Verb RunAs`, `runas.exe` |
| COM object | 2 | `New-Object -ComObject`, WScript.Shell |
| Scheduled tasks | 2 | `schtasks`, `Register-ScheduledTask` |
| WMI | 2 | `Invoke-WmiMethod`, `Get-WmiObject` |
| Module loading | 2 | `Import-Module` 从网络路径 |
| 安全命令通过 | 3 | `Get-Process`, `Get-ChildItem`, `Write-Host` |
| 混淆绕过尝试 | 3 | base64, 字符串拼接, 空格变形 |
| 组合命令 | 2 | `;` 分隔的多命令 |
**Mock**: 构造 `ParsedPowerShellCommand` 对象(不需要真实 AST
---
## 17.2 `src/tools/PowerShellTool/__tests__/commandSemantics.test.ts`~10 tests
**目标模块**: `src/tools/PowerShellTool/commandSemantics.ts`143 行)
| 测试用例 | 验证点 |
|---------|--------|
| grep exit 0/1/2 | 语义映射 |
| robocopy exit codes | Windows 特殊退出码 |
| findstr exit codes | Windows find 工具 |
| unknown command | 默认语义 |
| extractBaseCommand — basic | `grep "pattern" file``grep` |
| extractBaseCommand — path | `C:\tools\rg.exe``rg` |
| heuristicallyExtractBaseCommand | 模糊匹配 |
---
## 17.3 `src/tools/PowerShellTool/__tests__/destructiveCommandWarning.test.ts`~15 tests
**目标模块**: `src/tools/PowerShellTool/destructiveCommandWarning.ts`110 行)
| 测试用例 | 验证点 |
|---------|--------|
| Remove-Item -Recurse -Force | 危险 |
| Format-Volume | 危险 |
| git reset --hard | 危险 |
| DROP TABLE | 危险 |
| Remove-Item (no -Force) | 安全 |
| Get-ChildItem | 安全 |
| 管道组合 | `rm -rf` + pipe |
| 大小写混合 | `ReMoVe-ItEm` |
---
## 17.4 `src/tools/PowerShellTool/__tests__/gitSafety.test.ts`~12 tests
**目标模块**: `src/tools/PowerShellTool/gitSafety.ts`177 行)
| 测试用例 | 验证点 |
|---------|--------|
| normalizeGitPathArg — forward slash | 规范化 |
| normalizeGitPathArg — backslash | Windows 路径规范化 |
| normalizeGitPathArg — NTFS short name | `GITFI~1``.git` |
| isGitInternalPathPS — .git/config | true |
| isGitInternalPathPS — normal file | false |
| isDotGitPathPS — hidden git dir | true |
| isDotGitPathPS — .gitignore | false |
| bare repo attack | `.git` 路径遍历 |
---
## 17.5 `src/tools/LSPTool/__tests__/formatters.test.ts`~20 tests
**目标模块**: `src/tools/LSPTool/formatters.ts`593 行)
| 测试用例 | 验证点 |
|---------|--------|
| formatGoToDefinitionResult — single | 单个定义 |
| formatGoToDefinitionResult — multiple | 多个定义(分组) |
| formatFindReferencesResult | 引用列表 |
| formatHoverResult — markdown | markdown 内容 |
| formatHoverResult — plaintext | 纯文本 |
| formatDocumentSymbolResult — classes | 类符号 |
| formatDocumentSymbolResult — functions | 函数符号 |
| formatDocumentSymbolResult — nested | 嵌套符号 |
| formatWorkspaceSymbolResult | 工作区符号 |
| formatPrepareCallHierarchyResult | 调用层次 |
| formatIncomingCallsResult | 入调用 |
| formatOutgoingCallsResult | 出调用 |
| empty results | 各函数空结果 |
| groupByFile helper | 文件分组逻辑 |
---
## 17.6 `src/tools/GrepTool/__tests__/utils.test.ts`~10 tests
**目标模块**: `src/tools/GrepTool/GrepTool.ts`577 行)
| 测试用例 | 验证点 |
|---------|--------|
| applyHeadLimit — within limit | 不截断 |
| applyHeadLimit — exceeds limit | 正确截断 |
| applyHeadLimit — offset + limit | 分页逻辑 |
| applyHeadLimit — zero limit | 边界 |
| formatLimitInfo — basic | 格式化输出 |
**Mock**: `mock.module("src/utils/log.ts", ...)` 解锁导入
---
## 17.7 `src/tools/WebFetchTool/__tests__/utils.test.ts`~15 tests
**目标模块**: `src/tools/WebFetchTool/utils.ts`531 行)
| 测试用例 | 验证点 |
|---------|--------|
| validateURL — valid http | 通过 |
| validateURL — valid https | 通过 |
| validateURL — ftp | 拒绝 |
| validateURL — no protocol | 拒绝 |
| validateURL — localhost | 处理 |
| isPermittedRedirect — same host | 允许 |
| isPermittedRedirect — different host | 拒绝 |
| isPermittedRedirect — subdomain | 处理 |
| isRedirectInfo — valid object | true |
| isRedirectInfo — invalid | false |
---
## 17.8 `src/tools/WebFetchTool/__tests__/preapproved.test.ts`~10 tests
**目标模块**: `src/tools/WebFetchTool/preapproved.ts`167 行)
| 测试用例 | 验证点 |
|---------|--------|
| exact hostname match | 通过 |
| subdomain match | 处理 |
| path prefix match | `/docs/api` 匹配 |
| path non-match | `/internal` 不匹配 |
| unknown hostname | false |
| empty pathname | 边界 |
---
## 17.9 `src/tools/FileReadTool/__tests__/utils.test.ts`~15 tests
**目标模块**: `src/tools/FileReadTool/FileReadTool.ts`1184 行)
| 测试用例 | 验证点 |
|---------|--------|
| isBlockedDevicePath — /dev/sda | true |
| isBlockedDevicePath — /dev/null | 处理 |
| isBlockedDevicePath — normal file | false |
| detectSessionFileType — .jsonl | 会话文件类型 |
| detectSessionFileType — unknown | 未知类型 |
| formatFileLines — basic | 行号格式 |
| formatFileLines — empty | 空文件 |
---
## 17.10 `src/tools/AgentTool/__tests__/agentToolUtils.test.ts`~18 tests
**目标模块**: `src/tools/AgentTool/agentToolUtils.ts`688 行)
| 测试用例 | 验证点 |
|---------|--------|
| filterToolsForAgent — builtin only | 只返回内置工具 |
| filterToolsForAgent — exclude async | 排除异步工具 |
| filterToolsForAgent — permission mode | 权限过滤 |
| resolveAgentTools — wildcard | 通配符展开 |
| resolveAgentTools — explicit list | 显式列表 |
| countToolUses — multiple | 消息中工具调用计数 |
| countToolUses — zero | 无工具调用 |
| extractPartialResult — text only | 提取文本 |
| extractPartialResult — mixed | 混合内容 |
| getLastToolUseName — basic | 最后工具名 |
| getLastToolUseName — no tool use | 无工具调用 |
**Mock**: `mock.module("src/bootstrap/state.ts", ...)`, `mock.module("src/utils/log.ts", ...)`
---
## 17.11 `src/tools/LSPTool/__tests__/schemas.test.ts`~5 tests
**目标模块**: `src/tools/LSPTool/schemas.ts`216 行)
| 测试用例 | 验证点 |
|---------|--------|
| isValidLSPOperation — goToDefinition | true |
| isValidLSPOperation — findReferences | true |
| isValidLSPOperation — hover | true |
| isValidLSPOperation — invalid | false |
| isValidLSPOperation — empty string | false |

View File

@@ -1,110 +0,0 @@
# Phase 18 — WEAK 修复 + ACCEPTABLE 加固
> 创建日期2026-04-02
> 预计:+30 tests / 4 files (修改现有)
> 目标:修复所有 WEAK 评分测试文件,消除系统性问题
---
## 18.1 `src/utils/__tests__/format.test.ts` — 断言精确化(+5 tests
**问题**: `formatNumber`/`formatTokens`/`formatRelativeTime` 使用 `toContain`
**修复**: 改为 `toBe` 精确匹配
```diff
- expect(formatNumber(1500000)).toContain("1.5")
+ expect(formatNumber(1500000)).toBe("1.5m")
```
新增测试:
| 测试用例 | 验证点 |
|---------|--------|
| formatNumber — 0 | `"0"` |
| formatNumber — billions | `"1.5b"` |
| formatTokens — thousands | 精确匹配 |
| formatRelativeTime — hours ago | 精确匹配 |
| formatRelativeTime — days ago | 精确匹配 |
---
## 18.2 `src/utils/__tests__/envValidation.test.ts` — Bug 确认(+3 tests
**问题**: `value=1, lowerBound=100` 返回 `status: "valid"` — 函数名暗示有下界检查
**计划**: 先读取源码确认 `defaultValue``lowerBound` 的语义关系,然后:
- 如果是源码 bug → 在测试中注释标记,不修改源码
- 如果是设计意图 → 更新测试描述明确语义
新增测试:
| 测试用例 | 验证点 |
|---------|--------|
| parseFloat truncation | `"50.9"` → 50 |
| whitespace handling | `" 500 "` → 500 |
| very large number | overflow 处理 |
---
## 18.3 `src/utils/permissions/__tests__/PermissionMode.test.ts` — false 路径(+8 tests
**问题**: `isExternalPermissionMode` false 路径从未执行
**修复**: 覆盖所有 5 种 mode 的 true/false 期望
| 测试用例 | 验证点 |
|---------|--------|
| isExternalPermissionMode — plan | false |
| isExternalPermissionMode — auto | false |
| isExternalPermissionMode — default | false |
| permissionModeFromString — all modes | 5 种 mode 全覆盖 |
| permissionModeFromString — invalid | 默认值 |
| permissionModeFromString — case insensitive | 大小写 |
| isPermissionMode — valid strings | true |
| isPermissionMode — invalid strings | false |
---
## 18.4 `src/tools/shared/__tests__/gitOperationTracking.test.ts` — mock analytics+4 tests
**问题**: 未 mock analytics 依赖,测试产生副作用
**修复**: 添加 `mock.module("src/services/analytics/...", ...)`
新增测试:
| 测试用例 | 验证点 |
|---------|--------|
| parseGitCommitId — all GH PR actions | 补齐 6 个 action |
| detectGitOperation — no analytics call | mock 验证 |
| detectGitCommitId — various formats | SHA/短 SHA/HEAD |
| git operation tracking — edge cases | 空输入、畸形输入 |
---
## 排除清单
以下模块 **不纳入测试**,原因合理:
| 模块 | 行数 | 排除原因 |
|------|------|---------|
| `query.ts` | 1732 | 核心循环40+ 依赖,需完整集成环境 |
| `QueryEngine.ts` | 1320 | 编排器30+ 依赖 |
| `utils/hooks.ts` | 5121 | 51 exportsspawn 子进程 |
| `utils/config.ts` | 1817 | 文件系统 + lockfile + 全局状态 |
| `utils/auth.ts` | 2002 | 多 provider 认证,平台特定 |
| `utils/fileHistory.ts` | 1115 | 重 I/O 文件备份 |
| `utils/sessionRestore.ts` | 551 | 恢复状态涉及多个子系统 |
| `utils/ripgrep.ts` | 679 | spawn 子进程 |
| `utils/yaml.ts` | 15 | 两行 wrapper |
| `utils/lockfile.ts` | 43 | trivial wrapper |
| `screens/` / `components/` | — | Ink 渲染测试环境 |
| `bridge/` / `remote/` / `ssh/` | — | 网络层 |
| `daemon/` / `server/` | — | 进程管理 |
---
## 预期成果
| 指标 | Phase 16 后 | Phase 17 后 | Phase 18 后 |
|------|-----------|-----------|-----------|
| 测试数 | ~1417 | ~1567 | ~1597 |
| 文件数 | 76 | 87 | 91 |
| WEAK 文件 | 6 | 4 | **0** |

View File

@@ -1,435 +0,0 @@
# Phase 19 - Batch 1: 零依赖微型 utils
> 预计 ~154 tests / 13 文件 | 全部纯函数,无需 mock
---
## 1. `src/utils/__tests__/semanticBoolean.test.ts` (~8 tests)
**源文件**: `src/utils/semanticBoolean.ts` (30 行)
**依赖**: `zod/v4`
### 测试用例
```typescript
describe("semanticBoolean", () => {
// 基本 Zod 行为
test("parses boolean true to true")
test("parses boolean false to false")
test("parses string 'true' to true")
test("parses string 'false' to false")
// 边界
test("rejects string 'TRUE' (case-sensitive)")
test("rejects string 'FALSE' (case-sensitive)")
test("rejects number 1")
test("rejects null")
test("rejects undefined")
// 自定义 inner schema
test("works with custom inner schema (z.boolean().optional())")
})
```
### Mock 需求
---
## 2. `src/utils/__tests__/semanticNumber.test.ts` (~10 tests)
**源文件**: `src/utils/semanticNumber.ts` (37 行)
**依赖**: `zod/v4`
### 测试用例
```typescript
describe("semanticNumber", () => {
test("parses number 42")
test("parses number 0")
test("parses negative number -5")
test("parses float 3.14")
test("parses string '42' to 42")
test("parses string '-7.5' to -7.5")
test("rejects string 'abc'")
test("rejects empty string ''")
test("rejects null")
test("rejects boolean true")
test("works with custom inner schema (z.number().int().min(0))")
})
```
### Mock 需求
---
## 3. `src/utils/__tests__/lazySchema.test.ts` (~6 tests)
**源文件**: `src/utils/lazySchema.ts` (9 行)
**依赖**: 无
### 测试用例
```typescript
describe("lazySchema", () => {
test("returns a function")
test("calls factory on first invocation")
test("returns cached result on subsequent invocations")
test("factory is called only once (call count verification)")
test("works with different return types")
test("each call to lazySchema returns independent cache")
})
```
### Mock 需求
---
## 4. `src/utils/__tests__/withResolvers.test.ts` (~8 tests)
**源文件**: `src/utils/withResolvers.ts` (14 行)
**依赖**: 无
### 测试用例
```typescript
describe("withResolvers", () => {
test("returns object with promise, resolve, reject")
test("promise resolves when resolve is called")
test("promise rejects when reject is called")
test("resolve passes value through")
test("reject passes error through")
test("promise is instanceof Promise")
test("works with generic type parameter")
test("resolve/reject can be called asynchronously")
})
```
### Mock 需求
---
## 5. `src/utils/__tests__/userPromptKeywords.test.ts` (~12 tests)
**源文件**: `src/utils/userPromptKeywords.ts` (28 行)
**依赖**: 无
### 测试用例
```typescript
describe("matchesNegativeKeyword", () => {
test("matches 'wtf'")
test("matches 'shit'")
test("matches 'fucking broken'")
test("does not match normal input like 'fix the bug'")
test("is case-insensitive")
test("matches partial word in sentence")
})
describe("matchesKeepGoingKeyword", () => {
test("matches exact 'continue'")
test("matches 'keep going'")
test("matches 'go on'")
test("does not match 'cont'")
test("does not match empty string")
test("matches within larger sentence 'please continue'")
})
```
### Mock 需求
---
## 6. `src/utils/__tests__/xdg.test.ts` (~15 tests)
**源文件**: `src/utils/xdg.ts` (66 行)
**依赖**: 无(通过 options 参数注入)
### 测试用例
```typescript
describe("getXDGStateHome", () => {
test("returns ~/.local/state by default")
test("respects XDG_STATE_HOME env var")
test("uses custom homedir from options")
})
describe("getXDGCacheHome", () => {
test("returns ~/.cache by default")
test("respects XDG_CACHE_HOME env var")
})
describe("getXDGDataHome", () => {
test("returns ~/.local/share by default")
test("respects XDG_DATA_HOME env var")
})
describe("getUserBinDir", () => {
test("returns ~/.local/bin")
test("uses custom homedir from options")
})
describe("resolveOptions", () => {
test("defaults env to process.env")
test("defaults homedir to os.homedir()")
test("merges partial options")
})
describe("path construction", () => {
test("all paths end with correct subdirectory")
test("respects HOME env via homedir override")
})
```
### Mock 需求
无(通过 options.env 和 options.homedir 注入)
---
## 7. `src/utils/__tests__/horizontalScroll.test.ts` (~20 tests)
**源文件**: `src/utils/horizontalScroll.ts` (138 行)
**依赖**: 无
### 测试用例
```typescript
describe("calculateHorizontalScrollWindow", () => {
// 基本场景
test("all items fit within available width")
test("single item selected within view")
test("selected item at beginning")
test("selected item at end")
test("selected item beyond visible range scrolls right")
test("selected item before visible range scrolls left")
// 箭头指示器
test("showLeftArrow when items hidden on left")
test("showRightArrow when items hidden on right")
test("no arrows when all items visible")
test("both arrows when items hidden on both sides")
// 边界条件
test("empty itemWidths array")
test("single item")
test("available width is 0")
test("item wider than available width")
test("all items same width")
test("varying item widths")
test("firstItemHasSeparator adds separator width to first item")
test("selectedIdx in middle of overflow")
test("scroll snaps to show selected at left edge")
test("scroll snaps to show selected at right edge")
})
```
### Mock 需求
---
## 8. `src/utils/__tests__/generators.test.ts` (~18 tests)
**源文件**: `src/utils/generators.ts` (89 行)
**依赖**: 无
### 测试用例
```typescript
describe("lastX", () => {
test("returns last yielded value")
test("returns only value from single-yield generator")
test("throws on empty generator")
})
describe("returnValue", () => {
test("returns generator return value")
test("returns undefined for void return")
})
describe("toArray", () => {
test("collects all yielded values")
test("returns empty array for empty generator")
test("preserves order")
})
describe("fromArray", () => {
test("yields all array elements")
test("yields nothing for empty array")
})
describe("all", () => {
test("merges multiple generators preserving yield order")
test("respects concurrency cap")
test("handles empty generator array")
test("handles single generator")
test("handles generators of different lengths")
test("yields all values from all generators")
})
```
### Mock 需求
无(用 fromArray 构造测试数据)
---
## 9. `src/utils/__tests__/sequential.test.ts` (~12 tests)
**源文件**: `src/utils/sequential.ts` (57 行)
**依赖**: 无
### 测试用例
```typescript
describe("sequential", () => {
test("wraps async function, returns same result")
test("single call resolves normally")
test("concurrent calls execute sequentially (FIFO order)")
test("preserves arguments correctly")
test("error in first call does not block subsequent calls")
test("preserves rejection reason")
test("multiple args passed correctly")
test("returns different wrapper for each call to sequential")
test("handles rapid concurrent calls")
test("execution order matches call order")
test("works with functions returning different types")
test("wrapper has same arity expectations")
})
```
### Mock 需求
---
## 10. `src/utils/__tests__/fingerprint.test.ts` (~15 tests)
**源文件**: `src/utils/fingerprint.ts` (77 行)
**依赖**: `crypto` (内置)
### 测试用例
```typescript
describe("FINGERPRINT_SALT", () => {
test("has expected value '59cf53e54c78'")
})
describe("extractFirstMessageText", () => {
test("extracts text from first user message")
test("extracts text from single user message with array content")
test("returns empty string when no user messages")
test("skips assistant messages")
test("handles mixed content blocks (text + image)")
})
describe("computeFingerprint", () => {
test("returns deterministic 3-char hex string")
test("same input produces same fingerprint")
test("different message text produces different fingerprint")
test("different version produces different fingerprint")
test("handles short strings (length < 21)")
test("handles empty string")
test("fingerprint is valid hex")
})
describe("computeFingerprintFromMessages", () => {
test("end-to-end: messages -> fingerprint")
})
```
### Mock 需求
需要 `mock.module` 处理 `UserMessage`/`AssistantMessage` 类型依赖(查看实际 import 情况)
---
## 11. `src/utils/__tests__/configConstants.test.ts` (~8 tests)
**源文件**: `src/utils/configConstants.ts` (22 行)
**依赖**: 无
### 测试用例
```typescript
describe("NOTIFICATION_CHANNELS", () => {
test("contains expected channels")
test("is readonly array")
test("includes 'auto', 'iterm2', 'terminal_bell'")
})
describe("EDITOR_MODES", () => {
test("contains 'normal' and 'vim'")
test("has exactly 2 entries")
})
describe("TEAMMATE_MODES", () => {
test("contains 'auto', 'tmux', 'in-process'")
test("has exactly 3 entries")
})
```
### Mock 需求
---
## 12. `src/utils/__tests__/directMemberMessage.test.ts` (~12 tests)
**源文件**: `src/utils/directMemberMessage.ts` (70 行)
**依赖**: 仅类型(可 mock
### 测试用例
```typescript
describe("parseDirectMemberMessage", () => {
test("parses '@agent-name hello world'")
test("parses '@agent-name single-word'")
test("returns null for non-matching input")
test("returns null for empty string")
test("returns null for '@name' without message")
test("handles hyphenated agent names like '@my-agent msg'")
test("handles multiline message content")
test("extracts correct recipientName and message")
})
// sendDirectMemberMessage 需要 mock teamContext/writeToMailbox
describe("sendDirectMemberMessage", () => {
test("returns error when no team context")
test("returns error for unknown recipient")
test("calls writeToMailbox with correct args for valid recipient")
test("returns success for valid message")
})
```
### Mock 需求
`sendDirectMemberMessage` 需要 mock `AppState['teamContext']``WriteToMailboxFn`
---
## 13. `src/utils/__tests__/collapseHookSummaries.test.ts` (~12 tests)
**源文件**: `src/utils/collapseHookSummaries.ts` (60 行)
**依赖**: 仅类型
### 测试用例
```typescript
describe("collapseHookSummaries", () => {
test("returns same messages when no hook summaries")
test("collapses consecutive messages with same hookLabel")
test("does not collapse messages with different hookLabels")
test("aggregates hookCount across collapsed messages")
test("merges hookInfos arrays")
test("merges hookErrors arrays")
test("takes max totalDurationMs")
test("takes any truthy preventContinuation")
test("leaves single hook summary unchanged")
test("handles three consecutive same-label summaries")
test("preserves non-hook messages in between")
test("returns empty array for empty input")
})
```
### Mock 需求
需要构造 `RenderableMessage` mock 对象

View File

@@ -1,287 +0,0 @@
# Phase 19 - Batch 2: 更多 utils + state + commands
> 预计 ~120 tests / 8 文件 | 部分需轻量 mock
---
## 1. `src/utils/__tests__/collapseTeammateShutdowns.test.ts` (~10 tests)
**源文件**: `src/utils/collapseTeammateShutdowns.ts` (56 行)
**依赖**: 仅类型
### 测试用例
```typescript
describe("collapseTeammateShutdowns", () => {
test("returns same messages when no teammate shutdowns")
test("leaves single shutdown message unchanged")
test("collapses consecutive shutdown messages into batch")
test("batch attachment has correct count")
test("does not collapse non-consecutive shutdowns")
test("preserves non-shutdown messages between shutdowns")
test("handles empty array")
test("handles mixed message types")
test("collapses more than 2 consecutive shutdowns")
test("non-teammate task_status messages are not collapsed")
})
```
### Mock 需求
构造 `RenderableMessage` mock 对象(带 `task_status` attachment`status=completed``taskType=in_process_teammate`
---
## 2. `src/utils/__tests__/privacyLevel.test.ts` (~12 tests)
**源文件**: `src/utils/privacyLevel.ts` (56 行)
**依赖**: `process.env`
### 测试用例
```typescript
describe("getPrivacyLevel", () => {
test("returns 'default' when no env vars set")
test("returns 'essential-traffic' when CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC is set")
test("returns 'no-telemetry' when DISABLE_TELEMETRY is set")
test("'essential-traffic' takes priority over 'no-telemetry'")
})
describe("isEssentialTrafficOnly", () => {
test("returns true for 'essential-traffic' level")
test("returns false for 'default' level")
test("returns false for 'no-telemetry' level")
})
describe("isTelemetryDisabled", () => {
test("returns true for 'no-telemetry' level")
test("returns true for 'essential-traffic' level")
test("returns false for 'default' level")
})
describe("getEssentialTrafficOnlyReason", () => {
test("returns env var name when restricted")
test("returns null when unrestricted")
})
```
### Mock 需求
`process.env` 保存/恢复模式(参考现有 `envUtils.test.ts`
---
## 3. `src/utils/__tests__/textHighlighting.test.ts` (~18 tests)
**源文件**: `src/utils/textHighlighting.ts` (167 行)
**依赖**: `@alcalzone/ansi-tokenize`
### 测试用例
```typescript
describe("segmentTextByHighlights", () => {
// 基本
test("returns single segment with no highlights")
test("returns highlighted segment for single highlight")
test("returns two segments for highlight covering middle portion")
test("returns three segments for highlight in the middle")
// 多高亮
test("handles non-overlapping highlights")
test("handles overlapping highlights (priority-based)")
test("handles adjacent highlights")
// 边界
test("highlight starting at 0")
test("highlight ending at text length")
test("highlight covering entire text")
test("empty text with highlights")
test("empty highlights array returns single segment")
// ANSI 处理
test("correctly segments text with ANSI escape codes")
test("handles text with mixed ANSI and highlights")
// 属性
test("preserves highlight color property")
test("preserves highlight priority property")
test("preserves dimColor and inverse flags")
test("highlights with start > end are handled gracefully")
})
```
### Mock 需求
可能需要 mock `@alcalzone/ansi-tokenize`,或直接使用(如果有安装)
---
## 4. `src/utils/__tests__/detectRepository.test.ts` (~15 tests)
**源文件**: `src/utils/detectRepository.ts` (179 行)
**依赖**: git 命令(`getRemoteUrl`
### 重点测试函数
**`parseGitRemote(input: string): ParsedRepository | null`** — 纯正则解析
**`parseGitHubRepository(input: string): string | null`** — 纯函数
### 测试用例
```typescript
describe("parseGitRemote", () => {
// HTTPS
test("parses HTTPS URL: https://github.com/owner/repo.git")
test("parses HTTPS URL without .git suffix")
test("parses HTTPS URL with subdirectory path (only takes first 2 segments)")
// SSH
test("parses SSH URL: git@github.com:owner/repo.git")
test("parses SSH URL without .git suffix")
// ssh://
test("parses ssh:// URL: ssh://git@github.com/owner/repo.git")
// git://
test("parses git:// URL")
// 边界
test("returns null for invalid URL")
test("returns null for empty string")
test("handles GHE hostname")
test("handles port number in URL")
})
describe("parseGitHubRepository", () => {
test("extracts 'owner/repo' from valid remote URL")
test("handles plain 'owner/repo' string input")
test("returns null for non-GitHub host (if restricted)")
test("returns null for invalid input")
test("is case-sensitive for owner/repo")
})
```
### Mock 需求
仅测试 `parseGitRemote``parseGitHubRepository`(纯函数),不需要 mock git
---
## 5. `src/utils/__tests__/markdown.test.ts` (~20 tests)
**源文件**: `src/utils/markdown.ts` (382 行)
**依赖**: `marked`, `cli-highlight`, theme types
### 重点测试函数
**`padAligned(content, displayWidth, targetWidth, align)`** — 纯函数
### 测试用例
```typescript
describe("padAligned", () => {
test("left-aligns: pads with spaces on right")
test("right-aligns: pads with spaces on left")
test("center-aligns: pads with spaces on both sides")
test("no padding when displayWidth equals targetWidth")
test("handles content wider than targetWidth")
test("null/undefined align defaults to left")
test("handles empty string content")
test("handles zero displayWidth")
test("handles zero targetWidth")
test("center alignment with odd padding distribution")
})
```
注意:`numberToLetter`/`numberToRoman`/`getListNumber` 是私有函数,除非从模块导出否则无法直接测试。如果确实私有,则通过 `applyMarkdown` 间接测试列表渲染:
```typescript
describe("list numbering (via applyMarkdown)", () => {
test("numbered list renders with digits")
test("nested ordered list uses letters (a, b, c)")
test("deep nested list uses roman numerals")
test("unordered list uses bullet markers")
})
```
### Mock 需求
`padAligned` 无需 mock。`applyMarkdown` 可能需要 mock theme 依赖。
---
## 6. `src/state/__tests__/store.test.ts` (~15 tests)
**源文件**: `src/state/store.ts` (35 行)
**依赖**: 无
### 测试用例
```typescript
describe("createStore", () => {
test("returns object with getState, setState, subscribe")
test("getState returns initial state")
test("setState updates state via updater function")
test("setState does not notify when state unchanged (Object.is)")
test("setState notifies subscribers on change")
test("subscribe returns unsubscribe function")
test("unsubscribe stops notifications")
test("multiple subscribers all get notified")
test("onChange callback is called on state change")
test("onChange is not called when state unchanged")
test("works with complex state objects")
test("works with primitive state")
test("updater receives previous state")
test("sequential setState calls produce final state")
test("subscriber called after all state changes in synchronous batch")
})
```
### Mock 需求
---
## 7. `src/commands/plugin/__tests__/parseArgs.test.ts` (~18 tests)
**源文件**: `src/commands/plugin/parseArgs.ts` (104 行)
**依赖**: 无
### 测试用例
```typescript
describe("parsePluginArgs", () => {
// 无参数
test("returns { type: 'menu' } for undefined")
test("returns { type: 'menu' } for empty string")
test("returns { type: 'menu' } for whitespace only")
// help
test("returns { type: 'help' } for 'help'")
// install
test("parses 'install my-plugin' -> { type: 'install', name: 'my-plugin' }")
test("parses 'install my-plugin@github' with marketplace")
test("parses 'install https://github.com/...' as URL marketplace")
// uninstall
test("returns { type: 'uninstall', name: '...' }")
// enable/disable
test("returns { type: 'enable', name: '...' }")
test("returns { type: 'disable', name: '...' }")
// validate
test("returns { type: 'validate', name: '...' }")
// manage
test("returns { type: 'manage' }")
// marketplace 子命令
test("parses 'marketplace add ...'")
test("parses 'marketplace remove ...'")
test("parses 'marketplace list'")
// 边界
test("handles extra whitespace")
test("handles unknown subcommand gracefully")
})
```
### Mock 需求

View File

@@ -1,258 +0,0 @@
# Phase 19 - Batch 3: Tool 子模块纯逻辑
> 预计 ~113 tests / 6 文件 | 采用 `mock.module()` + `await import()` 模式
---
## 1. `src/tools/GrepTool/__tests__/headLimit.test.ts` (~20 tests)
**源文件**: `src/tools/GrepTool/GrepTool.ts` (578 行)
**目标函数**: `applyHeadLimit<T>`, `formatLimitInfo` (非导出,需确认可测性)
### 测试策略
如果函数是文件内导出的,直接 `await import()` 获取。如果私有,则通过 GrepTool 的输出间接测试,或提取到独立文件。
### 测试用例
```typescript
describe("applyHeadLimit", () => {
test("returns full array when limit is undefined (default 250)")
test("applies limit correctly: limits to N items")
test("limit=0 means no limit (returns all)")
test("applies offset correctly")
test("offset + limit combined")
test("offset beyond array length returns empty")
test("returns appliedLimit when truncation occurred")
test("returns appliedLimit=undefined when no truncation")
test("limit larger than array returns all items with appliedLimit=undefined")
test("empty array returns empty with appliedLimit=undefined")
test("offset=0 is default")
test("negative limit behavior")
})
describe("formatLimitInfo", () => {
test("formats 'limit: N, offset: M' when both present")
test("formats 'limit: N' when only limit")
test("formats 'offset: M' when only offset")
test("returns empty string when both undefined")
test("handles limit=0 (no limit, should not appear)")
})
```
### Mock 需求
需 mock 重依赖链(`log`, `slowOperations` 等),通过 `mock.module()` + `await import()` 只取目标函数
---
## 2. `src/tools/MCPTool/__tests__/classifyForCollapse.test.ts` (~25 tests)
**源文件**: `src/tools/MCPTool/classifyForCollapse.ts` (605 行)
**目标函数**: `classifyMcpToolForCollapse`, `normalize`
### 测试用例
```typescript
describe("normalize", () => {
test("leaves snake_case unchanged: 'search_issues'")
test("converts camelCase to snake_case: 'searchIssues' -> 'search_issues'")
test("converts kebab-case to snake_case: 'search-issues' -> 'search_issues'")
test("handles mixed: 'searchIssuesByStatus' -> 'search_issues_by_status'")
test("handles already lowercase single word")
test("handles empty string")
test("handles PascalCase: 'SearchIssues' -> 'search_issues'")
})
describe("classifyMcpToolForCollapse", () => {
// 搜索工具
test("classifies Slack search_messages as search")
test("classifies GitHub search_code as search")
test("classifies Linear search_issues as search")
test("classifies Datadog search_logs as search")
test("classifies Notion search as search")
// 读取工具
test("classifies Slack get_message as read")
test("classifies GitHub get_file_contents as read")
test("classifies Linear get_issue as read")
test("classifies Filesystem read_file as read")
// 双重分类
test("some tools are both search and read")
test("some tools are neither search nor read")
// 未知工具
test("unknown tool returns { isSearch: false, isRead: false }")
test("tool name with camelCase variant still matches")
test("tool name with kebab-case variant still matches")
// server name 不影响分类
test("server name parameter is accepted but unused in current logic")
// 边界
test("empty tool name returns false/false")
test("case sensitivity check (should match after normalize)")
test("handles tool names with numbers")
})
```
### Mock 需求
文件自包含(仅内部 Set + normalize 函数),需确认 `normalize` 是否导出
---
## 3. `src/tools/FileReadTool/__tests__/blockedPaths.test.ts` (~18 tests)
**源文件**: `src/tools/FileReadTool/FileReadTool.ts` (1184 行)
**目标函数**: `isBlockedDevicePath`, `getAlternateScreenshotPath`
### 测试用例
```typescript
describe("isBlockedDevicePath", () => {
// 阻止的设备
test("blocks /dev/zero")
test("blocks /dev/random")
test("blocks /dev/urandom")
test("blocks /dev/full")
test("blocks /dev/stdin")
test("blocks /dev/tty")
test("blocks /dev/console")
test("blocks /dev/stdout")
test("blocks /dev/stderr")
test("blocks /dev/fd/0")
test("blocks /dev/fd/1")
test("blocks /dev/fd/2")
// 阻止 /proc
test("blocks /proc/self/fd/0")
test("blocks /proc/123/fd/2")
// 允许的路径
test("allows /dev/null")
test("allows regular file paths")
test("allows /home/user/file.txt")
})
describe("getAlternateScreenshotPath", () => {
test("returns undefined for path without AM/PM")
test("returns alternate path for macOS screenshot with regular space before AM")
test("returns alternate path for macOS screenshot with U+202F before PM")
test("handles path without time component")
test("handles multiple AM/PM occurrences")
test("returns undefined when no space variant difference")
})
```
### Mock 需求
需 mock 重依赖链,通过 `await import()` 获取函数
---
## 4. `src/tools/AgentTool/__tests__/agentDisplay.test.ts` (~15 tests)
**源文件**: `src/tools/AgentTool/agentDisplay.ts` (105 行)
**目标函数**: `resolveAgentOverrides`, `compareAgentsByName`
### 测试用例
```typescript
describe("resolveAgentOverrides", () => {
test("marks no overrides when all agents active")
test("marks inactive agent as overridden")
test("overriddenBy shows the overriding agent source")
test("deduplicates agents by (agentType, source)")
test("preserves agent definition properties")
test("handles empty arrays")
test("handles agent from git worktree (duplicate detection)")
})
describe("compareAgentsByName", () => {
test("sorts alphabetically ascending")
test("returns negative when a.name < b.name")
test("returns positive when a.name > b.name")
test("returns 0 for same name")
test("is case-sensitive")
})
describe("AGENT_SOURCE_GROUPS", () => {
test("contains expected source groups in order")
test("has unique labels")
})
```
### Mock 需求
需 mock `AgentDefinition`, `AgentSource` 类型依赖
---
## 5. `src/tools/AgentTool/__tests__/agentToolUtils.test.ts` (~20 tests)
**源文件**: `src/tools/AgentTool/agentToolUtils.ts` (688 行)
**目标函数**: `countToolUses`, `getLastToolUseName`, `extractPartialResult`
### 测试用例
```typescript
describe("countToolUses", () => {
test("counts tool_use blocks in messages")
test("returns 0 for messages without tool_use")
test("returns 0 for empty array")
test("counts multiple tool_use blocks across messages")
test("counts tool_use in single message with multiple blocks")
})
describe("getLastToolUseName", () => {
test("returns last tool name from assistant message")
test("returns undefined for message without tool_use")
test("returns the last tool when multiple tool_uses present")
test("handles message with non-array content")
})
describe("extractPartialResult", () => {
test("extracts text from last assistant message")
test("returns undefined for messages without assistant content")
test("handles interrupted agent with partial text")
test("returns undefined for empty messages")
test("concatenates multiple text blocks")
test("skips non-text content blocks")
})
```
### Mock 需求
需 mock 消息类型依赖
---
## 6. `src/tools/SkillTool/__tests__/skillSafety.test.ts` (~15 tests)
**源文件**: `src/tools/SkillTool/SkillTool.ts` (1110 行)
**目标函数**: `skillHasOnlySafeProperties`, `extractUrlScheme`
### 测试用例
```typescript
describe("skillHasOnlySafeProperties", () => {
test("returns true for command with only safe properties")
test("returns true for command with undefined extra properties")
test("returns false for command with unsafe meaningful property")
test("returns true for command with null extra properties")
test("returns true for command with empty array extra property")
test("returns true for command with empty object extra property")
test("returns false for command with non-empty unsafe array")
test("returns false for command with non-empty unsafe object")
test("returns true for empty command object")
})
describe("extractUrlScheme", () => {
test("extracts 'gs' from 'gs://bucket/path'")
test("extracts 'https' from 'https://example.com'")
test("extracts 'http' from 'http://example.com'")
test("extracts 's3' from 's3://bucket/path'")
test("defaults to 'gs' for unknown scheme")
test("defaults to 'gs' for path without scheme")
test("defaults to 'gs' for empty string")
})
```
### Mock 需求
需 mock 重依赖链,`await import()` 获取函数

View File

@@ -1,215 +0,0 @@
# Phase 19 - Batch 4: Services 纯逻辑
> 预计 ~84 tests / 5 文件 | 部分需轻量 mock
---
## 1. `src/services/compact/__tests__/grouping.test.ts` (~15 tests)
**源文件**: `src/services/compact/grouping.ts` (64 行)
**目标函数**: `groupMessagesByApiRound`
### 测试用例
```typescript
describe("groupMessagesByApiRound", () => {
test("returns single group for single API round")
test("splits at new assistant message ID")
test("keeps tool_result messages with their parent assistant message")
test("handles streaming chunks (same assistant ID stays grouped)")
test("returns empty array for empty input")
test("handles all user messages (no assistant)")
test("handles alternating assistant IDs")
test("three API rounds produce three groups")
test("user messages before first assistant go in first group")
test("consecutive user messages stay in same group")
test("does not produce empty groups")
test("handles single message")
test("preserves message order within groups")
test("handles system messages")
test("tool_result after assistant stays in same round")
})
```
### Mock 需求
需构造 `Message` mock 对象type: 'user'/'assistant', message: { id, content }
---
## 2. `src/services/compact/__tests__/stripMessages.test.ts` (~20 tests)
**源文件**: `src/services/compact/compact.ts` (1709 行)
**目标函数**: `stripImagesFromMessages`, `collectReadToolFilePaths` (私有)
### 测试用例
```typescript
describe("stripImagesFromMessages", () => {
// user 消息处理
test("replaces image block with [image] text")
test("replaces document block with [document] text")
test("preserves text blocks unchanged")
test("handles multiple image/document blocks in single message")
test("returns original message when no media blocks")
// tool_result 内嵌套
test("replaces image inside tool_result content")
test("replaces document inside tool_result content")
test("preserves non-media tool_result content")
// 非用户消息
test("passes through assistant messages unchanged")
test("passes through system messages unchanged")
// 边界
test("handles empty message array")
test("handles string content (non-array) in user message")
test("does not mutate original messages")
})
describe("collectReadToolFilePaths", () => {
// 注意:这是私有函数,可能需要通过 stripImagesFromMessages 或其他导出间接测试
// 如果不可直接测试,则跳过或通过集成测试覆盖
test("collects file_path from Read tool_use blocks")
test("skips tool_use with FILE_UNCHANGED_STUB result")
test("returns empty set for messages without Read tool_use")
test("handles multiple Read calls across messages")
test("normalizes paths via expandPath")
})
```
### Mock 需求
需 mock `expandPath`(如果 collectReadToolFilePaths 要测)
需 mock `log`, `slowOperations` 等重依赖
构造 `Message` mock 对象
---
## 3. `src/services/compact/__tests__/prompt.test.ts` (~12 tests)
**源文件**: `src/services/compact/prompt.ts` (375 行)
**目标函数**: `formatCompactSummary`
### 测试用例
```typescript
describe("formatCompactSummary", () => {
test("strips <analysis>...</analysis> block")
test("replaces <summary>...</summary> with 'Summary:\\n' prefix")
test("handles analysis + summary together")
test("handles summary without analysis")
test("handles analysis without summary")
test("collapses multiple newlines to double")
test("trims leading/trailing whitespace")
test("handles empty string")
test("handles plain text without tags")
test("handles multiline analysis content")
test("preserves content between analysis and summary")
test("handles nested-like tags gracefully")
})
```
### Mock 需求
需 mock 重依赖链(`log`, feature flags 等)
`formatCompactSummary` 是纯字符串处理,如果 import 链不太重则无需复杂 mock
---
## 4. `src/services/mcp/__tests__/channelPermissions.test.ts` (~25 tests)
**源文件**: `src/services/mcp/channelPermissions.ts` (241 行)
**目标函数**: `hashToId`, `shortRequestId`, `truncateForPreview`, `filterPermissionRelayClients`
### 测试用例
```typescript
describe("hashToId", () => {
test("returns 5-char string")
test("uses only letters a-z excluding 'l'")
test("is deterministic (same input = same output)")
test("different inputs produce different outputs (with high probability)")
test("handles empty string")
})
describe("shortRequestId", () => {
test("returns 5-char string from tool use ID")
test("is deterministic")
test("avoids profanity substrings (retries with salt)")
test("returns a valid ID even if all retries hit bad words (unlikely)")
})
describe("truncateForPreview", () => {
test("returns JSON string for object input")
test("truncates to <=200 chars when input is long")
test("adds ellipsis or truncation indicator")
test("returns short input unchanged")
test("handles string input")
test("handles null/undefined input")
})
describe("filterPermissionRelayClients", () => {
test("keeps connected clients in allowlist with correct capabilities")
test("filters out disconnected clients")
test("filters out clients not in allowlist")
test("filters out clients missing required capabilities")
test("returns empty array for empty input")
test("type predicate narrows correctly")
})
describe("PERMISSION_REPLY_RE", () => {
test("matches 'y abcde'")
test("matches 'yes abcde'")
test("matches 'n abcde'")
test("matches 'no abcde'")
test("is case-insensitive")
test("does not match without ID")
})
```
### Mock 需求
`hashToId` 可能需要确认导出状态
`filterPermissionRelayClients` 需要 mock 客户端类型
`truncateForPreview` 可能依赖 `jsonStringify`(需 mock `slowOperations`
---
## 5. `src/services/mcp/__tests__/officialRegistry.test.ts` (~12 tests)
**源文件**: `src/services/mcp/officialRegistry.ts` (73 行)
**目标函数**: `normalizeUrl` (私有), `isOfficialMcpUrl`, `resetOfficialMcpUrlsForTesting`
### 测试用例
```typescript
describe("normalizeUrl", () => {
// 注意:如果是私有的,通过 isOfficialMcpUrl 间接测试
test("removes trailing slash")
test("removes query parameters")
test("preserves path")
test("handles URL with port")
test("handles URL with hash fragment")
})
describe("isOfficialMcpUrl", () => {
test("returns false when registry not loaded (initial state)")
test("returns true for URL added to registry")
test("returns false for non-registered URL")
test("uses normalized URL for comparison")
})
describe("resetOfficialMcpUrlsForTesting", () => {
test("clears the cached URLs")
test("allows fresh start after reset")
})
describe("URL normalization + lookup integration", () => {
test("URL with trailing slash matches normalized version")
test("URL with query params matches normalized version")
test("different URLs do not match")
test("case sensitivity check")
})
```
### Mock 需求
需 mock `axios`(避免网络请求)
使用 `resetOfficialMcpUrlsForTesting` 做测试隔离

View File

@@ -1,200 +0,0 @@
# Phase 19 - Batch 5: MCP 配置 + modelCost
> 预计 ~80 tests / 4 文件 | 需中等 mock
---
## 1. `src/services/mcp/__tests__/configUtils.test.ts` (~30 tests)
**源文件**: `src/services/mcp/config.ts` (1580 行)
**目标函数**: `unwrapCcrProxyUrl`, `urlPatternToRegex` (私有), `commandArraysMatch` (私有), `toggleMembership` (私有), `addScopeToServers` (私有), `dedupPluginMcpServers`, `getMcpServerSignature` (如导出)
### 测试策略
私有函数如不可直接测试,通过公开的 `dedupPluginMcpServers` 间接覆盖。导出函数直接测。
### 测试用例
```typescript
describe("unwrapCcrProxyUrl", () => {
test("returns original URL when no CCR proxy markers")
test("extracts mcp_url from CCR proxy URL with /v2/session_ingress/shttp/mcp/")
test("extracts mcp_url from CCR proxy URL with /v2/ccr-sessions/")
test("returns original URL when mcp_url param is missing")
test("handles malformed URL gracefully")
test("handles URL with both proxy marker and mcp_url")
test("preserves non-CCR URLs unchanged")
})
describe("dedupPluginMcpServers", () => {
test("keeps unique plugin servers")
test("suppresses plugin server duplicated by manual config")
test("suppresses plugin server duplicated by earlier plugin")
test("keeps servers with null signature")
test("returns empty for empty inputs")
test("reports suppressed with correct duplicateOf name")
test("handles multiple plugins with same config")
})
describe("toggleMembership (via integration)", () => {
test("adds item when shouldContain=true and not present")
test("removes item when shouldContain=false and present")
test("returns same array when already in desired state")
})
describe("addScopeToServers (via integration)", () => {
test("adds scope to each server config")
test("returns empty object for undefined input")
test("returns empty object for empty input")
test("preserves all original config properties")
})
describe("urlPatternToRegex (via integration)", () => {
test("matches exact URL")
test("matches wildcard pattern *.example.com")
test("matches multiple wildcards")
test("does not match non-matching URL")
test("escapes regex special characters in pattern")
})
describe("commandArraysMatch (via integration)", () => {
test("returns true for identical arrays")
test("returns false for different lengths")
test("returns false for same length different elements")
test("returns true for empty arrays")
})
```
### Mock 需求
需 mock `feature()` (bun:bundle), `jsonStringify`, `safeParseJSON`, `log`
通过 `mock.module()` + `await import()` 解锁
---
## 2. `src/services/mcp/__tests__/filterUtils.test.ts` (~20 tests)
**源文件**: `src/services/mcp/utils.ts` (576 行)
**目标函数**: `filterToolsByServer`, `hashMcpConfig`, `isToolFromMcpServer`, `isMcpTool`, `parseHeaders`
### 测试用例
```typescript
describe("filterToolsByServer", () => {
test("filters tools matching server name prefix")
test("returns empty for no matching tools")
test("handles empty tools array")
test("normalizes server name for matching")
})
describe("hashMcpConfig", () => {
test("returns 16-char hex string")
test("is deterministic")
test("excludes scope from hash")
test("different configs produce different hashes")
test("key order does not affect hash (sorted)")
})
describe("isToolFromMcpServer", () => {
test("returns true when tool belongs to specified server")
test("returns false for different server")
test("returns false for non-MCP tool name")
test("handles empty tool name")
})
describe("isMcpTool", () => {
test("returns true for tool name starting with 'mcp__'")
test("returns true when tool.isMcp is true")
test("returns false for regular tool")
test("returns false when neither condition met")
})
describe("parseHeaders", () => {
test("parses 'Key: Value' format")
test("parses multiple headers")
test("trims whitespace around key and value")
test("throws on missing colon")
test("throws on empty key")
test("handles value with colons (like URLs)")
test("returns empty object for empty array")
test("handles duplicate keys (last wins)")
})
```
### Mock 需求
需 mock `normalizeNameForMCP`, `mcpInfoFromString`, `jsonStringify`, `createHash`
`parseHeaders` 是最独立的,可能不需要太多 mock
---
## 3. `src/services/mcp/__tests__/channelNotification.test.ts` (~15 tests)
**源文件**: `src/services/mcp/channelNotification.ts` (317 行)
**目标函数**: `wrapChannelMessage`, `findChannelEntry`
### 测试用例
```typescript
describe("wrapChannelMessage", () => {
test("wraps content in <channel> tag with source attribute")
test("escapes server name in attribute")
test("includes meta attributes when provided")
test("escapes meta values via escapeXmlAttr")
test("filters out meta keys not matching SAFE_META_KEY pattern")
test("handles empty meta")
test("handles content with special characters")
test("formats with newlines between tags and content")
})
describe("findChannelEntry", () => {
test("finds server entry by exact name match")
test("finds plugin entry by matching second segment")
test("returns undefined for no match")
test("handles empty channels array")
test("handles server name without colon")
test("handles 'plugin:name' format correctly")
test("prefers exact match over partial match")
})
```
### Mock 需求
需 mock `escapeXmlAttr`(来自 xml.ts已有测试或直接使用
`CHANNEL_TAG` 常量需确认导出
---
## 4. `src/utils/__tests__/modelCost.test.ts` (~15 tests)
**源文件**: `src/utils/modelCost.ts` (232 行)
**目标函数**: `formatModelPricing`, `COST_TIER_*` 常量
### 测试用例
```typescript
describe("COST_TIER constants", () => {
test("COST_TIER_3_15 has inputTokens=3, outputTokens=15")
test("COST_TIER_15_75 has inputTokens=15, outputTokens=75")
test("COST_TIER_5_25 has inputTokens=5, outputTokens=25")
test("COST_TIER_30_150 has inputTokens=30, outputTokens=150")
test("COST_HAIKU_35 has inputTokens=0.8, outputTokens=4")
test("COST_HAIKU_45 has inputTokens=1, outputTokens=5")
})
describe("formatModelPricing", () => {
test("formats integer prices without decimals: '$3/$15 per Mtok'")
test("formats float prices with 2 decimals: '$0.80/$4.00 per Mtok'")
test("formats mixed: '$5/$25 per Mtok'")
test("formats large prices: '$30/$150 per Mtok'")
test("formats $1/$5 correctly (integer but small)")
test("handles zero prices: '$0/$0 per Mtok'")
})
describe("MODEL_COSTS", () => {
test("maps known model names to cost tiers")
test("contains entries for claude-sonnet-4-6")
test("contains entries for claude-opus-4-6")
test("contains entries for claude-haiku-4-5")
})
```
### Mock 需求
需 mock `log`, `slowOperations` 等重依赖modelCost.ts 通常 import 链较重)
`formatModelPricing``COST_TIER_*` 是纯数据/纯函数mock 成功后直接测