test(tools): verify timeout abort prevents post-timeout side effects

This commit is contained in:
William Valentin
2026-02-15 22:17:49 -08:00
parent b4006e91ff
commit 2eccd3e8eb
3 changed files with 45 additions and 4 deletions
+40
View File
@@ -59,6 +59,31 @@ const cancellableTool: Tool = {
},
};
function createSideEffectTool(sideEffect: { fired: boolean }): Tool {
return {
name: 'test.side_effect',
description: 'Cancellable side effect',
inputSchema: { type: 'object', properties: {} },
execute: async (_args, context) => {
return await new Promise((resolve) => {
const timer = setTimeout(() => {
sideEffect.fired = true;
resolve({ success: true, output: 'side effect fired' });
}, 120);
const onAbort = () => {
clearTimeout(timer);
resolve({ success: false, output: '', error: 'aborted' });
};
if (context?.signal?.aborted) {
onAbort();
return;
}
context?.signal?.addEventListener('abort', onAbort, { once: true });
});
},
};
}
describe('ToolExecutor', () => {
it('executes a tool and returns result', async () => {
const registry = new ToolRegistry();
@@ -114,6 +139,21 @@ describe('ToolExecutor', () => {
expect(result.error).toContain('timed out');
});
it('prevents post-timeout side effects for cancellable tools', async () => {
const sideEffect = { fired: false };
const registry = new ToolRegistry();
registry.register(createSideEffectTool(sideEffect));
const hooks = new HookEngine({ confirm: [], log: [], silent: [] });
const executor = new ToolExecutor(registry, hooks, { defaultTimeoutMs: 30 });
const result = await executor.execute('test.side_effect', {});
expect(result.success).toBe(false);
expect(result.error).toContain('timed out');
await new Promise(resolve => setTimeout(resolve, 180));
expect(sideEffect.fired).toBe(false);
});
it('truncates large output', async () => {
const registry = new ToolRegistry();
registry.register(bigOutputTool);