🧪 test: Guard Run-Scoped MCP Definition Handoff Boundaries

The original ClickHouse breaker storm regressed precisely at field
pass-through boundaries that unit tests of each end could not see:
initializeAgent dropping mcpAvailableTools from its destructure, and the
agent tool context losing it on the way into ON_TOOL_EXECUTE. Add direct
guards on both hops: the loadTools result must surface on the
initialized agent, and the captured toolExecuteOptions closure must
forward it to loadToolsForExecution.
This commit is contained in:
Danny Avila 2026-06-11 10:13:42 -04:00
parent 1ca238cae1
commit 0cb6c27c67
2 changed files with 93 additions and 0 deletions

View file

@ -539,6 +539,48 @@ describe('initializeClient — subagent loading', () => {
expect(arg.actionsEnabled).toBe(true);
});
it('threads run-scoped MCP tool definitions into ON_TOOL_EXECUTE loading', async () => {
/** Regression guard for the request-scoped MCP/PTC handoff: the
* `mcpAvailableTools` discovered at run start must survive
* `buildAgentToolContext` and reach `loadToolsForExecution`, otherwise
* request-scoped servers reinitialize on every programmatic tool call
* and can trip the MCP circuit breaker under parallel calls. */
const mcpTool = 'list_tables_mcp_ClickHouse';
const mcpAvailableTools = {
ClickHouse: {
[mcpTool]: {
type: 'function',
function: {
name: mcpTool,
description: 'List tables',
parameters: { type: 'object', properties: {} },
},
},
},
};
const primaryConfig = {
...makePrimaryConfig({}),
toolRegistry: new Map([[mcpTool, { name: mcpTool }]]),
mcpAvailableTools,
};
mockInitializeAgent.mockResolvedValue(primaryConfig);
await initializeClient({
req: makeSubagentReq(),
res: {},
signal: new AbortController().signal,
endpointOption: makeEndpointOption(),
});
expect(capturedToolExecuteOptions?.loadTools).toBeInstanceOf(Function);
await capturedToolExecuteOptions.loadTools([mcpTool], PRIMARY_ID);
expect(mockLoadToolsForExecution).toHaveBeenCalledTimes(1);
expect(mockLoadToolsForExecution).toHaveBeenCalledWith(
expect.objectContaining({ mcpAvailableTools }),
);
});
it('deduplicates repeated ids in subagents.agent_ids', async () => {
const subAgent = await createAgent({
id: DUPLICATE_SUBAGENT_ID,

View file

@ -2068,3 +2068,54 @@ describe('initializeAgent — code-generated file thread filter (regression)', (
expect(getUserCodeFiles).not.toHaveBeenCalled();
});
});
describe('initializeAgent — run-scoped MCP tool definitions', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('carries mcpAvailableTools from the loadTools result onto the initialized agent', async () => {
/** Regression guard for the request-scoped MCP/PTC handoff: dropping this
* field at the destructure boundary forces per-call reinitialization
* downstream and can storm the MCP circuit breaker. */
const { agent, req, res, loadTools, db } = createMocks();
const mcpTool = 'list_tables_mcp_ClickHouse';
const mcpAvailableTools = {
ClickHouse: {
[mcpTool]: {
type: 'function' as const,
function: {
name: mcpTool,
description: 'List tables',
parameters: { type: 'object' as const, properties: {} },
},
},
},
};
loadTools.mockResolvedValue({
tools: [],
toolContextMap: {},
dynamicToolContextMap: {},
userMCPAuthMap: undefined,
toolRegistry: undefined,
toolDefinitions: [],
hasDeferredTools: false,
mcpAvailableTools,
});
const result = await initializeAgent(
{
req,
res,
agent,
loadTools,
endpointOption: { endpoint: EModelEndpoint.agents },
allowedProviders: new Set([Providers.OPENAI]),
isInitialAgent: true,
},
db,
);
expect(result.mcpAvailableTools).toEqual(mcpAvailableTools);
});
});