17 KiB
17 KiB
工具函数(纯函数)测试计划
概述
覆盖 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 避免测试输出噪音 |