import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { createCattyToolsFromCatalog, resolveSessionQueueKeyForTests, withCattyToolContext, } from './capabilityTools'; import { ToolOutputStore } from './toolOutputStore'; import { buildTerminalWriteFingerprint, ToolResultDedup } from './toolResultDedup'; import { collectPreservedTerminalWriteFingerprints } from './turnDrivers/cattyMessageBuilder'; describe('capabilityTools session queue keys', () => { it('does not queue read-only harness tools behind terminal session writes', () => { const key = resolveSessionQueueKeyForTests( { capabilityId: 'harness.workspace.get_session_info', toolName: 'workspace_get_session_info', policy: { write: false, bypassesApproval: true }, }, { sessionId: 'session-a' }, 'chat-1', ); assert.equal(key, null); }); it('still serializes terminal.execute on the same session', () => { const key = resolveSessionQueueKeyForTests( { capabilityId: 'terminal.execute', toolName: 'terminal_execute', policy: { write: true, bypassesApproval: false }, }, { sessionId: 'session-a', command: 'ls' }, 'chat-1', ); assert.equal(key, 'chat-1:session-a'); }); }); describe('capabilityTools result fitting', () => { it('bounds failed terminal output and stores the original partial output behind a handle', async () => { const store = new ToolOutputStore(); const partialOutput = `${'build output\n'.repeat(20_000)}FATAL_MID=E_CONN_RESET_7319`; const longError = `API_TOKEN=tok_live_1234567890 ${'diagnostic '.repeat(2_000)}`; const { tools, toolsContext } = createCattyToolsFromCatalog( { aiExec: async () => ({ ok: false, error: longError, stdout: partialOutput, stderr: '', exitCode: -1, }), }, { sessions: [{ sessionId: 'session-1', hostId: 'host-1', hostname: 'prod.internal', label: 'prod', protocol: 'ssh', connected: true, }], }, [], 'auto', undefined, 'chat-1', store, ); const result = await withCattyToolContext( tools.terminal_execute, toolsContext.terminal_execute, 'call-1', ).execute({ sessionId: 'session-1', command: 'npm test' }) as { error: string; stdout?: string; }; assert.ok(result.error.length < 10_000); assert.doesNotMatch(result.error, /tok_live/); assert.match(result.error, /tool output handle/); assert.ok((result.stdout?.length ?? 0) < 30_000); assert.match(result.stdout ?? '', /output handle/); const handleId = result.stdout?.match(/handleId=(tool-output-[^\]\s]+)/)?.[1]; assert.ok(handleId); assert.match( store.read({ handleId, mode: 'tail', maxChars: 1_000 }, 'chat-1') ?? '', /FATAL_MID=E_CONN_RESET_7319/, ); }); it('truncates large vault note content and stores the full note body behind a handle', async () => { const store = new ToolOutputStore(); const body = `${'note line\n'.repeat(1000)}important ending`; const { tools, toolsContext } = createCattyToolsFromCatalog( { aiCapability: async () => ({ ok: true, note: { id: 'note-1', title: 'Long note', content: body, }, }), }, { sessions: [] }, [], 'auto', undefined, 'chat-1', store, ); const result = await withCattyToolContext( tools.vault_notes_get, toolsContext.vault_notes_get, 'call-1', ).execute( { noteId: 'note-1' }, ) as { note: { content: string } }; assert.notEqual(result.note.content, body); assert.match(result.note.content, /tool output handle/); const handleId = result.note.content.match(/handleId=(tool-output-[^\]\s]+)/)?.[1]; assert.ok(handleId); const recovered = store.readChunk({ handleId, mode: 'search', query: 'important ending', }, 'chat-1'); assert.match(recovered?.content ?? '', /important ending/); }); it('hard-caps explicit tool output reads and returns a continuation cursor', async () => { const store = new ToolOutputStore(); const body = `${'full note line\n'.repeat(1000)}important ending`; const handle = store.store({ chatSessionId: 'chat-1', capabilityId: 'vault.notes.get', content: body, }); const { tools, toolsContext } = createCattyToolsFromCatalog( {}, { sessions: [] }, [], 'auto', undefined, 'chat-1', store, ); const result = await withCattyToolContext( tools.tool_output_read, toolsContext.tool_output_read, 'call-1', ).execute( { handleId: handle.id, mode: 'full', maxChars: body.length + 100 }, ) as { content: string; nextOffset: number; hasMore: boolean; totalChars: number }; assert.ok(result.content.length <= 12_000); assert.equal(result.nextOffset, result.content.length); assert.equal(result.hasMore, true); assert.equal(result.totalChars, body.length); }); it('enforces a shared saved-output read budget across one turn', async () => { const store = new ToolOutputStore(); const dedup = new ToolResultDedup(); dedup.beginTurn(); const handle = store.store({ chatSessionId: 'chat-1', capabilityId: 'terminal.execute', content: 'x'.repeat(50_000), }); const { tools, toolsContext } = createCattyToolsFromCatalog( {}, { sessions: [] }, [], 'auto', undefined, 'chat-1', store, dedup, ); const reader = withCattyToolContext(tools.tool_output_read, toolsContext.tool_output_read); const first = await reader.execute({ handleId: handle.id, mode: 'range', offset: 0, maxChars: 12_000 }); const second = await reader.execute({ handleId: handle.id, mode: 'range', offset: 12_000, maxChars: 12_000 }); const third = await reader.execute({ handleId: handle.id, mode: 'range', offset: 24_000, maxChars: 12_000 }) as { error?: string }; assert.equal((first as { content: string }).content.length, 12_000); assert.equal((second as { content: string }).content.length, 12_000); assert.match(third.error ?? '', /read budget/); }); it('replays a completed terminal command instead of executing it again after retry compaction', async () => { let executions = 0; const dedup = new ToolResultDedup(); dedup.beginTurn(); const { tools, toolsContext } = createCattyToolsFromCatalog( { aiExec: async () => { executions += 1; return { ok: true, stdout: 'deployed once', stderr: '', exitCode: 0 }; }, }, { sessions: [{ sessionId: 'session-1', hostId: 'host-1', hostname: 'prod', label: 'prod', connected: true, }], }, [], 'auto', undefined, 'chat-1', undefined, dedup, ); const execute = withCattyToolContext(tools.terminal_execute, toolsContext.terminal_execute); await execute.execute({ sessionId: 'session-1', command: 'deploy production' }); dedup.enableWriteReplay(); const replay = await execute.execute({ sessionId: 'session-1', command: 'deploy production' }) as { replayedCompletedResult?: boolean; }; assert.equal(executions, 1); assert.equal(replay.replayedCompletedResult, true); const intentionalRepeat = await execute.execute({ sessionId: 'session-1', command: 'deploy production' }) as { replayedCompletedResult?: boolean; }; assert.equal(executions, 2); assert.equal(intentionalRepeat.replayedCompletedResult, undefined); }); it('executes an intentional repeat when the completed result is already in retry history', async () => { let executions = 0; const dedup = new ToolResultDedup(); dedup.beginTurn(); const { tools, toolsContext } = createCattyToolsFromCatalog( { aiExec: async () => { executions += 1; return { ok: true, stdout: `run ${executions}`, stderr: '', exitCode: 0 }; }, }, { sessions: [{ sessionId: 'session-1', hostId: 'host-1', hostname: 'prod', label: 'prod', connected: true, }], }, [], 'auto', undefined, 'chat-1', undefined, dedup, ); const execute = withCattyToolContext(tools.terminal_execute, toolsContext.terminal_execute); const args = { sessionId: 'session-1', command: 'npm test' }; await execute.execute(args); const retryHistory = [ { id: 'assistant-progress', role: 'assistant' as const, content: '', timestamp: 1, toolCalls: [{ id: 'call-1', name: 'terminal_execute', arguments: args }], }, { id: 'tool-progress', role: 'tool' as const, content: '', timestamp: 2, toolResults: [{ toolCallId: 'call-1', content: 'run 1' }], }, ]; dedup.enableWriteReplay(collectPreservedTerminalWriteFingerprints( retryHistory, 'assistant-progress', 'chat-1', )); const repeat = await execute.execute(args) as { replayedCompletedResult?: boolean }; assert.equal(executions, 2); assert.equal(repeat.replayedCompletedResult, undefined); }); it('pairs reused tool call IDs with the nearest preceding terminal command', () => { const commandA = { sessionId: 'session-1', command: 'npm test a' }; const commandB = { sessionId: 'session-1', command: 'npm test b' }; const retryHistory = [ { id: 'assistant-a', role: 'assistant' as const, content: '', timestamp: 1, toolCalls: [{ id: 'reused-call', name: 'terminal_execute', arguments: commandA }], }, { id: 'tool-a', role: 'tool' as const, content: '', timestamp: 2, toolResults: [{ toolCallId: 'reused-call', content: 'result a' }], }, { id: 'assistant-b', role: 'assistant' as const, content: '', timestamp: 3, toolCalls: [{ id: 'reused-call', name: 'terminal_execute', arguments: commandB }], }, { id: 'tool-b', role: 'tool' as const, content: '', timestamp: 4, toolResults: [{ toolCallId: 'reused-call', content: 'result b' }], }, ]; assert.deepEqual( collectPreservedTerminalWriteFingerprints(retryHistory, 'assistant-a', 'chat-1'), [ buildTerminalWriteFingerprint('terminal_execute', 'chat-1', commandA), buildTerminalWriteFingerprint('terminal_execute', 'chat-1', commandB), ], ); }); it('replays a started background job instead of starting it twice after retry compaction', async () => { let starts = 0; const dedup = new ToolResultDedup(); const { tools, toolsContext } = createCattyToolsFromCatalog( { aiCapability: async () => ({ ok: true, jobId: `job-${++starts}`, status: 'running', command: 'deploy --password swordfish', output: 'x'.repeat(30_000), }) }, { sessions: [] }, [], 'auto', undefined, 'chat-start', undefined, dedup, ); const start = withCattyToolContext(tools.terminal_start, toolsContext.terminal_start); await start.execute({ sessionId: 'session-1', command: 'npm run build' }); dedup.enableWriteReplay(); const replay = await start.execute({ sessionId: 'session-1', command: 'npm run build' }) as { replayedCompletedResult?: boolean; jobId?: string; command?: string; output?: string; }; assert.equal(starts, 1); assert.equal(replay.jobId, 'job-1'); assert.equal(replay.replayedCompletedResult, true); assert.doesNotMatch(replay.command ?? '', /swordfish/); assert.match(replay.output ?? '', /tool output handle/); const intentionalRestart = await start.execute({ sessionId: 'session-1', command: 'npm run build' }) as { replayedCompletedResult?: boolean; jobId?: string; }; assert.equal(starts, 2); assert.equal(intentionalRestart.jobId, 'job-2'); assert.equal(intentionalRestart.replayedCompletedResult, undefined); }); }); describe('capabilityTools terminal context reader', () => { it('reads terminal context from the only scoped terminal when sessionId is omitted', async () => { const { tools, toolsContext } = createCattyToolsFromCatalog( {}, { sessions: [{ sessionId: 'session-1', hostId: 'host-1', hostname: 'prod.internal', label: 'prod', connected: true, }], readTerminalContext: async (request) => ({ ok: true, sessionId: request.sessionId, label: 'prod', range: request.range ?? 'viewport', content: 'line-a\nline-b', totalLines: 2, startLine: 0, endLine: 1, returnedLines: 2, hasMoreBefore: false, hasMoreAfter: false, source: 'live', }), }, [], 'auto', undefined, 'chat-1', ); const result = await withCattyToolContext( tools.terminal_read_context, toolsContext.terminal_read_context, 'call-1', ).execute( { range: 'tail', maxLines: 20 }, ) as { sessionId: string; content: string; range: string }; assert.equal(result.sessionId, 'session-1'); assert.equal(result.range, 'tail'); assert.equal(result.content, 'line-a\nline-b'); }); it('fits large terminal context reads through the shared tool output store', async () => { const store = new ToolOutputStore(); const body = `${'terminal line output '.repeat(900)}important ending`; const { tools, toolsContext } = createCattyToolsFromCatalog( {}, { sessions: [{ sessionId: 'session-1', hostId: 'host-1', hostname: 'prod.internal', label: 'prod', connected: true, }], readTerminalContext: async (request) => ({ ok: true, sessionId: request.sessionId, label: 'prod', range: request.range ?? 'viewport', content: body, totalLines: 1, startLine: 0, endLine: 0, returnedLines: 1, hasMoreBefore: false, hasMoreAfter: false, source: 'live', }), }, [], 'auto', undefined, 'chat-1', store, ); const result = await withCattyToolContext( tools.terminal_read_context, toolsContext.terminal_read_context, 'call-1', ).execute( { range: 'viewport' }, ) as { content: string }; assert.notEqual(result.content, body); assert.match(result.content, /tool output handle/); const handleId = result.content.match(/handleId=(tool-output-[^\]\s]+)/)?.[1]; assert.ok(handleId); const recovered = store.readChunk({ handleId, mode: 'search', query: 'important ending', }, 'chat-1'); assert.match(recovered?.content ?? '', /important ending/); }); it('asks for sessionId when multiple scoped terminals are available', async () => { const { tools, toolsContext } = createCattyToolsFromCatalog( {}, { sessions: [ { sessionId: 'session-1', hostId: 'host-1', hostname: 'a', label: 'a', connected: true }, { sessionId: 'session-2', hostId: 'host-2', hostname: 'b', label: 'b', connected: true }, ], }, [], 'auto', undefined, 'chat-1', ); const result = await withCattyToolContext( tools.terminal_read_context, toolsContext.terminal_read_context, 'call-1', ).execute( { range: 'viewport' }, ) as { error?: string }; assert.match(result.error ?? '', /sessionId/); }); it('returns a small cached notice for an unchanged terminal context range', async () => { const dedup = new ToolResultDedup(); dedup.beginTurn(); const { tools, toolsContext } = createCattyToolsFromCatalog( {}, { sessions: [{ sessionId: 'session-1', hostId: 'host-1', hostname: 'prod.internal', label: 'prod', connected: true, }], readTerminalContext: async () => ({ ok: true, sessionId: 'session-1', range: 'tail', content: 'same terminal screen', totalLines: 1, startLine: 0, endLine: 0, returnedLines: 1, hasMoreBefore: false, hasMoreAfter: false, source: 'live', }), }, [], 'auto', undefined, 'chat-1', undefined, dedup, ); const reader = withCattyToolContext( tools.terminal_read_context, toolsContext.terminal_read_context, ); const first = await reader.execute({ sessionId: 'session-1', range: 'tail' }); const second = await reader.execute({ sessionId: 'session-1', range: 'tail' }); assert.equal(typeof first, 'object'); assert.match(String(second), /^\[cached\]/); }); }); describe('capabilityTools terminal polling', () => { it('tags polled output handles with the owning terminal for close cleanup', async () => { const store = new ToolOutputStore(); const dedup = new ToolResultDedup(); const { tools, toolsContext } = createCattyToolsFromCatalog( { aiCapability: async (method: string) => method.includes('jobStart') ? { ok: true, jobId: 'job-owned', status: 'running', nextOffset: 0 } : { ok: true, jobId: 'job-owned', status: 'running', output: 'x'.repeat(30_000), nextOffset: 30_000 }, }, { sessions: [] }, [], 'auto', undefined, 'chat-owned', store, dedup, ); await withCattyToolContext(tools.terminal_start, toolsContext.terminal_start) .execute({ sessionId: 'session-owned', command: 'npm run dev' }); const result = await withCattyToolContext(tools.terminal_poll, toolsContext.terminal_poll) .execute({ jobId: 'job-owned', offset: 0 }) as { output: string }; const handleId = result.output.match(/handleId=(tool-output-[^\]\s]+)/)?.[1]; assert.ok(handleId); store.pruneTerminalSession('chat-owned', 'session-owned'); assert.equal(store.get(handleId, 'chat-owned'), undefined); }); it('deduplicates an unchanged job output range', async () => { const dedup = new ToolResultDedup(); dedup.beginTurn(); const { tools, toolsContext } = createCattyToolsFromCatalog( { aiCapability: async () => ({ ok: true, jobId: 'job-1', sessionId: 'session-1', status: 'running', output: 'same build output', outputBaseOffset: 0, nextOffset: 17, totalOutputChars: 17, }), }, { sessions: [] }, [], 'auto', undefined, 'chat-1', undefined, dedup, ); const poll = withCattyToolContext(tools.terminal_poll, toolsContext.terminal_poll); const first = await poll.execute({ jobId: 'job-1', offset: 0 }); const second = await poll.execute({ jobId: 'job-1', offset: 0 }); assert.equal(typeof first, 'object'); assert.match(String(second), /^\[cached\]/); }); it('bounds follow-style monitor output before it reaches the model', async () => { const store = new ToolOutputStore(); const rawOutput = `${'x '.repeat(400)}\n${'log line\n'.repeat(500)}MONITOR_MIDDLE_EVIDENCE_7319\n${'tail line\n'.repeat(500)}`; const { tools, toolsContext } = createCattyToolsFromCatalog( { aiCapability: async () => ({ ok: true, jobId: 'monitor-job-unique', command: 'tail -f /var/log/app.log', status: 'running', output: rawOutput, nextOffset: 9_000, }), }, { sessions: [] }, [], 'auto', undefined, 'chat-monitor', store, ); const result = await withCattyToolContext( tools.terminal_poll, toolsContext.terminal_poll, ).execute({ jobId: 'monitor-job-unique', offset: 0 }) as { output: string }; assert.ok(result.output.length < 3_500); assert.ok(result.output.split('\n')[0].length <= 500); const handleId = result.output.match(/handleId=(tool-output-[^\]\s]+)/)?.[1]; assert.ok(handleId); assert.match( store.read({ handleId, mode: 'search', query: 'MONITOR_MIDDLE_EVIDENCE_7319' }, 'chat-monitor') ?? '', /MONITOR_MIDDLE_EVIDENCE_7319/, ); }); it('does not count empty monitor polls as output bursts', async () => { let polls = 0; const { tools, toolsContext } = createCattyToolsFromCatalog( { aiCapability: async () => ({ ok: true, jobId: 'quiet-monitor-job', command: 'tail -f /var/log/app.log', status: 'running', output: polls++ < 12 ? '' : 'first new line', nextOffset: 0, }), }, { sessions: [] }, [], 'auto', undefined, 'chat-quiet-monitor', ); const poll = withCattyToolContext(tools.terminal_poll, toolsContext.terminal_poll); for (let index = 0; index < 12; index += 1) { await poll.execute({ jobId: 'quiet-monitor-job', offset: 0 }); } const result = await poll.execute({ jobId: 'quiet-monitor-job', offset: 0 }) as { output: string }; assert.equal(result.output, 'first new line'); }); });