docs: 完成新版测试文档
This commit is contained in:
parent
ac1f02958c
commit
6f5623b26c
435
docs/test-plans/phase19-batch1-micro-utils.md
Normal file
435
docs/test-plans/phase19-batch1-micro-utils.md
Normal file
@ -0,0 +1,435 @@
|
||||
# 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 对象
|
||||
287
docs/test-plans/phase19-batch2-utils-state-commands.md
Normal file
287
docs/test-plans/phase19-batch2-utils-state-commands.md
Normal file
@ -0,0 +1,287 @@
|
||||
# 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 需求
|
||||
无
|
||||
258
docs/test-plans/phase19-batch3-tool-submodules.md
Normal file
258
docs/test-plans/phase19-batch3-tool-submodules.md
Normal file
@ -0,0 +1,258 @@
|
||||
# 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()` 获取函数
|
||||
215
docs/test-plans/phase19-batch4-services.md
Normal file
215
docs/test-plans/phase19-batch4-services.md
Normal file
@ -0,0 +1,215 @@
|
||||
# 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` 做测试隔离
|
||||
200
docs/test-plans/phase19-batch5-mcp-config.md
Normal file
200
docs/test-plans/phase19-batch5-mcp-config.md
Normal file
@ -0,0 +1,200 @@
|
||||
# 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 成功后直接测
|
||||
Loading…
x
Reference in New Issue
Block a user