fix(tools): stop cancelled calls before execution and discard late results

This commit is contained in:
Rudy Celekli committed 2026-10-14 14:17:54 -04:00
1 parent 6dd4d0156f
commit 691be39c1b
4 files changed
+50

No files matched your search

+41
View File
@@ -0,0 +1,41 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { composeCatalog, defineTool } from './catalog.js';
test('cancelled catalog calls cannot execute or return late results', async () => {
let calls = 0,
release,
block = false;
const action = defineTool({
name: 'fixture_action',
kind: 'action',
title: 'Fixture',
description: 'Offline fixture',
inputSchema: { type: 'object' },
run: async () => {
calls++;
return block
? new Promise((r) => (release = r))
: { summary: 'fixture', data: {} };
},
});
const catalog = composeCatalog({ tools: [action] });
const c = new AbortController();
c.abort();
await assert.rejects(
catalog.call('fixture_action', {}, { signal: c.signal }),
{ name: 'AbortError' },
);
assert.equal(calls, 0);
block = true;
const running = new AbortController();
const pending = catalog.call(
'fixture_action',
{},
{ signal: running.signal },
);
while (!release) await Promise.resolve();
running.abort();
release({ summary: 'late', data: {} });
await assert.rejects(pending, { name: 'AbortError' });
assert.equal(calls, 1);
});
+2
View File
@@ -178,6 +178,7 @@ export function composeCatalog({
return chain({ tool, args, signal, ...(parent ? { parent } : {}) });
};
const execute = async ({ tool, args, signal }) => {
signal?.throwIfAborted();
const problems = validateValue(tool.inputSchema, args);
if (problems.length)
throw new ToolError('invalid_arguments', problems.join('; '));
@@ -194,6 +195,7 @@ export function composeCatalog({
} catch (error) {
throw fromSourceError(error);
}
signal?.throwIfAborted();
if (!result || typeof result.summary !== 'string' || !result.data)
throw new TypeError(`${tool.name} returned no summary or data`);
if (