754 lines
37 KiB
TypeScript
754 lines
37 KiB
TypeScript
import assert from 'node:assert/strict'
|
|
import { renderToStaticMarkup } from 'react-dom/server'
|
|
import MessageBubble from '../src/renderer/src/components/MessageBubble'
|
|
import type { ChatMessage } from '../src/shared/types'
|
|
import {
|
|
AskUserQuestion,
|
|
PlanCreateBlock,
|
|
PlanUpdateBlock,
|
|
PermissionConfirm,
|
|
QuestionFloating,
|
|
QuestionAnswerTranscript,
|
|
TaskBlock,
|
|
ThinkingBlock,
|
|
ToolCallCard,
|
|
ToolCallRenderer,
|
|
ToolGroup,
|
|
buildQuestionAnswerPayload,
|
|
computeToolGroupVisibleStart,
|
|
getPermissionChoices,
|
|
getCapturedToolKind,
|
|
selectPlanTasks
|
|
} from '../src/renderer/src/workbuddy/conversation'
|
|
import { convertReadFile, createListFileOpenRequest, createReadFileOpenRequest } from '../src/renderer/src/workbuddy/conversation/ToolCallRenderer'
|
|
import { SourceToolHeader } from '../src/renderer/src/workbuddy/conversation/SourceToolHeader'
|
|
import { resolveSourceToolIcon } from '../src/renderer/src/workbuddy/conversation/SourceToolIcons'
|
|
import { SourceToolDiff } from '../src/renderer/src/workbuddy/conversation/SourceToolDiff'
|
|
|
|
const toolIconMarkup = (name: string) => renderToStaticMarkup(resolveSourceToolIcon(name) ?? null)
|
|
assert.match(toolIconMarkup('Read'), /M7\.7407 13\.66/)
|
|
assert.match(toolIconMarkup('read_file'), /M7\.7407 13\.66/)
|
|
assert.match(toolIconMarkup('PowerShell'), /M0\.1942 3\.7746/)
|
|
assert.match(toolIconMarkup('execute_command'), /M0\.1942 3\.7746/)
|
|
assert.match(toolIconMarkup('list_dir'), /M0\.1942 3\.7746/)
|
|
assert.equal(toolIconMarkup('Inspect'), toolIconMarkup('execute_command'))
|
|
assert.match(toolIconMarkup('Grep'), /M1\.2982 9\.8292/)
|
|
assert.match(toolIconMarkup('web_search'), /M1\.37 6\.6667/)
|
|
assert.match(toolIconMarkup('mcp_call_tool'), /M2\.9742 -0\.1497/)
|
|
assert.match(toolIconMarkup('mcp__browser__tabs_list'), /M6\.3745 2\.6414/)
|
|
assert.match(toolIconMarkup('mcp__computer-use__open_app'), /M6\.5 11\.0857/)
|
|
assert.match(renderToStaticMarkup(resolveSourceToolIcon('mcp__connector-proxy__pick_location', 'captured-icon') ?? null), /class="captured-icon"/)
|
|
assert.match(toolIconMarkup('mcp__workbuddy__skill_management'), /M4\.6063 4\.6987/)
|
|
const pluginToolAliases = [
|
|
'search_plugins', 'suggest_plugin_install', 'list_installed_plugins', 'uninstall_plugin', 'reconnect_plugin',
|
|
'mcp__workbuddy__search_plugins', 'mcp__workbuddy__suggest_plugin_install', 'mcp__workbuddy__list_installed_plugins', 'mcp__workbuddy__uninstall_plugin', 'mcp__workbuddy__reconnect_plugin',
|
|
'mcp__connector-proxy__search_plugins', 'mcp__connector-proxy__suggest_plugin_install', 'mcp__connector-proxy__list_installed_plugins', 'mcp__connector-proxy__uninstall_plugin', 'mcp__connector-proxy__reconnect_plugin'
|
|
]
|
|
for (const alias of pluginToolAliases) assert.match(toolIconMarkup(alias), /M5\.7784 2\.3425/)
|
|
assert.match(renderToStaticMarkup(resolveSourceToolIcon('mcp__connector-proxy__reconnect_plugin', 'plugin-icon') ?? null), /class="plugin-icon"/)
|
|
assert.match(toolIconMarkup('unregistered_tool'), /M2\.9742 -0\.1497/)
|
|
assert.match(toolIconMarkup('mcp__unknown__tool'), /M2\.9742 -0\.1497/)
|
|
|
|
const tool = {
|
|
id: 'tool-1',
|
|
name: 'execute_command',
|
|
status: 'cancelled' as const,
|
|
args: { command: 'echo test' },
|
|
result: { output: 'first line\nFINAL OUTPUT SENTINEL' }
|
|
}
|
|
|
|
const presentedFiles = renderToStaticMarkup(<ToolCallRenderer tool={{ id: 'present-files', name: 'present_files', status: 'success',
|
|
result: { result: '{"files":["result.html"]}' } }} />)
|
|
assert.match(presentedFiles, /cr-tool-compact-misc__pre/)
|
|
assert.match(presentedFiles, /result.html/)
|
|
assert.match(presentedFiles, /aria-expanded="false"/)
|
|
assert.doesNotMatch(presentedFiles, /cr-tool-call__data/)
|
|
const failedFiles = renderToStaticMarkup(<ToolCallRenderer tool={{ id: 'present-files', name: 'present_files', status: 'error',
|
|
result: { result: 'failed' } }} />)
|
|
assert.doesNotMatch(failedFiles, /aria-expanded=/)
|
|
|
|
const toolMarkup = renderToStaticMarkup(<ToolCallCard tool={tool} defaultExpanded />)
|
|
assert.match(toolMarkup, /execute_command/)
|
|
assert.match(toolMarkup, /Cancelled/)
|
|
assert.match(toolMarkup, /echo test/)
|
|
assert.match(toolMarkup, /FINAL OUTPUT SENTINEL/)
|
|
assert.match(renderToStaticMarkup(<ToolCallCard tool={{ ...tool, status: 'failed' }} />), /Failed/)
|
|
const completeOutput = 'Complete output that exceeds the summary. LAST OUTPUT SENTINEL'
|
|
const outputPreferred = renderToStaticMarkup(
|
|
<ToolCallCard
|
|
tool={{ id: 'output-1', name: 'custom', status: 'success', output: completeOutput, result: { output: 'short summary only' } }}
|
|
defaultExpanded
|
|
/>
|
|
)
|
|
assert.match(outputPreferred, /Complete output that exceeds the summary/)
|
|
assert.match(outputPreferred, /LAST OUTPUT SENTINEL/)
|
|
assert.doesNotMatch(outputPreferred, /short summary only/)
|
|
|
|
const collapsedGroup = renderToStaticMarkup(
|
|
<ToolGroup stats={{ toolCallCount: 2, messageCount: 1, errorCount: 1 }} summary={{ text: 'Running command' }}>
|
|
<span>group child</span>
|
|
</ToolGroup>
|
|
)
|
|
assert.match(collapsedGroup, /aria-expanded="false"/)
|
|
assert.match(collapsedGroup, /Running command/)
|
|
assert.match(collapsedGroup, /group child/, 'source collapse retains mounted children for height/opacity transitions')
|
|
assert.match(collapsedGroup, /aria-hidden="true" inert=""/)
|
|
|
|
const expandedGroup = renderToStaticMarkup(
|
|
<ToolGroup stats={{ toolCallCount: 2, messageCount: 1 }} defaultExpanded>
|
|
<span>group child</span>
|
|
</ToolGroup>
|
|
)
|
|
assert.match(expandedGroup, /Tools/)
|
|
assert.match(expandedGroup, /group child/)
|
|
assert.equal(computeToolGroupVisibleStart(20, undefined, 1), 5)
|
|
assert.equal(computeToolGroupVisibleStart(20, undefined, 2), 0)
|
|
|
|
const pagedGroup = renderToStaticMarkup(
|
|
<ToolGroup stats={{ toolCallCount: 20, messageCount: 0 }} defaultExpanded>
|
|
{Array.from({ length: 20 }, (_, index) => <span key={index}>{`item-${index}`}</span>)}
|
|
</ToolGroup>
|
|
)
|
|
assert.match(pagedGroup, /Show 5 earlier/)
|
|
assert.doesNotMatch(pagedGroup, /item-0</)
|
|
assert.match(pagedGroup, /item-19</)
|
|
const sourceSummaryGroup = renderToStaticMarkup(
|
|
<ToolGroup variant="summary" stats={{ toolCallCount: 20, messageCount: 20 }} summary={{ text: '运行命令' }} defaultExpanded>
|
|
{Array.from({ length: 20 }, (_, index) => <span key={index}>{`summary-item-${index}`}</span>)}
|
|
</ToolGroup>
|
|
)
|
|
assert.match(sourceSummaryGroup, /cr-collapse--summary/)
|
|
assert.match(sourceSummaryGroup, /content-inner--summary/)
|
|
assert.match(sourceSummaryGroup, /summary-item-0</)
|
|
assert.match(sourceSummaryGroup, /summary-item-19</)
|
|
assert.doesNotMatch(sourceSummaryGroup, /Show \d+ earlier/)
|
|
|
|
const permissionOptions = [
|
|
{ optionId: 'once', kind: 'allow_once', name: 'Allow once' },
|
|
{ optionId: 'always', kind: 'allow_always', name: 'Always allow' },
|
|
{ optionId: 'reject', kind: 'reject_once', name: 'Reject' }
|
|
]
|
|
assert.equal(getPermissionChoices(permissionOptions).primary?.optionId, 'once')
|
|
assert.equal(getPermissionChoices(permissionOptions).secondary?.optionId, 'always')
|
|
assert.equal(getPermissionChoices(permissionOptions, 'session').primary?.optionId, 'always')
|
|
|
|
const pendingPermission = renderToStaticMarkup(
|
|
<PermissionConfirm
|
|
pendingId="permission-1"
|
|
toolName="execute_command"
|
|
toolKind="computerUse"
|
|
options={permissionOptions}
|
|
resolve={() => undefined}
|
|
reject={() => undefined}
|
|
/>
|
|
)
|
|
assert.match(pendingPermission, /role="dialog"/)
|
|
assert.match(pendingPermission, /data-pending-id="permission-1"/)
|
|
assert.match(pendingPermission, /data-option-id="once"/)
|
|
assert.match(pendingPermission, /Always allow/)
|
|
assert.match(pendingPermission, /Reject/)
|
|
|
|
const answeredPermission = renderToStaticMarkup(
|
|
<PermissionConfirm
|
|
pendingId="permission-2"
|
|
toolName="image_gen"
|
|
options={permissionOptions}
|
|
status="answered"
|
|
resolve={() => undefined}
|
|
reject={() => undefined}
|
|
/>
|
|
)
|
|
assert.match(answeredPermission, /Approved/)
|
|
assert.doesNotMatch(answeredPermission, /data-option-id=/)
|
|
|
|
const respondingPermission = renderToStaticMarkup(
|
|
<PermissionConfirm
|
|
pendingId="permission-responding"
|
|
toolName="image_gen"
|
|
options={permissionOptions}
|
|
status="responding"
|
|
resolve={() => undefined}
|
|
reject={() => undefined}
|
|
/>
|
|
)
|
|
assert.match(respondingPermission, /Sending decision/)
|
|
assert.match(respondingPermission, /disabled=""/)
|
|
|
|
const failedPermission = renderToStaticMarkup(
|
|
<PermissionConfirm
|
|
pendingId="permission-error"
|
|
toolName="image_gen"
|
|
options={permissionOptions}
|
|
status="error"
|
|
error="Broker unavailable"
|
|
resolve={() => undefined}
|
|
reject={() => undefined}
|
|
/>
|
|
)
|
|
assert.match(failedPermission, /Broker unavailable/)
|
|
assert.match(failedPermission, /data-option-id="once"/)
|
|
|
|
const rejectedPermission = renderToStaticMarkup(
|
|
<PermissionConfirm
|
|
pendingId="permission-rejected"
|
|
toolName="image_gen"
|
|
options={permissionOptions}
|
|
status="rejected"
|
|
resolve={() => undefined}
|
|
reject={() => undefined}
|
|
/>
|
|
)
|
|
assert.match(rejectedPermission, /Rejected/)
|
|
|
|
const cancelledPermission = renderToStaticMarkup(
|
|
<PermissionConfirm
|
|
pendingId="permission-3"
|
|
toolName="image_gen"
|
|
options={permissionOptions}
|
|
status="cancelled"
|
|
resolve={() => undefined}
|
|
reject={() => undefined}
|
|
/>
|
|
)
|
|
assert.match(cancelledPermission, /Cancelled/)
|
|
|
|
const terminalApprovalMessage: ChatMessage = {
|
|
id: 'message-terminal-approval',
|
|
role: 'assistant',
|
|
content: 'Browser opened.',
|
|
parts: [{ type: 'text', text: 'Browser opened.' }],
|
|
status: 'done',
|
|
createdAt: 1,
|
|
interactions: [{
|
|
id: 'approval-terminal',
|
|
runId: 'run-terminal',
|
|
toolCallId: 'browser-tool',
|
|
title: 'Allow browser access?',
|
|
status: 'answered',
|
|
kind: 'approval',
|
|
requestId: 'request-terminal',
|
|
options: permissionOptions
|
|
}]
|
|
}
|
|
const terminalApprovalMarkup = renderToStaticMarkup(<MessageBubble message={terminalApprovalMessage} />)
|
|
assert.doesNotMatch(terminalApprovalMarkup, /conversation-high-credit-approval/,
|
|
'a completed non-inline approval must not leave a terminal card in the conversation')
|
|
|
|
const activeApprovalMarkup = renderToStaticMarkup(<MessageBubble message={{
|
|
...terminalApprovalMessage,
|
|
status: 'streaming',
|
|
interactions: terminalApprovalMessage.interactions?.map(interaction => ({ ...interaction, status: 'pending' as const }))
|
|
}} />)
|
|
assert.doesNotMatch(activeApprovalMarkup, /conversation-high-credit-approval/,
|
|
'an active approval belongs to the composer floating slot, not the conversation stream')
|
|
|
|
const questionConfig = {
|
|
title: 'Choose a runtime',
|
|
questions: [
|
|
{
|
|
id: 'runtime',
|
|
question: 'Which runtime?',
|
|
options: [
|
|
{ id: 'node', question: 'Node.js', description: 'JavaScript runtime' },
|
|
{ id: 'deno', question: 'Deno' }
|
|
]
|
|
},
|
|
{
|
|
id: 'features',
|
|
question: 'Select features',
|
|
multiSelect: true,
|
|
options: ['Tests', 'Lint']
|
|
}
|
|
]
|
|
}
|
|
const activeQuestionMessage: ChatMessage = {
|
|
id: 'message-active-question',
|
|
role: 'assistant',
|
|
content: '',
|
|
tools: [{
|
|
id: 'question-tool',
|
|
name: 'AskUserQuestion',
|
|
args: { questions: questionConfig.questions },
|
|
argsRaw: '{}',
|
|
status: 'running'
|
|
}],
|
|
parts: [{ type: 'tool', toolId: 'question-tool' }],
|
|
status: 'streaming',
|
|
createdAt: 2,
|
|
interactions: [{
|
|
id: 'question-active',
|
|
runId: 'run-question',
|
|
toolCallId: 'question-tool',
|
|
title: 'Need input',
|
|
status: 'pending',
|
|
kind: 'question',
|
|
questions: questionConfig.questions.map(question => ({ id: question.id, question: question.question, options: question.options }))
|
|
}]
|
|
}
|
|
const activeQuestionMarkup = renderToStaticMarkup(<MessageBubble message={activeQuestionMessage} />)
|
|
assert.doesNotMatch(activeQuestionMarkup, /ask-user-question/,
|
|
'an active user question must render only in the composer floating slot')
|
|
|
|
const questionMarkup = renderToStaticMarkup(<QuestionFloating config={questionConfig} />)
|
|
assert.match(questionMarkup, /Which runtime\?/)
|
|
assert.match(questionMarkup, /Node\.js/)
|
|
assert.match(questionMarkup, /JavaScript runtime/)
|
|
assert.match(questionMarkup, /1\/2/)
|
|
assert.match(questionMarkup, /Add your own answer/)
|
|
assert.match(questionMarkup, /role="button" aria-pressed="true"/)
|
|
|
|
const answer = buildQuestionAnswerPayload(
|
|
questionConfig.questions,
|
|
{ runtime: ['node'], features: ['Tests'] },
|
|
{ runtime: '', features: 'Custom feature' },
|
|
{ features: true },
|
|
questionConfig.title
|
|
)
|
|
assert.deepEqual(answer, {
|
|
title: 'Choose a runtime',
|
|
questions: [
|
|
{ id: 'runtime', question: 'Which runtime?', answers: ['node'] },
|
|
{ id: 'features', question: 'Select features', answers: ['Tests', 'Custom feature'] }
|
|
]
|
|
})
|
|
|
|
const pendingQuestion = renderToStaticMarkup(
|
|
<AskUserQuestion tool={{ name: 'AskUserQuestion', status: 'pending' }} />
|
|
)
|
|
assert.match(pendingQuestion, /Waiting for your answer/)
|
|
|
|
const completedQuestion = renderToStaticMarkup(
|
|
<AskUserQuestion
|
|
tool={{
|
|
name: 'AskUserQuestion',
|
|
status: 'executed',
|
|
metaData: { questionAnswer: { questions: [{ id: 'q1', question: 'Where?', answers: ['Here'] }] } }
|
|
}}
|
|
contentSettled={false}
|
|
/>
|
|
)
|
|
assert.match(completedQuestion, /Question for you/)
|
|
assert.match(completedQuestion, /Where\?/)
|
|
assert.match(completedQuestion, /Here/)
|
|
|
|
const cancelledQuestion = renderToStaticMarkup(
|
|
<AskUserQuestion tool={{ name: 'AskUserQuestion', status: 'cancelled' }} />
|
|
)
|
|
assert.match(cancelledQuestion, /Question cancelled/)
|
|
|
|
const failedQuestion = renderToStaticMarkup(
|
|
<AskUserQuestion tool={{ name: 'AskUserQuestion', status: 'failed' }} />
|
|
)
|
|
assert.match(failedQuestion, /Question failed/)
|
|
|
|
const skippedQuestion = renderToStaticMarkup(
|
|
<AskUserQuestion tool={{ name: 'AskUserQuestion', status: 'executed', metadata: { questionSkipped: true } }} />
|
|
)
|
|
assert.match(skippedQuestion, /Question skipped/)
|
|
|
|
const transcript = renderToStaticMarkup(
|
|
<QuestionAnswerTranscript questionAnswer={{ questions: [{ id: 'q2', question: 'Skipped?', answers: [] }] }} />
|
|
)
|
|
assert.match(transcript, /\[Skipped\]/)
|
|
|
|
const collapsedThinking = renderToStaticMarkup(<ThinkingBlock text="Hidden reasoning" isFinished />)
|
|
// The source collapse keeps its content mounted for CSS transitions; it must be inaccessible while closed.
|
|
assert.match(collapsedThinking, /cr-collapse--collapsed/)
|
|
assert.match(collapsedThinking, /class="cr-collapse__content" aria-hidden="true" inert=""/)
|
|
const expandedThinking = renderToStaticMarkup(<ThinkingBlock text="Visible reasoning" isFinished isLastContent renderMarkdown={text => <strong>{text}</strong>} />)
|
|
assert.match(expandedThinking, /Visible reasoning/)
|
|
assert.match(expandedThinking, /<strong>Visible reasoning<\/strong>/)
|
|
assert.doesNotMatch(expandedThinking, /cr-collapse--collapsed/)
|
|
|
|
const planTasks = Array.from({ length: 8 }, (_, index) => ({
|
|
id: `task-${index}`,
|
|
content: `Plan task ${index}`,
|
|
status: index < 5 ? 'completed' : index === 5 ? 'in_progress' : 'pending'
|
|
}))
|
|
assert.deepEqual(selectPlanTasks(planTasks, 3), {
|
|
visible: planTasks.slice(4, 7), hiddenCount: 5, currentIndex: 5, startIndex: 4
|
|
})
|
|
const successfulPlan = renderToStaticMarkup(
|
|
<PlanCreateBlock data={{ id: 'plan-1', name: 'Release plan', overview: 'Ship the release', todolist: planTasks }} status="success" maxDisplayTasks={3} onViewPlan={() => undefined} onDownloadPlan={() => undefined} />
|
|
)
|
|
assert.match(successfulPlan, /Release plan/)
|
|
assert.match(successfulPlan, /Plan task 5/)
|
|
assert.doesNotMatch(successfulPlan, /Current/, 'the captured plan list has no added Current label')
|
|
assert.match(successfulPlan, /\+ 5 更多/)
|
|
assert.match(successfulPlan, /Download plan/)
|
|
assert.doesNotMatch(successfulPlan, /Plan task 0/)
|
|
const failedPlan = renderToStaticMarkup(
|
|
<PlanCreateBlock data={{ id: 'plan-2', name: 'Broken plan' }} status="error" />
|
|
)
|
|
assert.match(failedPlan, /Failed/)
|
|
const activePlanUpdate = renderToStaticMarkup(
|
|
<PlanUpdateBlock data={{ name: 'Release plan', status: 'building' }} status="running" />
|
|
)
|
|
assert.match(activePlanUpdate, /Release plan confirmed/)
|
|
assert.match(activePlanUpdate, /cb-plan-update-spinner/)
|
|
|
|
const cancelledTask = renderToStaticMarkup(
|
|
<TaskBlock data={{ description: 'Run checks', subagentName: 'builder', finalResult: 'Stopped output', toolInfo: [{ name: 'execute_command', needApprove: true, toolCallId: 'inner-tool' }] }} status="cancelled" />
|
|
)
|
|
assert.match(cancelledTask, /任务 builder 子代理: Run checks/)
|
|
assert.match(cancelledTask, /任务执行已取消!/)
|
|
assert.match(cancelledTask, /data-status="cancelled"/)
|
|
|
|
assert.equal(getCapturedToolKind('Bash'), 'command')
|
|
assert.equal(getCapturedToolKind('bash'), 'command')
|
|
assert.equal(getCapturedToolKind('glob'), 'file-list')
|
|
assert.equal(getCapturedToolKind('grep'), 'file-list')
|
|
assert.equal(getCapturedToolKind('read'), 'read-file')
|
|
assert.equal(getCapturedToolKind('NotebookEdit'), 'write-file')
|
|
assert.equal(getCapturedToolKind('Grep'), 'file-list')
|
|
assert.equal(getCapturedToolKind('WebSearch'), 'web-search')
|
|
assert.equal(getCapturedToolKind('todo_write'), 'todo-write')
|
|
assert.equal(getCapturedToolKind('TodoWrite'), 'todo-write')
|
|
assert.equal(getCapturedToolKind('mcp__ardot__create_design'), 'mcp-display')
|
|
assert.equal(getCapturedToolKind('mcp__browser__tab_goto'), 'mcp-display')
|
|
assert.equal(getCapturedToolKind('mcp__custom__unknown'), undefined)
|
|
assert.equal(getCapturedToolKind('custom_tool'), undefined)
|
|
const commandRenderer = renderToStaticMarkup(
|
|
<ToolCallRenderer tool={{ id: 'cmd-1', name: 'Bash', status: 'success', args: { command: 'npm test' }, result: { result: { stdout: 'all checks passed', exitCode: 0 } } }} defaultExpanded />
|
|
)
|
|
assert.match(commandRenderer, /npm test/)
|
|
assert.match(commandRenderer, /all checks passed/)
|
|
assert.match(commandRenderer, /cr-tool-exec__output/)
|
|
assert.match(commandRenderer, /cr-tool-head__primary/)
|
|
assert.doesNotMatch(commandRenderer, /Exit code 0/)
|
|
assert.doesNotMatch(commandRenderer, /Full output/)
|
|
assert.doesNotMatch(commandRenderer, /Arguments/)
|
|
assert.equal(getCapturedToolKind('powershell'), 'command')
|
|
const runningCommandRenderer = renderToStaticMarkup(
|
|
<ToolCallRenderer tool={{ id: 'cmd-running', name: 'bash', status: 'running', args: {
|
|
command: 'ls src', description: 'List contents of src directory'
|
|
}, output: 'first output snapshot\nsecond output snapshot' }} defaultExpanded />
|
|
)
|
|
assert.match(runningCommandRenderer, /List contents of src directory/)
|
|
assert.match(runningCommandRenderer, /aria-expanded="false"/,
|
|
'a running command stays on its one-line summary even when the parent requests expanded details')
|
|
assert.match(runningCommandRenderer, /cr-tool-exp__content-shell" aria-hidden="true" inert=""/)
|
|
assert.equal((runningCommandRenderer.match(/class="cr-tool-exec__output"/g) ?? []).length, 1,
|
|
'the command output remains one replaceable details region')
|
|
assert.match(runningCommandRenderer, /first output snapshot\nsecond output snapshot/)
|
|
const runningGlobRenderer = renderToStaticMarkup(
|
|
<ToolCallRenderer tool={{ id: 'glob-running', name: 'glob', status: 'running', intent: 'Inspecting source layout', args: { path: 'src' }, output: 'src/index.ts\nsrc/main.ts' }} />
|
|
)
|
|
assert.match(runningGlobRenderer, /cr-tool-list-files__header/)
|
|
assert.match(runningGlobRenderer, /搜索中/)
|
|
assert.match(runningGlobRenderer, /title="src">Inspecting source layout<\/span>/)
|
|
assert.match(runningGlobRenderer, /cr-tool-head__secondary">src<\/span>/)
|
|
assert.match(runningGlobRenderer, /aria-expanded="false"/)
|
|
assert.match(runningGlobRenderer, /src\/index\.ts\nsrc\/main\.ts/)
|
|
const inferredRunningListRenderer = renderToStaticMarkup(
|
|
<ToolCallRenderer tool={{ id: 'list-no-intent', name: 'list_dir', status: 'running', args: { path: 'src' }, output: 'index.ts\nmain.ts' }} />
|
|
)
|
|
assert.match(inferredRunningListRenderer, /List contents of src directory/,
|
|
'a list_dir call without provider intent still gets a descriptive one-line action title')
|
|
assert.doesNotMatch(inferredRunningListRenderer, /cr-tool-head__secondary/,
|
|
'the inferred action title does not repeat the directory as secondary text')
|
|
assert.match(inferredRunningListRenderer, /aria-expanded="false"/,
|
|
'a running directory listing remains one collapsed, replaceable row')
|
|
const inferredRootListRenderer = renderToStaticMarkup(
|
|
<ToolCallRenderer tool={{ id: 'list-root-no-path', name: 'list_dir', status: 'success', args: {}, result: { files: [] } }} />
|
|
)
|
|
assert.match(inferredRootListRenderer, /List contents of the project directory/,
|
|
'an omitted list_dir path resolves to the documented workspace root')
|
|
const runningCommandWithIntent = renderToStaticMarkup(
|
|
<ToolCallRenderer tool={{ id: 'cmd-intent', name: 'bash', status: 'running', intent: 'List contents of src directory', args: { command: 'ls src' }, output: 'file-a.ts\nfile-b.ts' }} />
|
|
)
|
|
assert.match(runningCommandWithIntent, /List contents of src directory/)
|
|
assert.match(runningCommandWithIntent, /aria-expanded="false"/)
|
|
const emptySuccessfulCommand = renderToStaticMarkup(
|
|
<ToolCallRenderer tool={{ id: 'cmd-empty', name: 'Bash', status: 'success', args: { command: 'true' }, result: { stdout: '(empty)', stderr: '(empty)', exitCode: 0, message: 'Operation completed' } }} defaultExpanded />
|
|
)
|
|
assert.doesNotMatch(emptySuccessfulCommand, /cr-tool-exec__output/)
|
|
assert.doesNotMatch(emptySuccessfulCommand, /Command failed/)
|
|
assert.match(emptySuccessfulCommand, /cr-tool-exec__ok/)
|
|
assert.match(emptySuccessfulCommand, /运行成功/)
|
|
const commandOutputFallback = renderToStaticMarkup(
|
|
<ToolCallRenderer tool={{ id: 'cmd-output', name: 'Bash', status: 'success', args: { command: 'run task' }, output: completeOutput, result: { success: true, exitCode: 0 } }} defaultExpanded />
|
|
)
|
|
assert.match(commandOutputFallback, /LAST OUTPUT SENTINEL/)
|
|
assert.doesNotMatch(commandOutputFallback, /"success": true/)
|
|
assert.doesNotMatch(commandOutputFallback, /Full output/)
|
|
const failedCommandRenderer = renderToStaticMarkup(
|
|
<ToolCallRenderer tool={{
|
|
id: 'cmd-failed', name: 'Bash', status: 'failed', args: { command: 'run failing task' },
|
|
result: { result: { stdout: 'started', stderr: 'operation rejected', exitCode: 7 } }
|
|
}} />
|
|
)
|
|
assert.match(failedCommandRenderer, /started/)
|
|
assert.match(failedCommandRenderer, /operation rejected/)
|
|
assert.match(failedCommandRenderer, /执行失败 \(退出码: 7\)/)
|
|
assert.doesNotMatch(failedCommandRenderer, /Exit code 7/)
|
|
const readRenderer = renderToStaticMarkup(
|
|
<ToolCallRenderer tool={{ id: 'read-1', name: 'Read', status: 'executed', args: { file_path: 'src/main.ts' }, result: { content: 'line three\nline four' } }} defaultExpanded />
|
|
)
|
|
assert.match(readRenderer, /src\/main\.ts/)
|
|
assert.match(readRenderer, /class="cr-tool-head__status">已读取<\/span>/)
|
|
assert.match(readRenderer, /cr-tool-head__icon[^>]*><svg/)
|
|
assert.doesNotMatch(readRenderer, /line three/, 'source read card is header only; content remains in the file-open adapter')
|
|
const readIntentRenderer = renderToStaticMarkup(
|
|
<ToolCallRenderer tool={{ id: 'read-intent', name: 'read_file', status: 'running', intent: 'Reading project handover status', args: { path: 'docs/handover.md' } }} />
|
|
)
|
|
assert.match(readIntentRenderer, /Reading project handover status/)
|
|
assert.match(readIntentRenderer, /handover\.md/)
|
|
const inferredReadRenderer = renderToStaticMarkup(
|
|
<ToolCallRenderer tool={{ id: 'read-no-intent', name: 'read_file', status: 'success', args: { path: 'docs/README.md', offset: 10, limit: 5 }, result: { content: 'contents' } }} />
|
|
)
|
|
assert.match(inferredReadRenderer, /Read file docs\/README\.md \(lines 10-14\)/,
|
|
'a read_file call without provider intent still gets a descriptive action title')
|
|
const readOutputFallback = renderToStaticMarkup(
|
|
<ToolCallRenderer tool={{ id: 'read-output', name: 'Read', status: 'executed', args: { file_path: 'src/result.ts' }, result: { success: true }, output: 'complete file output sentinel' }} defaultExpanded />
|
|
)
|
|
assert.doesNotMatch(readOutputFallback, /complete file output sentinel/)
|
|
const openedReadTool = {
|
|
id: 'read-open', name: 'Read', status: 'executed' as const,
|
|
args: { file_path: 'src/open.ts' },
|
|
result: [{ type: 'text', text: 'line four\nline five' }]
|
|
}
|
|
assert.deepEqual(createReadFileOpenRequest(openedReadTool), {
|
|
toolCallId: 'read-open', toolName: 'Read', filePath: 'src/open.ts', startLine: undefined, endLine: undefined, content: 'line four\nline five'
|
|
})
|
|
const numberedReadTool = {
|
|
id: 'read-numbered-open', name: 'Read', status: 'executed' as const,
|
|
args: { file_path: 'src/numbered.ts' }, result: [{ type: 'text', text: '1→line one\n2→line two\n' }]
|
|
}
|
|
assert.equal(createReadFileOpenRequest(numberedReadTool).content, 'line one\nline two')
|
|
const partialReadTool = {
|
|
id: 'read-partial-open', name: 'Read', status: 'executed' as const,
|
|
args: { file_path: 'src/partial.ts', startLine: 10, endLine: 12 }, result: [{ type: 'text', text: 'partial chunk' }]
|
|
}
|
|
assert.equal(convertReadFile(partialReadTool).content, undefined)
|
|
assert.deepEqual(createReadFileOpenRequest(partialReadTool), {
|
|
toolCallId: 'read-partial-open', toolName: 'Read', filePath: 'src/partial.ts', startLine: 10, endLine: 12, content: undefined
|
|
})
|
|
const missingReadPathRenderer = renderToStaticMarkup(
|
|
<ToolCallRenderer tool={{ id: 'read-missing-path', name: 'Read', status: 'executed', args: {}, result: [{ type: 'text', text: 'read output without a path' }] }} />
|
|
)
|
|
assert.match(missingReadPathRenderer, /已读取/)
|
|
assert.doesNotMatch(missingReadPathRenderer, /read output without a path/)
|
|
const failedReadRenderer = renderToStaticMarkup(
|
|
<ToolCallRenderer tool={{ id: 'read-failed', name: 'Read', status: 'failed', args: { file_path: 'missing.ts' }, result: { result: { error: 'ENOENT: missing.ts' } } }} />
|
|
)
|
|
assert.match(failedReadRenderer, /cr-tool-head__icon--error/)
|
|
assert.doesNotMatch(failedReadRenderer, /ENOENT: missing\.ts/)
|
|
const numberedReadRenderer = renderToStaticMarkup(
|
|
<ToolCallRenderer tool={{ id: 'read-2', name: 'read_file', status: 'executed', args: {}, result: [{ type: 'text', text: 'line ' }, { type: 'text', text: 'joined' }] }} defaultExpanded />
|
|
)
|
|
assert.doesNotMatch(numberedReadRenderer, /line joined/)
|
|
assert.doesNotMatch(numberedReadRenderer, /line \njoined/)
|
|
const partialReadRenderer = renderToStaticMarkup(
|
|
<ToolCallRenderer tool={{ id: 'read-3', name: 'Read', status: 'executed', args: { offset: 10 }, result: [{ type: 'text', text: 'partial chunk' }] }} defaultExpanded />
|
|
)
|
|
assert.doesNotMatch(partialReadRenderer, /partial chunk/)
|
|
const todoWriteRenderer = renderToStaticMarkup(
|
|
<ToolCallRenderer
|
|
tool={{
|
|
id: 'todo-write-1', name: 'todo_write', status: 'executed', isLastContent: true,
|
|
args: { _rawInput: { merge: true, totalCount: 3, todos: [
|
|
{ id: 'done', content: 'Completed task', status: 'completed' },
|
|
{ id: 'active', content: 'Original active task', activeForm: 'Writing the active task', status: 'in_progress' },
|
|
{ id: 'pending', content: 'Pending task', status: 'pending' }
|
|
] } }
|
|
}}
|
|
/>
|
|
)
|
|
assert.match(todoWriteRenderer, /进度更新 1\/3/)
|
|
assert.match(todoWriteRenderer, /Completed task/)
|
|
assert.match(todoWriteRenderer, /Writing the active task/)
|
|
assert.match(todoWriteRenderer, /Pending task/)
|
|
assert.match(todoWriteRenderer, /cb-plan-task--completed/)
|
|
assert.match(todoWriteRenderer, /cb-plan-task--in_progress/)
|
|
assert.match(todoWriteRenderer, /cb-plan-task--pending/)
|
|
const todoStepsRenderer = renderToStaticMarkup(
|
|
<ToolCallRenderer tool={{ id: 'todo-write-steps', name: 'TodoWrite', status: 'running', args: { steps: [{ title: 'Compatibility step', status: 'doing' }] } }} defaultExpanded />
|
|
)
|
|
assert.match(todoStepsRenderer, /Compatibility step/)
|
|
assert.match(todoStepsRenderer, /cb-plan-task--in_progress/)
|
|
const fileListRenderer = renderToStaticMarkup(
|
|
<ToolCallRenderer tool={{ id: 'files-1', name: 'Glob', status: 'success', args: { pattern: 'src/**/*.ts' }, result: { result: { results: [{ path: 'src/main.ts' }, { path: 'src/util.ts' }] } } }} defaultExpanded />
|
|
)
|
|
assert.match(fileListRenderer, /src\/main\.ts/)
|
|
assert.match(fileListRenderer, /src\/util\.ts/)
|
|
assert.match(fileListRenderer, /搜索文件/)
|
|
assert.match(fileListRenderer, /M1\.2982 9\.8292/)
|
|
const listFileRequest = createListFileOpenRequest(
|
|
{ id: 'files-open', name: 'Grep', status: 'success' },
|
|
{ filePath: 'src/main.ts', fileName: 'main.ts', isDirectory: false, startLine: 12, endLine: 13, content: 'matched text' }
|
|
)
|
|
assert.deepEqual(listFileRequest, { toolCallId: 'files-open', toolName: 'Grep', filePath: 'src/main.ts', startLine: 12, endLine: 13 })
|
|
const markdownWebSearchRenderer = renderToStaticMarkup(
|
|
<ToolCallRenderer tool={{
|
|
id: 'web-1', name: 'WebSearch', status: 'success', args: {},
|
|
result: '# Search Results for "captured query"\n\n## 1. [First result](https://example.com/one)\n\n## 2. [Second result](https://example.com/two)'
|
|
}} defaultExpanded />
|
|
)
|
|
assert.match(markdownWebSearchRenderer, /captured query/)
|
|
assert.match(markdownWebSearchRenderer, /First result/)
|
|
assert.match(markdownWebSearchRenderer, /Second result/)
|
|
const structuredWebSearchRenderer = renderToStaticMarkup(
|
|
<ToolCallRenderer tool={{
|
|
id: 'web-2', name: 'web_search', status: 'success', args: { query: 'fallback query' }, result: { results: [{ title: 'Structured result', url: 'https://example.org' }] },
|
|
metaData: { _meta: { 'codebuddy.ai': { rawResponse: { type: 'web_search_tool_result', query: 'structured query', results: [{ title: 'Raw response result', url: 'https://example.net' }] } } } }
|
|
}} defaultExpanded />
|
|
)
|
|
assert.match(structuredWebSearchRenderer, /structured query/)
|
|
assert.match(structuredWebSearchRenderer, /Raw response result/)
|
|
assert.match(structuredWebSearchRenderer, /网页搜索/)
|
|
assert.match(structuredWebSearchRenderer, /M1\.37 6\.6667/)
|
|
assert.doesNotMatch(structuredWebSearchRenderer, /Structured result/)
|
|
const webFetchRenderer = renderToStaticMarkup(
|
|
<ToolCallRenderer tool={{
|
|
id: 'web-fetch-icon', name: 'web_fetch', status: 'success', args: { url: 'https://example.org/page' },
|
|
result: { result: { title: 'Example page', content: 'Fetched body' } }
|
|
}} />
|
|
)
|
|
assert.match(webFetchRenderer, /网页获取/)
|
|
assert.match(webFetchRenderer, /M1\.37 6\.6667/)
|
|
const mcpCallRenderer = renderToStaticMarkup(
|
|
<ToolCallRenderer tool={{
|
|
id: 'mcp-call-1', name: 'mcp_call_tool', status: 'success',
|
|
args: { serverName: 'browser', toolName: 'navigate', arguments: { url: 'https://example.com' } },
|
|
result: { result: { data: [{ type: 'text', text: 'navigation completed' }] } }
|
|
}} />
|
|
)
|
|
assert.match(mcpCallRenderer, /browser \(navigate\)/)
|
|
assert.match(mcpCallRenderer, /cr-tool-mcp__body/)
|
|
assert.match(mcpCallRenderer, /https:\/\/example\.com/)
|
|
assert.match(mcpCallRenderer, /navigation completed/)
|
|
assert.match(mcpCallRenderer, /运行成功/)
|
|
assert.match(mcpCallRenderer, /M2\.9742 -0\.1497/)
|
|
const mcpErrorRenderer = renderToStaticMarkup(
|
|
<ToolCallRenderer tool={{ id: 'mcp-call-error', name: 'mcp_call_tool', status: 'failed', result: { result: { error: 'server unavailable', isError: true } } }} />
|
|
)
|
|
assert.match(mcpErrorRenderer, /server unavailable/)
|
|
assert.match(mcpErrorRenderer, /cr-tool-head__icon--error/)
|
|
const mcpDisplayRenderer = renderToStaticMarkup(
|
|
<ToolCallRenderer tool={{
|
|
id: 'mcp-display-1', name: 'mcp__ardot__create_design', status: 'success', args: { name: 'Landing page' },
|
|
result: { result: { data: [{ type: 'text', text: 'design created' }] } }
|
|
}} />
|
|
)
|
|
assert.match(mcpDisplayRenderer, /cr-tool-display-list/)
|
|
assert.match(mcpDisplayRenderer, /create design/)
|
|
assert.match(mcpDisplayRenderer, /Landing page/)
|
|
assert.match(mcpDisplayRenderer, /design created/)
|
|
assert.match(mcpDisplayRenderer, /M2\.9742 -0\.1497/)
|
|
const editRenderer = renderToStaticMarkup(
|
|
<ToolCallRenderer tool={{ id: 'edit-1', name: 'Edit', status: 'success', args: { filePath: 'src/main.ts', edits: [{ old_str: 'before', new_str: 'after' }] } }} defaultExpanded onOpenFile={() => undefined} />
|
|
)
|
|
assert.match(editRenderer, /before/)
|
|
assert.match(editRenderer, /after/)
|
|
assert.match(editRenderer, /class="cr-tool-head__status">编辑<\/span>/)
|
|
assert.match(editRenderer, /1 处修改/)
|
|
assert.match(editRenderer, /role="link"/)
|
|
const replacementRenderer = renderToStaticMarkup(
|
|
<ToolCallRenderer tool={{ id: 'replace-1', name: 'replace_in_file', status: 'success', args: { filePath: 'src/main.ts', old_str: 'before', new_str: 'after' } }} defaultExpanded />
|
|
)
|
|
assert.match(replacementRenderer, /\+1/)
|
|
assert.match(replacementRenderer, /-1/)
|
|
const streamedWriteContent = Array.from({ length: 502 }, (_, index) => `line-${index}`).join('\n')
|
|
const streamedWriteRenderer = renderToStaticMarkup(
|
|
<ToolCallRenderer tool={{ id: 'write-stream-1', name: 'replace_in_file', status: 'running', isLastContent: true, args: { filePath: 'src/new.ts', content: streamedWriteContent } }} />
|
|
)
|
|
assert.match(streamedWriteRenderer, /修改中/)
|
|
assert.match(streamedWriteRenderer, /line-499/)
|
|
assert.match(streamedWriteRenderer, /… 省略 2 行/)
|
|
assert.doesNotMatch(streamedWriteRenderer, /line-501/)
|
|
assert.doesNotMatch(streamedWriteRenderer, /cr-tool-write__content--add/)
|
|
const creatingRenderer = renderToStaticMarkup(
|
|
<ToolCallRenderer tool={{ id: 'write-create-1', name: 'write_file', status: 'running', args: { filePath: 'src/created.ts', content: 'new content' } }} />
|
|
)
|
|
assert.match(creatingRenderer, /生成中/)
|
|
assert.doesNotMatch(creatingRenderer, /cr-tool-write__content--add/)
|
|
const createdRenderer = renderToStaticMarkup(
|
|
<ToolCallRenderer tool={{ id: 'write-create-done', name: 'write_file', status: 'success', args: { filePath: 'src/created.ts', content: 'new content' } }} defaultExpanded />
|
|
)
|
|
assert.match(createdRenderer, /cr-tool-write__content--add/)
|
|
const successfulWriteWithOutput = renderToStaticMarkup(
|
|
<ToolCallRenderer tool={{
|
|
id: 'write-success-output', name: 'write_file', status: 'success',
|
|
args: { filePath: 'src/success.ts', content: 'saved content' },
|
|
result: { result: { newContent: 'saved content' } }, output: '{"success":true}'
|
|
}} defaultExpanded />
|
|
)
|
|
assert.doesNotMatch(successfulWriteWithOutput, /role="alert"/)
|
|
assert.doesNotMatch(successfulWriteWithOutput, /\{"success":true\}/)
|
|
const failedWriteWithOutput = renderToStaticMarkup(
|
|
<ToolCallRenderer tool={{ id: 'write-failure-output', name: 'write_file', status: 'failed', output: 'write failed' }} />
|
|
)
|
|
assert.doesNotMatch(failedWriteWithOutput, /role="alert"/)
|
|
assert.doesNotMatch(failedWriteWithOutput, /write failed/)
|
|
assert.match(failedWriteWithOutput, /cr-tool-head__icon--error/)
|
|
const virtualWriteDiff = renderToStaticMarkup(
|
|
<SourceToolDiff oldCode="" newCode={Array.from({ length: 100 }, (_, index) => `line-${index}`).join('\n')} fileName="src/new.ts" />
|
|
)
|
|
assert.match(virtualWriteDiff, /cr-tool-diff--virtual/)
|
|
assert.ok((virtualWriteDiff.match(/cr-tool-diff__virtual-line/g) ?? []).length < 100)
|
|
const planRenderer = renderToStaticMarkup(
|
|
<ToolCallRenderer
|
|
tool={{
|
|
id: 'plan-3',
|
|
name: 'plan_create',
|
|
status: 'success',
|
|
result: {
|
|
result: {
|
|
data: JSON.stringify({
|
|
id: 'plan-3',
|
|
name: 'Created by tool',
|
|
status: 'prepare',
|
|
overview: 'Plan overview',
|
|
todolist: [{ id: 1, content: 'First task', status: 'completed' }]
|
|
})
|
|
}
|
|
}
|
|
}}
|
|
/>
|
|
)
|
|
assert.match(planRenderer, /Created by tool/)
|
|
assert.match(planRenderer, /First task/)
|
|
assert.doesNotMatch(planRenderer, /查看计划/)
|
|
const actionablePlan = renderToStaticMarkup(
|
|
<ToolCallRenderer
|
|
tool={{ id: 'plan-actions', name: 'plan_create', status: 'success', result: { result: { data: { id: 'plan-actions', name: 'Action plan', status: 'building', todolist: [{ id: 'todo-1', content: 'Clickable task', status: 'in_progress' }] } } } }}
|
|
onPlanTaskClick={() => undefined}
|
|
onPlanView={() => undefined}
|
|
onPlanBuild={() => undefined}
|
|
/>
|
|
)
|
|
assert.match(actionablePlan, /role="button"/)
|
|
assert.match(actionablePlan, /查看计划/)
|
|
assert.match(actionablePlan, />执行</)
|
|
const exitPlanRenderer = renderToStaticMarkup(
|
|
<ToolCallRenderer
|
|
tool={{
|
|
id: 'exit-plan-1',
|
|
name: 'exit_plan_mode',
|
|
status: 'pending',
|
|
isFinished: false,
|
|
metaData: { acp_request_permission: { planInfo: { plan: '# Release plan\nShip safely', planFilePath: 'plan.md' } } }
|
|
}}
|
|
onOpenFile={() => undefined}
|
|
/>
|
|
)
|
|
assert.match(exitPlanRenderer, /role="button"/)
|
|
assert.match(exitPlanRenderer, /Release plan/)
|
|
assert.match(exitPlanRenderer, /Ship safely/)
|
|
const fallbackRenderer = renderToStaticMarkup(
|
|
<ToolCallRenderer tool={{ id: 'custom-1', name: 'unregistered_tool', status: 'failed', args: { value: 'kept' } }} defaultExpanded />
|
|
)
|
|
assert.match(fallbackRenderer, /unregistered_tool/)
|
|
assert.match(fallbackRenderer, /kept/)
|
|
|
|
const sharedToolHeader = renderToStaticMarkup(
|
|
<SourceToolHeader primaryContent="Shared header" statusText="Running" isExecuting collapsible expanded onToggle={() => undefined} />
|
|
)
|
|
assert.match(sharedToolHeader, /class="cr-tool-head cr-tool-head--collapsible cr-tool-head--expanded"/)
|
|
assert.match(sharedToolHeader, /cr-tool-head__status--loading/)
|
|
assert.match(sharedToolHeader, /aria-expanded="true"/)
|
|
|
|
console.log('Conversation specialized renderer SSR regression checks passed')
|
|
|