Skip to main content
Glama
introspection.test.ts9.9 kB
/** * Tests for execution introspection and debugging features */ import { describe, it } from "node:test"; import { strict as assert } from "node:assert"; import { McpGraphApi } from "../src/api.js"; import type { ExecutionHooks, ExecutionStatus } from "../src/types/execution.js"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { dirname } from "node:path"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const projectRoot = join(__dirname, ".."); describe("Execution introspection and debugging", () => { describe("execution hooks", () => { it("should call onNodeStart and onNodeComplete hooks", async () => { const configPath = join(projectRoot, "examples", "switch_example.yaml"); const api = new McpGraphApi(configPath); const nodeStartCalls: Array<{ nodeId: string; nodeType: string }> = []; const nodeCompleteCalls: Array<{ nodeId: string; duration: number }> = []; const hooks: ExecutionHooks = { onNodeStart: async (nodeId, node) => { nodeStartCalls.push({ nodeId, nodeType: node.type }); return true; // Continue execution }, onNodeComplete: async (nodeId, node, input, output, duration) => { nodeCompleteCalls.push({ nodeId, duration }); }, }; await api.executeTool("test_switch", { value: 5 }, { hooks }); // Verify hooks were called assert(nodeStartCalls.length > 0, "onNodeStart should be called"); assert(nodeCompleteCalls.length > 0, "onNodeComplete should be called"); // Verify entry node was called const entryCall = nodeStartCalls.find((c) => c.nodeId === "entry"); assert(entryCall !== undefined, "Entry node should be called"); assert(entryCall.nodeType === "entry", "Entry node should have type 'entry'"); // Verify switch node was called const switchCall = nodeStartCalls.find((c) => c.nodeId === "switch_node"); assert(switchCall !== undefined, "Switch node should be called"); assert(switchCall.nodeType === "switch", "Switch node should have type 'switch'"); // Verify durations are recorded assert( nodeCompleteCalls.every((c) => c.duration >= 0), "All durations should be non-negative" ); await api.close(); }); it("should call onNodeError hook when node fails", async () => { const configPath = join(projectRoot, "examples", "switch_example.yaml"); const api = new McpGraphApi(configPath); let errorHookCalled = false; let errorNodeId: string | undefined; const hooks: ExecutionHooks = { onNodeError: async (nodeId, node, error) => { errorHookCalled = true; errorNodeId = nodeId; }, }; // This should not error, but we can test error handling with a malformed config // For now, just verify the hook structure is correct await api.executeTool("test_switch", { value: 5 }, { hooks }); // In a real error scenario, errorHookCalled would be true // For now, we just verify the hook is properly structured assert(typeof hooks.onNodeError === "function", "onNodeError hook should be callable"); await api.close(); }); }); describe("execution history", () => { it("should return execution history with timing", async () => { const configPath = join(projectRoot, "examples", "switch_example.yaml"); const api = new McpGraphApi(configPath); const result = await api.executeTool("test_switch", { value: 5 }, { enableTelemetry: true, }); assert(result.executionHistory !== undefined, "Execution history should be present"); assert(Array.isArray(result.executionHistory), "Execution history should be an array"); assert(result.executionHistory.length > 0, "Execution history should have entries"); // Verify history entries have required fields for (const record of result.executionHistory) { assert(typeof record.nodeId === "string", "Record should have nodeId"); assert(typeof record.nodeType === "string", "Record should have nodeType"); assert(typeof record.startTime === "number", "Record should have startTime"); assert(typeof record.endTime === "number", "Record should have endTime"); assert(typeof record.duration === "number", "Record should have duration"); assert(record.duration >= 0, "Duration should be non-negative"); assert(record.endTime >= record.startTime, "endTime should be >= startTime"); } await api.close(); }); }); describe("telemetry", () => { it("should collect telemetry when enabled", async () => { const configPath = join(projectRoot, "examples", "switch_example.yaml"); const api = new McpGraphApi(configPath); const result = await api.executeTool("test_switch", { value: 5 }, { enableTelemetry: true, }); assert(result.telemetry !== undefined, "Telemetry should be present"); assert(typeof result.telemetry.totalDuration === "number", "Total duration should be a number"); assert(result.telemetry.totalDuration >= 0, "Total duration should be non-negative"); assert(result.telemetry.nodeDurations instanceof Map, "Node durations should be a Map"); assert(result.telemetry.nodeCounts instanceof Map, "Node counts should be a Map"); assert(typeof result.telemetry.errorCount === "number", "Error count should be a number"); // Verify node counts match history const history = result.executionHistory || []; const nodeTypeCounts = new Map<string, number>(); for (const record of history) { const count = nodeTypeCounts.get(record.nodeType) || 0; nodeTypeCounts.set(record.nodeType, count + 1); } // Compare counts for (const [nodeType, count] of nodeTypeCounts) { const telemetryCount = result.telemetry!.nodeCounts.get(nodeType); assert(telemetryCount === count, `Node count for ${nodeType} should match history`); } await api.close(); }); it("should not collect telemetry when disabled", async () => { const configPath = join(projectRoot, "examples", "switch_example.yaml"); const api = new McpGraphApi(configPath); const result = await api.executeTool("test_switch", { value: 5 }, { enableTelemetry: false, }); assert(result.telemetry === undefined, "Telemetry should not be present when disabled"); await api.close(); }); }); describe("execution controller", () => { it("should provide controller when hooks are used", async () => { const configPath = join(projectRoot, "examples", "switch_example.yaml"); const api = new McpGraphApi(configPath); const hooks: ExecutionHooks = { onNodeStart: async () => true, }; // Start execution in background const executionPromise = api.executeTool("test_switch", { value: 5 }, { hooks }); // Controller should be available during execution const controller = api.getController(); assert(controller !== null, "Controller should be available during execution"); // Wait for execution to complete await executionPromise; // Controller should be cleaned up after execution const controllerAfter = api.getController(); assert(controllerAfter === null, "Controller should be null after execution"); await api.close(); }); it("should allow getting execution state", async () => { const configPath = join(projectRoot, "examples", "switch_example.yaml"); const api = new McpGraphApi(configPath); const hooks: ExecutionHooks = { onNodeStart: async () => true, }; // Start execution const executionPromise = api.executeTool("test_switch", { value: 5 }, { hooks }); // Try to get state (may be null if execution completes too quickly) const state = api.getExecutionState(); // State might be null if execution completes before we check // This is expected behavior await executionPromise; // After execution, state should be null const stateAfter = api.getExecutionState(); assert(stateAfter === null, "State should be null after execution"); await api.close(); }); }); describe("breakpoints", () => { it("should pause at breakpoints", async () => { const configPath = join(projectRoot, "examples", "switch_example.yaml"); const api = new McpGraphApi(configPath); let pausedAtNode: string | null = null; let resumeCalled = false; const hooks: ExecutionHooks = { onNodeStart: async (nodeId) => { if (nodeId === "switch_node") { pausedAtNode = nodeId; // Return false to pause return false; } return true; }, onPause: async (nodeId) => { pausedAtNode = nodeId; }, onResume: async () => { resumeCalled = true; }, }; // Start execution with breakpoint const executionPromise = api.executeTool("test_switch", { value: 5 }, { hooks, breakpoints: ["switch_node"], }); // Wait a bit for execution to start await new Promise((resolve) => setTimeout(resolve, 10)); const controller = api.getController(); if (controller) { const state = controller.getState(); // Execution might be paused or might have completed // This test verifies the breakpoint mechanism exists assert(state !== null, "State should be available"); } // Resume if paused if (controller && pausedAtNode) { controller.resume(); } await executionPromise; await api.close(); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/TeamSparkAI/mcpGraph'

If you have feedback or need assistance with the MCP directory API, please join our Discord server