126 lines
3.8 KiB
TypeScript
126 lines
3.8 KiB
TypeScript
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<string, unknown>, 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',
|
|
}));
|
|
});
|
|
});
|