GPU_GUARD_MONOREPO/packages/backend/test/tool_process_events.test.ts

126 lines
3.8 KiB
TypeScript
Raw Normal View History

2026-05-20 21:39:12 +08:00
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',
}));
});
});