105 lines
5.1 KiB
Markdown
105 lines
5.1 KiB
Markdown
|
|
# 权限系统测试计划
|
|||
|
|
|
|||
|
|
## 概述
|
|||
|
|
|
|||
|
|
权限系统控制工具是否可以执行,包含规则解析器、权限检查管线和权限模式判断。测试重点是纯函数解析器和规则匹配逻辑。
|
|||
|
|
|
|||
|
|
## 被测文件
|
|||
|
|
|
|||
|
|
| 文件 | 关键导出 |
|
|||
|
|
|------|----------|
|
|||
|
|
| `src/utils/permissions/permissionRuleParser.ts` | `permissionRuleValueFromString`, `permissionRuleValueToString`, `escapeRuleContent`, `unescapeRuleContent`, `normalizeLegacyToolName`, `getLegacyToolNames` |
|
|||
|
|
| `src/utils/permissions/PermissionMode.ts` | 权限模式常量和辅助函数 |
|
|||
|
|
| `src/utils/permissions/permissions.ts` | `hasPermissionsToUseTool`, `getDenyRuleForTool`, `checkRuleBasedPermissions` |
|
|||
|
|
| `src/types/permissions.ts` | `PermissionMode`, `PermissionBehavior`, `PermissionRule` 类型定义 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 测试用例
|
|||
|
|
|
|||
|
|
### src/utils/permissions/permissionRuleParser.ts
|
|||
|
|
|
|||
|
|
#### describe('escapeRuleContent')
|
|||
|
|
|
|||
|
|
- test('escapes backslashes first') — `'test\\value'` → `'test\\\\value'`
|
|||
|
|
- test('escapes opening parentheses') — `'print(1)'` → `'print\\(1\\)'`
|
|||
|
|
- test('escapes closing parentheses') — `'func()'` → `'func\\(\\)'`
|
|||
|
|
- test('handles combined escape') — `'echo "test\\nvalue"'` 中的 `\\` 先转义
|
|||
|
|
- test('handles empty string') — `''` → `''`
|
|||
|
|
- test('no-op for string without special chars') — `'npm install'` 原样返回
|
|||
|
|
|
|||
|
|
#### describe('unescapeRuleContent')
|
|||
|
|
|
|||
|
|
- test('unescapes parentheses') — `'print\\(1\\)'` → `'print(1)'`
|
|||
|
|
- test('unescapes backslashes last') — `'test\\\\nvalue'` → `'test\\nvalue'`
|
|||
|
|
- test('handles empty string')
|
|||
|
|
- test('roundtrip: escape then unescape returns original') — `unescapeRuleContent(escapeRuleContent(x)) === x`
|
|||
|
|
|
|||
|
|
#### describe('permissionRuleValueFromString')
|
|||
|
|
|
|||
|
|
- test('parses tool name only') — `'Bash'` → `{ toolName: 'Bash' }`
|
|||
|
|
- test('parses tool name with content') — `'Bash(npm install)'` → `{ toolName: 'Bash', ruleContent: 'npm install' }`
|
|||
|
|
- test('parses content with escaped parentheses') — `'Bash(python -c "print\\(1\\)")'` → ruleContent 为 `'python -c "print(1)"'`
|
|||
|
|
- test('treats empty parens as tool-wide rule') — `'Bash()'` → `{ toolName: 'Bash' }`(无 ruleContent)
|
|||
|
|
- test('treats wildcard content as tool-wide rule') — `'Bash(*)'` → `{ toolName: 'Bash' }`
|
|||
|
|
- test('normalizes legacy tool names') — `'Task'` → `{ toolName: 'Agent' }`(或对应的 AGENT_TOOL_NAME)
|
|||
|
|
- test('handles malformed input: no closing paren') — `'Bash(npm'` → 整个字符串作为 toolName
|
|||
|
|
- test('handles malformed input: content after closing paren') — `'Bash(npm)extra'` → 整个字符串作为 toolName
|
|||
|
|
- test('handles missing tool name') — `'(foo)'` → 整个字符串作为 toolName
|
|||
|
|
|
|||
|
|
#### describe('permissionRuleValueToString')
|
|||
|
|
|
|||
|
|
- test('serializes tool name only') — `{ toolName: 'Bash' }` → `'Bash'`
|
|||
|
|
- test('serializes with content') — `{ toolName: 'Bash', ruleContent: 'npm install' }` → `'Bash(npm install)'`
|
|||
|
|
- test('escapes content with parentheses') — ruleContent 含 `()` 时正确转义
|
|||
|
|
- test('roundtrip: fromString then toString preserves value') — 往返一致
|
|||
|
|
|
|||
|
|
#### describe('normalizeLegacyToolName')
|
|||
|
|
|
|||
|
|
- test('maps Task to Agent tool name') — `'Task'` → AGENT_TOOL_NAME
|
|||
|
|
- test('maps KillShell to TaskStop tool name') — `'KillShell'` → TASK_STOP_TOOL_NAME
|
|||
|
|
- test('maps AgentOutputTool to TaskOutput tool name')
|
|||
|
|
- test('returns unknown names unchanged') — `'UnknownTool'` → `'UnknownTool'`
|
|||
|
|
|
|||
|
|
#### describe('getLegacyToolNames')
|
|||
|
|
|
|||
|
|
- test('returns legacy names for canonical name') — 给定 AGENT_TOOL_NAME 返回包含 `'Task'`
|
|||
|
|
- test('returns empty array for name with no legacy aliases')
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### src/utils/permissions/permissions.ts — 需 Mock
|
|||
|
|
|
|||
|
|
#### describe('getDenyRuleForTool')
|
|||
|
|
|
|||
|
|
- test('returns deny rule matching tool name') — 匹配到 blanket deny 规则时返回
|
|||
|
|
- test('returns null when no deny rules match') — 无匹配时返回 null
|
|||
|
|
- test('matches MCP tools by server prefix') — `mcp__server` 规则匹配该 server 下的 MCP 工具
|
|||
|
|
- test('does not match content-specific deny rules') — 有 ruleContent 的 deny 规则不作为 blanket deny
|
|||
|
|
|
|||
|
|
#### describe('checkRuleBasedPermissions')(集成级别)
|
|||
|
|
|
|||
|
|
- test('deny rule takes precedence over allow') — 同时有 allow 和 deny 时 deny 优先
|
|||
|
|
- test('ask rule prompts user') — 匹配 ask 规则返回 `{ behavior: 'ask' }`
|
|||
|
|
- test('allow rule permits execution') — 匹配 allow 规则返回 `{ behavior: 'allow' }`
|
|||
|
|
- test('passthrough when no rules match') — 无匹配规则返回 passthrough
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Mock 需求
|
|||
|
|
|
|||
|
|
| 依赖 | Mock 方式 | 说明 |
|
|||
|
|
|------|-----------|------|
|
|||
|
|
| `bun:bundle` (feature) | 已 polyfill | BRIEF_TOOL_NAME 条件加载 |
|
|||
|
|
| Tool 常量导入 | 实际值 | AGENT_TOOL_NAME 等从常量文件导入 |
|
|||
|
|
| `appState` | mock object | `hasPermissionsToUseTool` 中的状态依赖 |
|
|||
|
|
| Tool 对象 | mock object | 模拟 tool 的 name, checkPermissions 等 |
|
|||
|
|
|
|||
|
|
## 集成测试场景
|
|||
|
|
|
|||
|
|
### describe('Permission pipeline end-to-end')
|
|||
|
|
|
|||
|
|
- test('deny rule blocks tool before it runs') — deny 规则在 call 前拦截
|
|||
|
|
- test('bypassPermissions mode allows all') — bypass 模式下 ask → allow
|
|||
|
|
- test('dontAsk mode converts ask to deny') — dontAsk 模式下 ask → deny
|