import { Type } from '@sinclair/typebox'; import { runAttempt } from '../src/modules/netaclaw/runtime/attempt.js'; jest.mock('../src/modules/netaclaw/runtime/model_selection.js', () => ({ getProvider: jest.fn(), })); describe('tool process events', () => { const { getProvider } = require('../src/modules/netaclaw/runtime/model_selection.js'); beforeEach(() => { jest.resetAllMocks(); }); it('normalizes tool process updates and emits them from runAttempt', async () => { const chat = jest.fn() .mockResolvedValueOnce({ content: 'tool turn', toolCalls: [ { id: 'call-1', name: 'slow_tool', arguments: '{"taskId":"task-1"}' }, ], usage: { inputTokens: 1, outputTokens: 1 }, }) .mockResolvedValueOnce({ content: 'done', toolCalls: [], usage: { inputTokens: 1, outputTokens: 1 }, }); getProvider.mockReturnValue({ chat }); const updates: any[] = []; await runAttempt({ messages: [{ role: 'user', content: 'run slow tool' }], tools: [{ name: 'slow_tool', label: 'Slow Tool', description: 'Slow tool', parameters: Type.Object({ taskId: Type.String() }), execute: async (_id: string, _args: { taskId: string }, onUpdate?: (event: any) => void) => { onUpdate?.({ operationId: 'tool-provided-operation-id', source: 'tool', targetType: 'tool', stage: 'prepare', status: 'running', message: 'Preparing work', detail: 'x'.repeat(600), current: 1, total: 2, percent: 500, payload: { apiKey: 'secret', note: 'payload', }, }); return 'ok'; }, }], config: { provider: 'mock', model: 'mock', apiKey: 'k' }, onToolProcessEvent: update => updates.push(update), }); expect(updates[0]).toEqual(expect.objectContaining({ toolCallId: 'call-1', operationId: 'tool-call-1', toolName: 'slow_tool', args: { taskId: 'task-1' }, })); expect(updates[0].event).toEqual(expect.objectContaining({ version: 1, operationId: 'tool-call-1', targetType: 'tool', source: 'tool', percent: 100, detail: `${'x'.repeat(500)}...`, payload: { apiKey: '[filtered]', note: 'payload', }, })); }); it('does not fail tool execution when process event observer throws', async () => { const chat = jest.fn() .mockResolvedValueOnce({ content: 'tool turn', toolCalls: [ { id: 'call-1', name: 'slow_tool', arguments: '{"token":"secret","taskId":"task-1"}' }, ], usage: { inputTokens: 1, outputTokens: 1 }, }) .mockResolvedValueOnce({ content: 'done', toolCalls: [], usage: { inputTokens: 1, outputTokens: 1 }, }); getProvider.mockReturnValue({ chat }); const result = await runAttempt({ messages: [{ role: 'user', content: 'run slow tool' }], tools: [{ name: 'slow_tool', label: 'Slow Tool', description: 'Slow tool', parameters: Type.Object({ taskId: Type.String() }), execute: async (_id: string, _args: Record, onUpdate?: (event: any) => void) => { onUpdate?.({ message: 'still observational' }); return 'ok'; }, }], config: { provider: 'mock', model: 'mock', apiKey: 'k' }, onToolProcessEvent: () => { throw new Error('socket closed'); }, }); expect(result.finalContent).toBe('done'); expect(result.messages).toContainEqual(expect.objectContaining({ role: 'tool', content: 'ok', toolCallId: 'call-1', })); }); });