import assert from 'node:assert/strict' import { renderToStaticMarkup } from 'react-dom/server' import type { ChatInteraction } from '../src/shared/conversation' import { ToolCallRenderer } from '../src/renderer/src/workbuddy/conversation/ToolCallRenderer' import { findToolApproval, hasSourcePermissionRequest, isActiveToolApproval, sourcePermissionMetadata, toolApprovalOption } from '../src/renderer/src/workbuddy/conversation/tool-approval' import type { ConversationToolCall } from '../src/renderer/src/workbuddy/conversation/types' const permissionOptions = [ { optionId: 'always', kind: 'allow_always' }, { optionId: 'once', kind: 'allow_once' }, { optionId: 'allow', kind: 'allow' }, { optionId: 'reject', kind: 'reject' }, { optionId: 'reject-once', kind: 'reject_once' } ] assert.equal(toolApprovalOption(permissionOptions, 'allow')?.optionId, 'once', 'once takes priority over allow_always and allow') assert.equal(toolApprovalOption([permissionOptions[0]], 'allow'), undefined, 'allow_always is never used by the source action') assert.equal(toolApprovalOption([permissionOptions[2]], 'allow')?.optionId, 'allow') assert.equal(toolApprovalOption(permissionOptions, 'reject')?.optionId, 'reject-once') assert.equal(toolApprovalOption([permissionOptions[3]], 'reject')?.optionId, 'reject') const firstApproval = { id: 'approval-1', runId: 'run', toolCallId: 'file-call', title: 'Authorize file write', status: 'pending', kind: 'approval', requestId: 'request-1', options: permissionOptions } satisfies ChatInteraction const latestApproval = { ...firstApproval, id: 'approval-2', requestId: 'request-2', status: 'responding' } satisfies ChatInteraction const unrelatedApproval = { ...firstApproval, id: 'approval-other', toolCallId: 'other-call' } satisfies ChatInteraction assert.equal(findToolApproval([firstApproval, unrelatedApproval, latestApproval], 'file-call'), latestApproval) assert.equal(findToolApproval([firstApproval], 'missing'), undefined) assert.equal(isActiveToolApproval(firstApproval as Extract), true) assert.equal(isActiveToolApproval(latestApproval as Extract), true) assert.equal(isActiveToolApproval({ ...latestApproval, status: 'answered' } as Extract), false) const request = { requestId: 'request-file', permissionOptions: [{ optionId: 'once', name: 'Allow once', kind: 'allow_once' }] } const fileTool: ConversationToolCall = { id: 'file-call', name: 'edit_file', status: 'pending', args: { filePath: 'src/file.ts', oldContent: 'before', content: 'after' }, metaData: { acp_request_permission: request } } const commandTool: ConversationToolCall = { id: 'command-call', name: 'execute_command', status: 'pending', args: { command: 'npm test' }, metadata: { acp_request_permission: { ...request, requestId: 'request-command' } } } assert.equal(hasSourcePermissionRequest(fileTool), true) assert.equal(hasSourcePermissionRequest(commandTool), true) assert.equal(hasSourcePermissionRequest({ ...fileTool, metaData: undefined, metadata: fileTool.metaData }), true) assert.equal(hasSourcePermissionRequest({ ...fileTool, metaData: { acp_request_permission: { permissionOptions: request.permissionOptions } } }), false) assert.equal(hasSourcePermissionRequest({ ...fileTool, metaData: { acp_request_permission: { requestId: 'empty-options', permissionOptions: [] } } }), false) assert.equal(hasSourcePermissionRequest({ ...fileTool, metaData: undefined, metadata: undefined }), false) const mergedMetadata = sourcePermissionMetadata({ source: 'kept', acp_request_permission: { scope: 'local', requestId: 'old' } }, latestApproval as Extract) assert.deepEqual(mergedMetadata, { source: 'kept', acp_request_permission: { scope: 'local', requestId: 'request-2', permissionOptions: permissionOptions } }) const pendingControl = { status: 'pending' as const, onDecision: () => undefined } const respondingControl = { status: 'responding' as const, onDecision: () => undefined } const rendered = (tool: ConversationToolCall, control = pendingControl) => renderToStaticMarkup() const extractActions = (markup: string) => [...markup.matchAll(/]*data-tool-decision="([^"]+)")(?=[^>]*class="[^"]*cr-tool-btn--([\w-]+)[^"]*")([^>]*)>([\s\S]*?)<\/button>/g)].map((match) => ({ decision: match[1], variant: match[2], disabled: /\sdisabled(?:="")?(?:\s|$)/.test(match[3]), busy: /aria-busy="true"/.test(match[3]), label: match[4].replace(/<[^>]*>/g, '') })) const pendingFile = rendered(fileTool) assert.deepEqual(extractActions(pendingFile).map(({ decision, variant, label }) => ({ decision, variant, label })), [ { decision: 'allow', variant: 'primary', label: '接受' }, { decision: 'reject', variant: 'danger', label: '拒绝' } ]) const pendingCommand = rendered(commandTool) assert.match(pendingCommand, /确认执行命令?/) assert.deepEqual(extractActions(pendingCommand).map(({ decision, variant, label }) => ({ decision, variant, label })), [ { decision: 'allow', variant: 'primary', label: '运行' }, { decision: 'skip', variant: 'default', label: '跳过' }, { decision: 'reject', variant: 'danger', label: '拒绝' } ]) for (const tool of [ { ...fileTool, status: 'running' as const }, { ...commandTool, status: 'running' as const }, { ...fileTool, metaData: undefined, metadata: undefined }, { ...commandTool, metaData: undefined, metadata: undefined }, { ...fileTool, metaData: { acp_request_permission: { requestId: 'empty', permissionOptions: [] } } } ]) { assert.deepEqual(extractActions(rendered(tool)).length, 0) } const respondingCommand = extractActions(rendered(commandTool, respondingControl)) assert.deepEqual(respondingCommand.map(({ decision, disabled, busy }) => ({ decision, disabled, busy })), [ { decision: 'allow', disabled: true, busy: true }, { decision: 'skip', disabled: true, busy: true }, { decision: 'reject', disabled: true, busy: true } ]) console.log('Tool approval helper and inline source markup regression checks passed.')