diff --git a/docs/safety/auto-mode.mdx b/docs/safety/auto-mode.mdx new file mode 100644 index 0000000..2e038da --- /dev/null +++ b/docs/safety/auto-mode.mdx @@ -0,0 +1,189 @@ +--- +title: "Auto Mode - AI 分类器驱动的自主执行模式" +description: "详解 Claude Code 的 auto mode:基于 transcript classifier 的自动权限决策、两阶段分类流水线、危险权限剥离机制、模式切换状态管理、以及与 plan mode 的协作方式。" +keywords: ["auto mode", "yoloClassifier", "transcript classifier", "权限分类", "自动执行", "两阶段分类"] +--- + +## 概述 + +Auto mode 是 Claude Code 的一种权限模式,让 AI 进入**连续自主执行**状态。与传统模式(每个敏感操作都弹出权限对话框等待用户审批)不同,auto mode 使用 AI 分类器(transcript classifier)自动判断每个工具调用是否安全,从而实现无中断的执行体验。 + +``` +权限模式层级: + +default → auto → bypassPermissions +(逐项确认) (AI 分类器审批) (全部放行) +``` + +## 核心架构 + +### 1. AI 分类器(yoloClassifier) + +分类器是 auto mode 的核心,位于 `src/utils/permissions/yoloClassifier.ts`。 + +每个工具调用经过分类器评估,返回三种裁决之一: + +| 裁决 | 含义 | 处理方式 | +|------|------|---------| +| **allow** | 操作安全 | 直接执行,用户无感知 | +| **deny** | 操作危险 | 拒绝执行,向 AI 返回拒绝消息 | +| **ask** | 无法确定 | 回退到传统权限对话框 | + +分类器的判断基于完整的对话上下文(transcript),而不仅仅看单条命令——它能理解操作的意图和上下文。 + +### 2. 两阶段分类流水线 + +分类器支持两阶段 XML 分类(`classifyYoloActionXml`): + +``` +Stage 1 — "fast"(快速判断) +├── max_tokens=64,stop_sequences 触发即时 yes/no +├── 如果 allow → 直接放行(最快路径) +└── 如果 block → 进入 Stage 2 + +Stage 2 — "thinking"(深度思考) +├── chain-of-thought 推理 +├── 减少误报(false positives) +└── 最终决定 allow / deny / ask +``` + +两个阶段共享相同的 system prompt 和 user content,利用 API 的 prompt caching(1 小时 TTL)优化性能。 + +可通过配置选择模式: +- `'both'`(默认)— 两阶段都跑 +- `'fast'` — 只跑 Stage 1 +- `'thinking'` — 只跑 Stage 2 + +### 3. 分类器结果类型 + +```typescript +// src/types/permissions.ts +type YoloClassifierResult = { + thinking?: string // 分类器的推理过程 + shouldBlock: boolean // 是否阻止 + reason: string // 决策原因 + unavailable?: boolean // 分类器是否不可用 + transcriptTooLong?: boolean // 对话是否超出上下文窗口 + model: string // 使用的分类器模型 + stage?: 'fast' | 'thinking' // 哪个阶段做出的决定 + // ... token 使用量、耗时等监控字段 +} +``` + +## 安全机制 + +### 危险权限剥离 + +进入 auto mode 时,系统调用 `stripDangerousPermissionsForAutoMode()`(`permissionSetup.ts:510`),移除所有可能绕过分类器的 allow 规则。 + +被剥离的规则类型(`dangerousPatterns.ts`): + +| 规则类型 | 示例 | 剥离原因 | +|---------|------|---------| +| **Bash 代码执行** | `Bash(python:*)`, `Bash(node:*)` | 解释器可执行任意代码,绕过分类器审查 | +| **Shell 入口** | `Bash(bash:*)`, `Bash(sh:*)` | 直接 shell 访问等同无限制 | +| **Agent 规则** | `Agent(*)` | 任何 Agent allow 规则会绕过分类器审批子代理 | +| **PowerShell 代码执行** | `PowerShell(node:*)` | 同 Bash 逻辑 | +| **权限提升** | `Bash(sudo:*)`, `Bash(eval:*)` | 可执行任意命令 | + +剥离的规则被暂存在 `strippedDangerousRules` 中,退出 auto mode 时通过 `restoreDangerousPermissions()` 恢复。 + +### 模型支持检测 + +不是所有模型都支持 auto mode。`modelSupportsAutoMode()`(`src/utils/betas.ts`)检查当前模型是否具备安全分类能力。不支持的模型无法进入 auto mode。 + +### Circuit Breaker 机制 + +`autoModeState.ts` 维护一个 circuit breaker 标志: + +```typescript +let autoModeCircuitBroken = false // 由远程配置控制 +``` + +当远程配置(GrowthBook `tengu_auto_mode_config.enabled`)设为 `'disabled'` 时,circuit breaker 触发,阻止 auto mode 的进入和继续使用。这为 Anthropic 提供了远程紧急关停能力。 + +## 模式切换状态管理 + +### 进入 Auto Mode + +`transitionPermissionMode()`(`permissionSetup.ts:597`)处理所有模式切换: + +``` +1. 检查 auto mode gate 是否开启(isAutoModeGateEnabled) +2. 设置 autoModeActive = true +3. 调用 stripDangerousPermissionsForAutoMode() 剥离危险规则 +4. 向对话注入 Auto Mode 系统提示 +``` + +### 退出 Auto Mode + +``` +1. 设置 autoModeActive = false +2. 设置 needsAutoModeExitAttachment = true(触发退出通知) +3. 调用 restoreDangerousPermissions() 恢复被剥离的规则 +4. 向对话注入 "Exited Auto Mode" 提示 +``` + +### 触发路径 + +Auto mode 可通过以下方式激活: +- CLI 参数 `--enable-auto-mode` +- settings.json 中的 `autoMode` 配置 +- Plan mode 默认使用 auto mode 语义(`useAutoModeDuringPlan`,默认 true) +- SDK 控制消息 +- REPL 中 Shift+Tab 切换 + +## 系统提示词 + +### 进入时(Full Instructions) + +注入到对话中的指令(`messages.ts:3464`): + +> Auto mode is active. The user chose continuous, autonomous execution. You should: +> +> 1. **Execute immediately** — 直接实现,做合理假设 +> 2. **Minimize interruptions** — 常规决策自行判断,减少提问 +> 3. **Prefer action over planning** — 默认直接编码,不进 plan mode +> 4. **Expect course corrections** — 用户可随时纠正 +> 5. **Do not take overly destructive actions** — 删除数据/修改生产系统仍需确认 +> 6. **Avoid data exfiltration** — 不主动分享密钥/内部文档 + +### 持续运行时(Sparse Instructions) + +后续轮次注入简短提醒: + +> Auto mode still active. Execute autonomously, minimize interruptions, prefer action over planning. + +### 退出时(Exit Instructions) + +> You have exited auto mode. Ask clarifying questions when the approach is ambiguous rather than making assumptions. + +## 与 Plan Mode 的协作 + +Plan mode 默认使用 auto mode 语义(`getUseAutoModeDuringPlan()`,默认 true)。这意味着: + +- Plan mode 进入时,如果 auto mode 可用,也会激活分类器 +- `isAutoModeActive()` 是权威信号(`prePlanMode`/`strippedDangerousRules` 不可靠) +- 退出 plan mode 时会同时退出 auto mode + +## 分类器不可用的降级策略 + +当分类器 API 不可用时(`unavailable: true` 或 `transcriptTooLong: true`): + +- 不会直接 allow — 回退到传统的权限对话框(ask) +- 向 AI 发送消息:"{model} is temporarily unavailable, so auto mode cannot determine the safety of {toolName} right now." +- 确定性错误(如对话过长)不重试,直接降级 + +## 相关源码索引 + +| 文件 | 职责 | +|------|------| +| `src/utils/permissions/yoloClassifier.ts` | 分类器核心实现 | +| `src/utils/permissions/autoModeState.ts` | Auto mode 状态管理 | +| `src/utils/permissions/permissionSetup.ts` | 模式切换、危险权限剥离 | +| `src/utils/permissions/dangerousPatterns.ts` | 危险命令模式列表 | +| `src/utils/permissions/classifierDecision.ts` | 分类器决策处理 | +| `src/utils/permissions/classifierShared.ts` | 分类器共享逻辑 | +| `src/utils/messages.ts` | Auto mode 系统提示词 | +| `src/types/permissions.ts` | 权限类型定义 | +| `src/utils/betas.ts` | 模型 auto mode 支持检测 | diff --git a/docs/test-plans/phase-16-zero-dep-pure-functions.md b/docs/test-plans/phase-16-zero-dep-pure-functions.md new file mode 100644 index 0000000..23ebcb0 --- /dev/null +++ b/docs/test-plans/phase-16-zero-dep-pure-functions.md @@ -0,0 +1,188 @@ +# 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` class — 手动异步队列,实现 `AsyncIterator` + +| 测试用例 | 验证点 | +|---------|--------| +| 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` 实现确定性测试 diff --git a/docs/test-plans/phase-17-tool-submodules.md b/docs/test-plans/phase-17-tool-submodules.md new file mode 100644 index 0000000..e01b080 --- /dev/null +++ b/docs/test-plans/phase-17-tool-submodules.md @@ -0,0 +1,203 @@ +# 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 | diff --git a/docs/test-plans/phase-18-weak-fixes.md b/docs/test-plans/phase-18-weak-fixes.md new file mode 100644 index 0000000..0e78f9a --- /dev/null +++ b/docs/test-plans/phase-18-weak-fixes.md @@ -0,0 +1,110 @@ +# 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** |