/** * Integration tests for directToolNames propagation in StandardGraph. * * Verifies that graph-managed tools (handoff/transfer) are correctly identified * as `directToolNames` in both the event-driven and traditional ToolNode paths. * This is critical for HITL bypass — without directToolNames, handoff tools * would incorrectly trigger approval prompts. */ import { StandardGraph } from '@/graphs/Graph'; import { ToolNode as CustomToolNode } from '@/tools/ToolNode'; import { AgentContext } from '@/agents/AgentContext'; import type * as t from '@/types'; /** * Minimal mock tool that satisfies the GenericTool interface. */ function createMockTool(name: string): t.GenericTool { return { name, invoke: async () => 'ok', lc_namespace: ['test'], } as unknown as t.GenericTool; } /** * Create a minimal StandardGraph instance with a single agent. * The agent can optionally have graphTools (simulating handoff tools). */ function createGraphWithAgent({ graphTools, toolDefinitions, }: { graphTools?: t.GenericTool[]; toolDefinitions?: t.LCTool[]; } = {}): StandardGraph { const graph = new StandardGraph({ agents: [ { agentId: 'test-agent', provider: 'openai' as any, clientOptions: { model: 'gpt-4' }, }, ], }); // Manually set graphTools on the agent context (normally done by MultiAgentGraph.createHandoffTools) const context = graph.agentContexts.get('test-agent'); if (context && graphTools) { context.graphTools = graphTools; } if (context && toolDefinitions) { context.toolDefinitions = toolDefinitions; } return graph; } /** * Access the private `initializeTools` method to get the ToolNode it creates. * This lets us inspect directToolNames without running a full graph. */ function getToolNode( graph: StandardGraph, agentId: string ): CustomToolNode { const context = graph.agentContexts.get(agentId); // eslint-disable-next-line @typescript-eslint/no-explicit-any return (graph as any).initializeTools({ currentTools: context?.tools, currentToolMap: context?.toolMap, agentContext: context, }); } describe('directToolNames in StandardGraph.initializeTools', () => { describe('traditional (non-event-driven) path', () => { it('builds directToolNames when graphTools are present', () => { const handoffTool = createMockTool('lc_transfer_to_agent_abc'); const graph = createGraphWithAgent({ graphTools: [handoffTool], }); const toolNode = getToolNode(graph, 'test-agent'); // eslint-disable-next-line @typescript-eslint/no-explicit-any const directNames = (toolNode as any).directToolNames as | Set | undefined; expect(directNames).toBeDefined(); expect(directNames).toBeInstanceOf(Set); expect(directNames!.has('lc_transfer_to_agent_abc')).toBe(true); }); it('includes all graph tools in directToolNames', () => { const tool1 = createMockTool('lc_transfer_to_agent_abc'); const tool2 = createMockTool('lc_transfer_to_agent_xyz'); const graph = createGraphWithAgent({ graphTools: [tool1, tool2], }); const toolNode = getToolNode(graph, 'test-agent'); // eslint-disable-next-line @typescript-eslint/no-explicit-any const directNames = (toolNode as any).directToolNames as | Set | undefined; expect(directNames).toBeDefined(); expect(directNames!.size).toBe(2); expect(directNames!.has('lc_transfer_to_agent_abc')).toBe(true); expect(directNames!.has('lc_transfer_to_agent_xyz')).toBe(true); }); it('returns undefined directToolNames when no graphTools', () => { const graph = createGraphWithAgent({}); const toolNode = getToolNode(graph, 'test-agent'); // eslint-disable-next-line @typescript-eslint/no-explicit-any const directNames = (toolNode as any).directToolNames as | Set | undefined; expect(directNames).toBeUndefined(); }); it('returns undefined directToolNames when graphTools is empty', () => { const graph = createGraphWithAgent({ graphTools: [], }); const toolNode = getToolNode(graph, 'test-agent'); // eslint-disable-next-line @typescript-eslint/no-explicit-any const directNames = (toolNode as any).directToolNames as | Set | undefined; expect(directNames).toBeUndefined(); }); }); describe('event-driven path', () => { it('builds directToolNames when graphTools and toolDefinitions are present', () => { const handoffTool = createMockTool('lc_transfer_to_agent_abc'); const toolDef: t.LCTool = { name: 'content_tool', description: 'A content tool', parameters: { type: 'object', properties: {} }, } as unknown as t.LCTool; const graph = createGraphWithAgent({ graphTools: [handoffTool], toolDefinitions: [toolDef], }); const toolNode = getToolNode(graph, 'test-agent'); // eslint-disable-next-line @typescript-eslint/no-explicit-any const directNames = (toolNode as any).directToolNames as | Set | undefined; expect(directNames).toBeDefined(); expect(directNames!.has('lc_transfer_to_agent_abc')).toBe(true); }); it('event-driven mode is activated by toolDefinitions', () => { const toolDef: t.LCTool = { name: 'content_tool', description: 'A content tool', parameters: { type: 'object', properties: {} }, } as unknown as t.LCTool; const graph = createGraphWithAgent({ toolDefinitions: [toolDef], }); const toolNode = getToolNode(graph, 'test-agent'); // eslint-disable-next-line @typescript-eslint/no-explicit-any const isEventDriven = (toolNode as any).eventDrivenMode; expect(isEventDriven).toBe(true); }); }); describe('HITL integration', () => { it('handoff tool bypasses approval when directToolNames is set and HITL is enabled', () => { const handoffTool = createMockTool('lc_transfer_to_agent_abc'); const graph = createGraphWithAgent({ graphTools: [handoffTool], }); // Simulate HITL config being set graph.compileOptions = { toolApprovalConfig: { defaultPolicy: 'always', executionContext: 'interactive', }, } as any; const toolNode = getToolNode(graph, 'test-agent'); // eslint-disable-next-line @typescript-eslint/no-explicit-any const requiresApproval = (toolNode as any).requiresApproval.bind( toolNode ); // Handoff tool should bypass expect(requiresApproval('lc_transfer_to_agent_abc', {})).toBe(false); // Regular tool should still require approval expect(requiresApproval('content_tool', {})).toBe(true); }); }); });