Files
claude-code/src/__tests__/Tool.test.ts
claude-code-best 92f8a92fbb feat: 正式启用 auto mode (#307)
* fix: 修复settings.json内存状态溢出的问题

* fix: 修复auto mode gate check未处理的promise rejection

在 bypassPermissionsKillswitch.ts 的 useKickOffCheckAndDisableAutoModeIfNeeded
中,void fire-and-forget 调用缺少 .catch() 处理,导致 verifyAutoModeGateAccess
失败时产生 unhandled promise rejection。同时移除 permissionSetup.ts 中冗余的
null check。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: 开放 auto mode 和 bypass mode 给所有用户

通过 Shift+Tab 统一循环:default → acceptEdits → plan → auto → bypassPermissions → default

- 移除 USER_TYPE 分支判断,所有用户使用同一循环路径
- isBypassPermissionsModeAvailable 始终为 true
- isAutoModeAvailable 初始化直接为 true
- 移除 AutoModeOptInDialog 确认流程
- 简化 isAutoModeGateEnabled 仅保留快模式熔断器
- 简化 verifyAutoModeGateAccess 仅检查快模式
- 移除 GrowthBook/Statsig 远程门控
- bypass permissions killswitch 改为 no-op
- 新增 24 个测试覆盖循环逻辑和门控不变量

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: 为sideQuery添加Langfuse追踪

sideQuery 绕过了 claude.ts 的主 API 路径,导致所有走 sideQuery 的调用
(auto mode classifier、permission explainer、session search 等)都没有
Langfuse 记录。现在为每次 sideQuery 调用创建独立 trace 并记录 LLM observation,
未配置 Langfuse 时全部 no-op。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: ACP availableModes 补齐 bypassPermissions 并修正测试 import 路径

- ACP agent availableModes 按条件包含 bypassPermissions(非 root/sandbox)
- 顺序对齐 REPL 循环:default → acceptEdits → plan → auto → bypassPermissions
- 新增 2 个测试验证 availableModes 包含 bypassPermissions 及模式切换
- 修正 getNextPermissionMode.test.ts 和 permissionSetup.test.ts 的 import 路径

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 10:20:27 +08:00

208 lines
6.3 KiB
TypeScript

import { describe, expect, test } from 'bun:test'
import {
buildTool,
toolMatchesName,
findToolByName,
getEmptyToolPermissionContext,
filterToolProgressMessages,
} from '../Tool'
// Minimal tool definition for testing buildTool
function makeMinimalToolDef(overrides: Record<string, unknown> = {}) {
return {
name: 'TestTool',
inputSchema: { type: 'object' as const } as any,
maxResultSizeChars: 10000,
call: async () => ({ data: 'ok' }),
description: async () => 'A test tool',
prompt: async () => 'test prompt',
mapToolResultToToolResultBlockParam: (
content: unknown,
toolUseID: string,
) => ({
type: 'tool_result' as const,
tool_use_id: toolUseID,
content: String(content),
}),
renderToolUseMessage: () => null,
...overrides,
}
}
describe('buildTool', () => {
test('fills in default isEnabled as true', () => {
const tool = buildTool(makeMinimalToolDef())
expect(tool.isEnabled()).toBe(true)
})
test('fills in default isConcurrencySafe as false', () => {
const tool = buildTool(makeMinimalToolDef())
expect(tool.isConcurrencySafe({})).toBe(false)
})
test('fills in default isReadOnly as false', () => {
const tool = buildTool(makeMinimalToolDef())
expect(tool.isReadOnly({})).toBe(false)
})
test('fills in default isDestructive as false', () => {
const tool = buildTool(makeMinimalToolDef())
expect(tool.isDestructive!({})).toBe(false)
})
test('fills in default checkPermissions as allow', async () => {
const tool = buildTool(makeMinimalToolDef())
const input = { foo: 'bar' }
const result = await tool.checkPermissions(input, {} as any)
expect(result).toEqual({ behavior: 'allow', updatedInput: input })
})
test('fills in default userFacingName from tool name', () => {
const tool = buildTool(makeMinimalToolDef())
expect(tool.userFacingName(undefined)).toBe('TestTool')
})
test('fills in default toAutoClassifierInput as empty string', () => {
const tool = buildTool(makeMinimalToolDef())
expect(tool.toAutoClassifierInput({})).toBe('')
})
test('preserves explicitly provided methods', () => {
const tool = buildTool(
makeMinimalToolDef({
isEnabled: () => false,
isConcurrencySafe: () => true,
isReadOnly: () => true,
}),
)
expect(tool.isEnabled()).toBe(false)
expect(tool.isConcurrencySafe({})).toBe(true)
expect(tool.isReadOnly({})).toBe(true)
})
test('preserves all non-defaultable properties', () => {
const tool = buildTool(makeMinimalToolDef())
expect(tool.name).toBe('TestTool')
expect(tool.maxResultSizeChars).toBe(10000)
expect(typeof tool.call).toBe('function')
expect(typeof tool.description).toBe('function')
expect(typeof tool.prompt).toBe('function')
})
})
describe('toolMatchesName', () => {
test('returns true for exact name match', () => {
expect(toolMatchesName({ name: 'Bash' }, 'Bash')).toBe(true)
})
test('returns false for non-matching name', () => {
expect(toolMatchesName({ name: 'Bash' }, 'Read')).toBe(false)
})
test('returns true when name matches an alias', () => {
expect(
toolMatchesName(
{ name: 'Bash', aliases: ['BashTool', 'Shell'] },
'BashTool',
),
).toBe(true)
})
test('returns false when aliases is undefined', () => {
expect(toolMatchesName({ name: 'Bash' }, 'BashTool')).toBe(false)
})
test('returns false when aliases is empty', () => {
expect(toolMatchesName({ name: 'Bash', aliases: [] }, 'BashTool')).toBe(
false,
)
})
})
describe('findToolByName', () => {
const mockTools = [
buildTool(makeMinimalToolDef({ name: 'Bash' })),
buildTool(makeMinimalToolDef({ name: 'Read', aliases: ['FileRead'] })),
buildTool(makeMinimalToolDef({ name: 'Edit' })),
]
test('finds tool by primary name', () => {
const tool = findToolByName(mockTools, 'Bash')
expect(tool).toBeDefined()
expect(tool!.name).toBe('Bash')
})
test('finds tool by alias', () => {
const tool = findToolByName(mockTools, 'FileRead')
expect(tool).toBeDefined()
expect(tool!.name).toBe('Read')
})
test('returns undefined when no match', () => {
expect(findToolByName(mockTools, 'NonExistent')).toBeUndefined()
})
test('returns first match when duplicates exist', () => {
const dupeTools = [
buildTool(makeMinimalToolDef({ name: 'Bash', maxResultSizeChars: 100 })),
buildTool(makeMinimalToolDef({ name: 'Bash', maxResultSizeChars: 200 })),
]
const tool = findToolByName(dupeTools, 'Bash')
expect(tool!.maxResultSizeChars).toBe(100)
})
})
describe('getEmptyToolPermissionContext', () => {
test('returns default permission mode', () => {
const ctx = getEmptyToolPermissionContext()
expect(ctx.mode).toBe('default')
})
test('returns empty maps and arrays', () => {
const ctx = getEmptyToolPermissionContext()
expect(ctx.additionalWorkingDirectories.size).toBe(0)
expect(ctx.alwaysAllowRules).toEqual({})
expect(ctx.alwaysDenyRules).toEqual({})
expect(ctx.alwaysAskRules).toEqual({})
})
test('returns isBypassPermissionsModeAvailable as true', () => {
const ctx = getEmptyToolPermissionContext()
expect(ctx.isBypassPermissionsModeAvailable).toBe(true)
})
})
describe('filterToolProgressMessages', () => {
test('filters out hook_progress messages', () => {
const messages = [
{ data: { type: 'hook_progress', hookName: 'pre' } },
{ data: { type: 'tool_progress', toolName: 'Bash' } },
] as any[]
const result = filterToolProgressMessages(messages)
expect(result).toHaveLength(1)
expect((result[0]!.data as any).type).toBe('tool_progress')
})
test('keeps tool progress messages', () => {
const messages = [
{ data: { type: 'tool_progress', toolName: 'Bash' } },
{ data: { type: 'tool_progress', toolName: 'Read' } },
] as any[]
const result = filterToolProgressMessages(messages)
expect(result).toHaveLength(2)
})
test('returns empty array for empty input', () => {
expect(filterToolProgressMessages([])).toEqual([])
})
test('handles messages without type field', () => {
const messages = [
{ data: { toolName: 'Bash' } },
{ data: { type: 'hook_progress' } },
] as any[]
const result = filterToolProgressMessages(messages)
expect(result).toHaveLength(1)
})
})