|
| 1 | +# Plan 10 — 修复 WEAK 评分测试文件 |
| 2 | + |
| 3 | +> 优先级:高 | 8 个文件 | 预估新增/修改 ~60 个测试用例 |
| 4 | +
|
| 5 | +本计划修复 testing-spec.md 中评定为 WEAK 的 8 个测试文件的断言缺陷和覆盖缺口。 |
| 6 | + |
| 7 | +--- |
| 8 | + |
| 9 | +## 10.1 `src/utils/__tests__/format.test.ts` |
| 10 | + |
| 11 | +**问题**:`formatNumber`、`formatTokens`、`formatRelativeTime` 使用 `toContain` 代替精确匹配,无法检测格式回归。 |
| 12 | + |
| 13 | +### 修改清单 |
| 14 | + |
| 15 | +#### formatNumber — toContain → toBe |
| 16 | + |
| 17 | +```typescript |
| 18 | +// 当前(弱) |
| 19 | +expect(formatNumber(1321)).toContain("k"); |
| 20 | +expect(formatNumber(1500000)).toContain("m"); |
| 21 | + |
| 22 | +// 修复为 |
| 23 | +expect(formatNumber(1321)).toBe("1.3k"); |
| 24 | +expect(formatNumber(1500000)).toBe("1.5m"); |
| 25 | +``` |
| 26 | + |
| 27 | +> 注意:`Intl.NumberFormat` 输出可能因 locale 不同。若 CI locale 不一致,改用 `toMatch(/^\d+(\.\d)?[km]$/)` 正则匹配。 |
| 28 | +
|
| 29 | +#### formatTokens — 补精确断言 |
| 30 | + |
| 31 | +```typescript |
| 32 | +expect(formatTokens(1000)).toBe("1k"); |
| 33 | +expect(formatTokens(1500)).toBe("1.5k"); |
| 34 | +``` |
| 35 | + |
| 36 | +#### formatRelativeTime — toContain → toBe |
| 37 | + |
| 38 | +```typescript |
| 39 | +// 当前(弱) |
| 40 | +expect(formatRelativeTime(diff, now)).toContain("30"); |
| 41 | +expect(formatRelativeTime(diff, now)).toContain("ago"); |
| 42 | + |
| 43 | +// 修复为 |
| 44 | +expect(formatRelativeTime(diff, now)).toBe("30s ago"); |
| 45 | +``` |
| 46 | + |
| 47 | +#### 新增:formatDuration 进位边界 |
| 48 | + |
| 49 | +| 用例 | 输入 | 期望 | |
| 50 | +|------|------|------| |
| 51 | +| 59.5s 进位 | 59500ms | 至少含 `1m` | |
| 52 | +| 59m59s 进位 | 3599000ms | 至少含 `1h` | |
| 53 | +| sub-millisecond | 0.5ms | `"<1ms"` 或 `"0ms"` | |
| 54 | + |
| 55 | +#### 新增:未测试函数 |
| 56 | + |
| 57 | +| 函数 | 最少用例 | |
| 58 | +|------|---------| |
| 59 | +| `formatRelativeTimeAgo` | 2(过去 / 未来) | |
| 60 | +| `formatLogMetadata` | 1(基本调用不抛错) | |
| 61 | +| `formatResetTime` | 2(有值 / null) | |
| 62 | +| `formatResetText` | 1(基本调用) | |
| 63 | + |
| 64 | +--- |
| 65 | + |
| 66 | +## 10.2 `src/tools/shared/__tests__/gitOperationTracking.test.ts` |
| 67 | + |
| 68 | +**问题**:`detectGitOperation` 内部调用 `getCommitCounter()`、`getPrCounter()`、`logEvent()`,测试产生分析副作用。 |
| 69 | + |
| 70 | +### 修改清单 |
| 71 | + |
| 72 | +#### 添加 analytics mock |
| 73 | + |
| 74 | +在文件顶部添加 `mock.module`: |
| 75 | + |
| 76 | +```typescript |
| 77 | +import { mock, afterAll, afterEach, beforeEach } from "bun:test"; |
| 78 | + |
| 79 | +mock.module("src/services/analytics/index.ts", () => ({ |
| 80 | + logEvent: mock(() => {}), |
| 81 | +})); |
| 82 | + |
| 83 | +mock.module("src/bootstrap/state.ts", () => ({ |
| 84 | + getCommitCounter: mock(() => ({ increment: mock(() => {}) })), |
| 85 | + getPrCounter: mock(() => ({ increment: mock(() => {}) })), |
| 86 | +})); |
| 87 | +``` |
| 88 | + |
| 89 | +> 需验证 `detectGitOperation` 的实际导入路径,按需调整 mock 目标。 |
| 90 | +
|
| 91 | +#### 新增:缺失的 GH PR actions |
| 92 | + |
| 93 | +| 用例 | 输入 | 期望 | |
| 94 | +|------|------|------| |
| 95 | +| gh pr edit | `'gh pr edit 123 --title "fix"'` | `result.pr.number === 123` | |
| 96 | +| gh pr close | `'gh pr close 456'` | `result.pr.number === 456` | |
| 97 | +| gh pr ready | `'gh pr ready 789'` | `result.pr.number === 789` | |
| 98 | +| gh pr comment | `'gh pr comment 123 --body "done"'` | `result.pr.number === 123` | |
| 99 | + |
| 100 | +#### 新增:parseGitCommitId 边界 |
| 101 | + |
| 102 | +| 用例 | 输入 | 期望 | |
| 103 | +|------|------|------| |
| 104 | +| 完整 40 字符 SHA | `'[abcdef0123456789abcdef0123456789abcdef01] ...'` | 返回完整 40 字符 | |
| 105 | +| 畸形括号输出 | `'create mode 100644 file.txt'` | 返回 `null` | |
| 106 | + |
| 107 | +--- |
| 108 | + |
| 109 | +## 10.3 `src/utils/permissions/__tests__/PermissionMode.test.ts` |
| 110 | + |
| 111 | +**问题**:`isExternalPermissionMode` 在非 ant 环境永远返回 true,false 路径从未执行;mode 覆盖不完整。 |
| 112 | + |
| 113 | +### 修改清单 |
| 114 | + |
| 115 | +#### 补全 mode 覆盖 |
| 116 | + |
| 117 | +| 函数 | 缺失的 mode | |
| 118 | +|------|-------------| |
| 119 | +| `permissionModeTitle` | `bypassPermissions`, `dontAsk` | |
| 120 | +| `permissionModeShortTitle` | `dontAsk`, `acceptEdits` | |
| 121 | +| `getModeColor` | `dontAsk`, `acceptEdits`, `plan` | |
| 122 | +| `permissionModeFromString` | `acceptEdits`, `bypassPermissions` | |
| 123 | +| `toExternalPermissionMode` | `acceptEdits`, `bypassPermissions` | |
| 124 | + |
| 125 | +#### 修复 isExternalPermissionMode |
| 126 | + |
| 127 | +```typescript |
| 128 | +// 当前:只测了非 ant 环境(永远 true) |
| 129 | +// 需要新增 ant 环境测试 |
| 130 | +describe("when USER_TYPE is 'ant'", () => { |
| 131 | + beforeEach(() => { |
| 132 | + process.env.USER_TYPE = "ant"; |
| 133 | + }); |
| 134 | + afterEach(() => { |
| 135 | + delete process.env.USER_TYPE; |
| 136 | + }); |
| 137 | + |
| 138 | + test("returns false for 'auto' in ant context", () => { |
| 139 | + expect(isExternalPermissionMode("auto")).toBe(false); |
| 140 | + }); |
| 141 | + |
| 142 | + test("returns false for 'bubble' in ant context", () => { |
| 143 | + expect(isExternalPermissionMode("bubble")).toBe(false); |
| 144 | + }); |
| 145 | + |
| 146 | + test("returns true for non-ant modes in ant context", () => { |
| 147 | + expect(isExternalPermissionMode("plan")).toBe(true); |
| 148 | + }); |
| 149 | +}); |
| 150 | +``` |
| 151 | + |
| 152 | +#### 新增:permissionModeSchema |
| 153 | + |
| 154 | +| 用例 | 输入 | 期望 | |
| 155 | +|------|------|------| |
| 156 | +| 有效 mode | `'plan'` | `success: true` | |
| 157 | +| 无效 mode | `'invalid'` | `success: false` | |
| 158 | + |
| 159 | +--- |
| 160 | + |
| 161 | +## 10.4 `src/utils/permissions/__tests__/dangerousPatterns.test.ts` |
| 162 | + |
| 163 | +**问题**:纯数据 smoke test,无行为验证。 |
| 164 | + |
| 165 | +### 修改清单 |
| 166 | + |
| 167 | +#### 新增:重复值检查 |
| 168 | + |
| 169 | +```typescript |
| 170 | +test("CROSS_PLATFORM_CODE_EXEC has no duplicates", () => { |
| 171 | + const set = new Set(CROSS_PLATFORM_CODE_EXEC); |
| 172 | + expect(set.size).toBe(CROSS_PLATFORM_CODE_EXEC.length); |
| 173 | +}); |
| 174 | + |
| 175 | +test("DANGEROUS_BASH_PATTERNS has no duplicates", () => { |
| 176 | + const set = new Set(DANGEROUS_BASH_PATTERNS); |
| 177 | + expect(set.size).toBe(DANGEROUS_BASH_PATTERNS.length); |
| 178 | +}); |
| 179 | +``` |
| 180 | + |
| 181 | +#### 新增:全量成员断言(用 Set 确保精确) |
| 182 | + |
| 183 | +```typescript |
| 184 | +test("CROSS_PLATFORM_CODE_EXEC contains expected interpreters", () => { |
| 185 | + const expected = ["node", "python", "python3", "ruby", "perl", "php", |
| 186 | + "bun", "deno", "npx", "tsx"]; |
| 187 | + const set = new Set(CROSS_PLATFORM_CODE_EXEC); |
| 188 | + for (const entry of expected) { |
| 189 | + expect(set.has(entry)).toBe(true); |
| 190 | + } |
| 191 | +}); |
| 192 | +``` |
| 193 | + |
| 194 | +#### 新增:空字符串不匹配 |
| 195 | + |
| 196 | +```typescript |
| 197 | +test("empty string does not match any pattern", () => { |
| 198 | + for (const pattern of DANGEROUS_BASH_PATTERNS) { |
| 199 | + expect("".startsWith(pattern)).toBe(false); |
| 200 | + } |
| 201 | +}); |
| 202 | +``` |
| 203 | + |
| 204 | +--- |
| 205 | + |
| 206 | +## 10.5 `src/utils/__tests__/zodToJsonSchema.test.ts` |
| 207 | + |
| 208 | +**问题**:object 属性仅 `toBeDefined` 未验证类型结构;optional 字段未验证 absence。 |
| 209 | + |
| 210 | +### 修改清单 |
| 211 | + |
| 212 | +#### 修复 object schema 测试 |
| 213 | + |
| 214 | +```typescript |
| 215 | +// 当前(弱) |
| 216 | +expect(schema.properties!.name).toBeDefined(); |
| 217 | +expect(schema.properties!.age).toBeDefined(); |
| 218 | + |
| 219 | +// 修复为 |
| 220 | +expect(schema.properties!.name).toEqual({ type: "string" }); |
| 221 | +expect(schema.properties!.age).toEqual({ type: "number" }); |
| 222 | +``` |
| 223 | + |
| 224 | +#### 修复 optional 字段测试 |
| 225 | + |
| 226 | +```typescript |
| 227 | +test("optional field is not in required array", () => { |
| 228 | + const schema = zodToJsonSchema(z.object({ |
| 229 | + required: z.string(), |
| 230 | + optional: z.string().optional(), |
| 231 | + })); |
| 232 | + expect(schema.required).toEqual(["required"]); |
| 233 | + expect(schema.required).not.toContain("optional"); |
| 234 | +}); |
| 235 | +``` |
| 236 | + |
| 237 | +#### 新增:缺失的 schema 类型 |
| 238 | + |
| 239 | +| 用例 | 输入 | 期望 | |
| 240 | +|------|------|------| |
| 241 | +| `z.literal("foo")` | `z.literal("foo")` | `{ const: "foo" }` | |
| 242 | +| `z.null()` | `z.null()` | `{ type: "null" }` | |
| 243 | +| `z.union()` | `z.union([z.string(), z.number()])` | `{ anyOf: [...] }` | |
| 244 | +| `z.record()` | `z.record(z.string(), z.number())` | `{ type: "object", additionalProperties: { type: "number" } }` | |
| 245 | +| `z.tuple()` | `z.tuple([z.string(), z.number()])` | `{ type: "array", items: [...], additionalItems: false }` | |
| 246 | +| 嵌套 object | `z.object({ a: z.object({ b: z.string() }) })` | 验证嵌套属性结构 | |
| 247 | + |
| 248 | +--- |
| 249 | + |
| 250 | +## 10.6 `src/utils/__tests__/envValidation.test.ts` |
| 251 | + |
| 252 | +**问题**:`validateBoundedIntEnvVar` lower bound=100 时 value=1 返回 `status: "valid"`,疑似源码 bug。 |
| 253 | + |
| 254 | +### 修改清单 |
| 255 | + |
| 256 | +#### 验证 lower bound 行为 |
| 257 | + |
| 258 | +```typescript |
| 259 | +// 当前测试 |
| 260 | +test("value of 1 with lower bound 100", () => { |
| 261 | + const result = validateBoundedIntEnvVar("1", { defaultValue: 100, upperLimit: 1000, lowerLimit: 100 }); |
| 262 | + // 如果源码有 bug,这里应该暴露 |
| 263 | + expect(result.effective).toBeGreaterThanOrEqual(100); |
| 264 | + expect(result.status).toBe(result.effective !== 100 ? "capped" : "valid"); |
| 265 | +}); |
| 266 | +``` |
| 267 | + |
| 268 | +#### 新增边界用例 |
| 269 | + |
| 270 | +| 用例 | value | lowerLimit | 期望 | |
| 271 | +|------|-------|------------|------| |
| 272 | +| 低于 lower bound | `"50"` | 100 | `effective: 100, status: "capped"` | |
| 273 | +| 等于 lower bound | `"100"` | 100 | `effective: 100, status: "valid"` | |
| 274 | +| 浮点截断 | `"50.7"` | 100 | `effective: 100`(parseInt 截断后 cap) | |
| 275 | +| 空白字符 | `" 500 "` | 1 | `effective: 500, status: "valid"` | |
| 276 | +| defaultValue 为 0 | `"0"` | 0 | 需确认 `parsed <= 0` 逻辑 | |
| 277 | + |
| 278 | +> **行动**:先确认 `validateBoundedIntEnvVar` 源码中 lower bound 的实际执行路径。如果确实不生效,需先修源码再补测试。 |
| 279 | +
|
| 280 | +--- |
| 281 | + |
| 282 | +## 10.7 `src/utils/__tests__/file.test.ts` |
| 283 | + |
| 284 | +**问题**:`addLineNumbers` 仅 `toContain`,未验证完整格式。 |
| 285 | + |
| 286 | +### 修改清单 |
| 287 | + |
| 288 | +#### 修复 addLineNumbers 断言 |
| 289 | + |
| 290 | +```typescript |
| 291 | +// 当前(弱) |
| 292 | +expect(result).toContain("1"); |
| 293 | +expect(result).toContain("hello"); |
| 294 | + |
| 295 | +// 修复为(需确定 isCompactLinePrefixEnabled 行为) |
| 296 | +// 假设 compact=false,格式为 " 1→hello" |
| 297 | +test("formats single line with tab prefix", () => { |
| 298 | + // 先确认环境,如果 compact 模式不确定,用正则 |
| 299 | + expect(result).toMatch(/^\s*\d+[→\t]hello$/m); |
| 300 | +}); |
| 301 | +``` |
| 302 | + |
| 303 | +#### 新增:stripLineNumberPrefix 边界 |
| 304 | + |
| 305 | +| 用例 | 输入 | 期望 | |
| 306 | +|------|------|------| |
| 307 | +| 纯数字行 | `"123"` | `""` | |
| 308 | +| 无内容前缀 | `"→"` | `""` | |
| 309 | +| compact 格式 `"1\thello"` | `"1\thello"` | `"hello"` | |
| 310 | + |
| 311 | +#### 新增:pathsEqual 边界 |
| 312 | + |
| 313 | +| 用例 | a | b | 期望 | |
| 314 | +|------|---|---|------| |
| 315 | +| 尾部斜杠差异 | `"/a/b"` | `"/a/b/"` | `false` | |
| 316 | +| `..` 段 | `"/a/../b"` | `"/b"` | 视实现而定 | |
| 317 | + |
| 318 | +--- |
| 319 | + |
| 320 | +## 10.8 `src/utils/__tests__/notebook.test.ts` |
| 321 | + |
| 322 | +**问题**:`mapNotebookCellsToToolResult` 内容检查用 `toContain`,未验证 XML 格式。 |
| 323 | + |
| 324 | +### 修改清单 |
| 325 | + |
| 326 | +#### 修复 content 断言 |
| 327 | + |
| 328 | +```typescript |
| 329 | +// 当前(弱) |
| 330 | +expect(result).toContain("cell-0"); |
| 331 | +expect(result).toContain("print('hello')"); |
| 332 | + |
| 333 | +// 修复为 |
| 334 | +expect(result).toContain('<cell id="cell-0">'); |
| 335 | +expect(result).toContain("</cell>"); |
| 336 | +``` |
| 337 | + |
| 338 | +#### 新增:parseCellId 边界 |
| 339 | + |
| 340 | +| 用例 | 输入 | 期望 | |
| 341 | +|------|------|------| |
| 342 | +| 负数 | `"cell--1"` | `null` | |
| 343 | +| 前导零 | `"cell-007"` | `7` | |
| 344 | +| 极大数 | `"cell-999999999"` | `999999999` | |
| 345 | + |
| 346 | +#### 新增:mapNotebookCellsToToolResult 边界 |
| 347 | + |
| 348 | +| 用例 | 输入 | 期望 | |
| 349 | +|------|------|------| |
| 350 | +| 空 data 数组 | `{ cells: [] }` | 空字符串或空结果 | |
| 351 | +| 无 cell_id | `{ cell_type: "code", source: "x" }` | fallback 到 `cell-${index}` | |
| 352 | +| error output | `{ output_type: "error", ename: "Error", evalue: "msg" }` | 包含 error 信息 | |
| 353 | + |
| 354 | +--- |
| 355 | + |
| 356 | +## 验收标准 |
| 357 | + |
| 358 | +- [ ] `bun test` 全部通过 |
| 359 | +- [ ] 8 个文件评分从 WEAK 提升至 ACCEPTABLE 或 GOOD |
| 360 | +- [ ] `toContain` 仅用于警告文本等确实不确定精确值的场景 |
| 361 | +- [ ] envValidation bug 确认并修复(或确认非 bug 并更新测试) |
0 commit comments