mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
docs: 清理垃圾文档
This commit is contained in:
@@ -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') — 默认值应为 false(fail-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 为空 Map,rules 为空对象
|
||||
- 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') — 合并去重逻辑
|
||||
@@ -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') — `<` → `<`
|
||||
- test('escapes greater-than') — `>` → `>`
|
||||
- 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>` → `<a & b>`
|
||||
|
||||
#### describe('escapeXmlAttr')
|
||||
|
||||
- test('escapes all xml chars plus quotes') — `"` → `"`, `'` → `'`
|
||||
- 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"` → `"0123"`
|
||||
- test('leaves half-width digits unchanged') — `"0123"` → `"0123"`
|
||||
- test('mixed content') — `"port 8080"` → `"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` 避免测试输出噪音 |
|
||||
@@ -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') — 注入后重新构建上下文
|
||||
@@ -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
|
||||
@@ -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 检查 |
|
||||
@@ -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')
|
||||
@@ -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 * * * *'` → null(minute max=59)
|
||||
- test('returns null for invalid step') — `'*/0 * * * *'` → null(step=0)
|
||||
- test('returns null for reversed range') — `'10-5 * * * *'` → null(lo>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 相关测试需注意运行环境
|
||||
@@ -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'`
|
||||
|
||||
#### 代理 URL(CCR 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 端到端验证
|
||||
@@ -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 # userSettings(mock 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')
|
||||
@@ -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 环境永远返回 true,false 路径从未执行;mode 覆盖不完整。
|
||||
|
||||
### 修改清单
|
||||
|
||||
#### 补全 mode 覆盖
|
||||
|
||||
| 函数 | 缺失的 mode |
|
||||
|------|-------------|
|
||||
| `permissionModeTitle` | `bypassPermissions`, `dontAsk` |
|
||||
| `permissionModeShortTitle` | `dontAsk`, `acceptEdits` |
|
||||
| `getModeColor` | `dontAsk`, `acceptEdits`, `plan` |
|
||||
| `permissionModeFromString` | `acceptEdits`, `bypassPermissions` |
|
||||
| `toExternalPermissionMode` | `acceptEdits`, `bypassPermissions` |
|
||||
|
||||
#### 修复 isExternalPermissionMode
|
||||
|
||||
```typescript
|
||||
// 当前:只测了非 ant 环境(永远 true)
|
||||
// 需要新增 ant 环境测试
|
||||
describe("when USER_TYPE is 'ant'", () => {
|
||||
beforeEach(() => {
|
||||
process.env.USER_TYPE = "ant";
|
||||
});
|
||||
afterEach(() => {
|
||||
delete process.env.USER_TYPE;
|
||||
});
|
||||
|
||||
test("returns false for 'auto' in ant context", () => {
|
||||
expect(isExternalPermissionMode("auto")).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false for 'bubble' in ant context", () => {
|
||||
expect(isExternalPermissionMode("bubble")).toBe(false);
|
||||
});
|
||||
|
||||
test("returns true for non-ant modes in ant context", () => {
|
||||
expect(isExternalPermissionMode("plan")).toBe(true);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### 新增:permissionModeSchema
|
||||
|
||||
| 用例 | 输入 | 期望 |
|
||||
|------|------|------|
|
||||
| 有效 mode | `'plan'` | `success: true` |
|
||||
| 无效 mode | `'invalid'` | `success: false` |
|
||||
|
||||
---
|
||||
|
||||
## 10.4 `src/utils/permissions/__tests__/dangerousPatterns.test.ts`
|
||||
|
||||
**问题**:纯数据 smoke test,无行为验证。
|
||||
|
||||
### 修改清单
|
||||
|
||||
#### 新增:重复值检查
|
||||
|
||||
```typescript
|
||||
test("CROSS_PLATFORM_CODE_EXEC has no duplicates", () => {
|
||||
const set = new Set(CROSS_PLATFORM_CODE_EXEC);
|
||||
expect(set.size).toBe(CROSS_PLATFORM_CODE_EXEC.length);
|
||||
});
|
||||
|
||||
test("DANGEROUS_BASH_PATTERNS has no duplicates", () => {
|
||||
const set = new Set(DANGEROUS_BASH_PATTERNS);
|
||||
expect(set.size).toBe(DANGEROUS_BASH_PATTERNS.length);
|
||||
});
|
||||
```
|
||||
|
||||
#### 新增:全量成员断言(用 Set 确保精确)
|
||||
|
||||
```typescript
|
||||
test("CROSS_PLATFORM_CODE_EXEC contains expected interpreters", () => {
|
||||
const expected = ["node", "python", "python3", "ruby", "perl", "php",
|
||||
"bun", "deno", "npx", "tsx"];
|
||||
const set = new Set(CROSS_PLATFORM_CODE_EXEC);
|
||||
for (const entry of expected) {
|
||||
expect(set.has(entry)).toBe(true);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### 新增:空字符串不匹配
|
||||
|
||||
```typescript
|
||||
test("empty string does not match any pattern", () => {
|
||||
for (const pattern of DANGEROUS_BASH_PATTERNS) {
|
||||
expect("".startsWith(pattern)).toBe(false);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10.5 `src/utils/__tests__/zodToJsonSchema.test.ts`
|
||||
|
||||
**问题**:object 属性仅 `toBeDefined` 未验证类型结构;optional 字段未验证 absence。
|
||||
|
||||
### 修改清单
|
||||
|
||||
#### 修复 object schema 测试
|
||||
|
||||
```typescript
|
||||
// 当前(弱)
|
||||
expect(schema.properties!.name).toBeDefined();
|
||||
expect(schema.properties!.age).toBeDefined();
|
||||
|
||||
// 修复为
|
||||
expect(schema.properties!.name).toEqual({ type: "string" });
|
||||
expect(schema.properties!.age).toEqual({ type: "number" });
|
||||
```
|
||||
|
||||
#### 修复 optional 字段测试
|
||||
|
||||
```typescript
|
||||
test("optional field is not in required array", () => {
|
||||
const schema = zodToJsonSchema(z.object({
|
||||
required: z.string(),
|
||||
optional: z.string().optional(),
|
||||
}));
|
||||
expect(schema.required).toEqual(["required"]);
|
||||
expect(schema.required).not.toContain("optional");
|
||||
});
|
||||
```
|
||||
|
||||
#### 新增:缺失的 schema 类型
|
||||
|
||||
| 用例 | 输入 | 期望 |
|
||||
|------|------|------|
|
||||
| `z.literal("foo")` | `z.literal("foo")` | `{ const: "foo" }` |
|
||||
| `z.null()` | `z.null()` | `{ type: "null" }` |
|
||||
| `z.union()` | `z.union([z.string(), z.number()])` | `{ anyOf: [...] }` |
|
||||
| `z.record()` | `z.record(z.string(), z.number())` | `{ type: "object", additionalProperties: { type: "number" } }` |
|
||||
| `z.tuple()` | `z.tuple([z.string(), z.number()])` | `{ type: "array", items: [...], additionalItems: false }` |
|
||||
| 嵌套 object | `z.object({ a: z.object({ b: z.string() }) })` | 验证嵌套属性结构 |
|
||||
|
||||
---
|
||||
|
||||
## 10.6 `src/utils/__tests__/envValidation.test.ts`
|
||||
|
||||
**问题**:`validateBoundedIntEnvVar` lower bound=100 时 value=1 返回 `status: "valid"`,疑似源码 bug。
|
||||
|
||||
### 修改清单
|
||||
|
||||
#### 验证 lower bound 行为
|
||||
|
||||
```typescript
|
||||
// 当前测试
|
||||
test("value of 1 with lower bound 100", () => {
|
||||
const result = validateBoundedIntEnvVar("1", { defaultValue: 100, upperLimit: 1000, lowerLimit: 100 });
|
||||
// 如果源码有 bug,这里应该暴露
|
||||
expect(result.effective).toBeGreaterThanOrEqual(100);
|
||||
expect(result.status).toBe(result.effective !== 100 ? "capped" : "valid");
|
||||
});
|
||||
```
|
||||
|
||||
#### 新增边界用例
|
||||
|
||||
| 用例 | value | lowerLimit | 期望 |
|
||||
|------|-------|------------|------|
|
||||
| 低于 lower bound | `"50"` | 100 | `effective: 100, status: "capped"` |
|
||||
| 等于 lower bound | `"100"` | 100 | `effective: 100, status: "valid"` |
|
||||
| 浮点截断 | `"50.7"` | 100 | `effective: 100`(parseInt 截断后 cap) |
|
||||
| 空白字符 | `" 500 "` | 1 | `effective: 500, status: "valid"` |
|
||||
| defaultValue 为 0 | `"0"` | 0 | 需确认 `parsed <= 0` 逻辑 |
|
||||
|
||||
> **行动**:先确认 `validateBoundedIntEnvVar` 源码中 lower bound 的实际执行路径。如果确实不生效,需先修源码再补测试。
|
||||
|
||||
---
|
||||
|
||||
## 10.7 `src/utils/__tests__/file.test.ts`
|
||||
|
||||
**问题**:`addLineNumbers` 仅 `toContain`,未验证完整格式。
|
||||
|
||||
### 修改清单
|
||||
|
||||
#### 修复 addLineNumbers 断言
|
||||
|
||||
```typescript
|
||||
// 当前(弱)
|
||||
expect(result).toContain("1");
|
||||
expect(result).toContain("hello");
|
||||
|
||||
// 修复为(需确定 isCompactLinePrefixEnabled 行为)
|
||||
// 假设 compact=false,格式为 " 1→hello"
|
||||
test("formats single line with tab prefix", () => {
|
||||
// 先确认环境,如果 compact 模式不确定,用正则
|
||||
expect(result).toMatch(/^\s*\d+[→\t]hello$/m);
|
||||
});
|
||||
```
|
||||
|
||||
#### 新增:stripLineNumberPrefix 边界
|
||||
|
||||
| 用例 | 输入 | 期望 |
|
||||
|------|------|------|
|
||||
| 纯数字行 | `"123"` | `""` |
|
||||
| 无内容前缀 | `"→"` | `""` |
|
||||
| compact 格式 `"1\thello"` | `"1\thello"` | `"hello"` |
|
||||
|
||||
#### 新增:pathsEqual 边界
|
||||
|
||||
| 用例 | a | b | 期望 |
|
||||
|------|---|---|------|
|
||||
| 尾部斜杠差异 | `"/a/b"` | `"/a/b/"` | `false` |
|
||||
| `..` 段 | `"/a/../b"` | `"/b"` | 视实现而定 |
|
||||
|
||||
---
|
||||
|
||||
## 10.8 `src/utils/__tests__/notebook.test.ts`
|
||||
|
||||
**问题**:`mapNotebookCellsToToolResult` 内容检查用 `toContain`,未验证 XML 格式。
|
||||
|
||||
### 修改清单
|
||||
|
||||
#### 修复 content 断言
|
||||
|
||||
```typescript
|
||||
// 当前(弱)
|
||||
expect(result).toContain("cell-0");
|
||||
expect(result).toContain("print('hello')");
|
||||
|
||||
// 修复为
|
||||
expect(result).toContain('<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 并更新测试)
|
||||
@@ -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` 用于精确值检查的场景
|
||||
@@ -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 | 增大 margin:TTL=5ms, wait 50ms |
|
||||
|
||||
> 仅在 CI 出现 flaky 时执行此加固。
|
||||
|
||||
---
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] `gitOperationTracking.test.ts` 无分析副作用(可通过在 mock 中增加 `expect(logEvent).toHaveBeenCalledTimes(N)` 验证)
|
||||
- [ ] `PermissionMode.test.ts` 的 `isExternalPermissionMode` 覆盖 true + false 分支
|
||||
- [ ] `providers.test.ts` 的 `originalEnv` 死代码已删除
|
||||
- [ ] 所有修改 env 的测试文件恢复完整
|
||||
- [ ] `bun test` 全部通过
|
||||
@@ -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` 全部通过
|
||||
@@ -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` 全部通过
|
||||
@@ -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` 全部通过
|
||||
@@ -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` 实现确定性测试
|
||||
@@ -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 |
|
||||
@@ -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 exports,spawn 子进程 |
|
||||
| `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** |
|
||||
@@ -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 对象
|
||||
@@ -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 需求
|
||||
无
|
||||
@@ -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()` 获取函数
|
||||
@@ -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` 做测试隔离
|
||||
@@ -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 成功后直接测
|
||||
Reference in New Issue
Block a user