Skip to main content
Glama
jmandel

Smart EHR MCP Server

by jmandel
TaskLiaison.integration.test.ts14.2 kB
// File: a4a/client/test/browser/TaskLiaison.integration.test.ts import { describe, test, expect } from './testRunner.js'; import { TaskLiaison } from '../../src/TaskLiaison.js'; import type { TaskLiaisonSnapshot, // Removed TaskLiaisonConfig, UserFacingSummaryView } from '../../src/TaskLiaison.js'; import type { ClosePayload, ErrorPayload, ClientCloseReason, A2AClientConfig } from '../../src/A2AClient.js'; // Import A2AClientConfig import { Task, Message, TextPart, TaskState, TaskSendParams, Artifact } from '../../src/types.js'; // --- Test View Types --- // REMOVED TestSummaryView interface // REMOVED DefaultPromptView interface // --- Test Strategies (REMOVED - Using defaults for now) --- // --- Constants --- const JOKE_AGENT_URL = 'http://localhost:3100/a2a'; const JOKE_AGENT_CARD_URL = 'http://localhost:3100/.well-known/agent.json'; const POLL_INTERVAL_MS = 500; const TEST_TIMEOUT = 20000; const LONG_TEST_TIMEOUT = 30000; const VERY_LONG_TEST_TIMEOUT = 40000; // --- Helper Functions --- // Helper to wait for the TaskLiaison's snapshot to meet a condition // Removed generics <TSummary extends UserFacingSummaryView, TPrompt> async function waitForLiaisonSnapshot( liaison: TaskLiaison | null, conditionFn: (snapshot: TaskLiaisonSnapshot | null) => boolean, errorMessage = 'TaskLiaison snapshot condition not met', timeout = TEST_TIMEOUT ): Promise<TaskLiaisonSnapshot | null> { // Use non-generic TaskLiaisonSnapshot type CurrentSnapshotType = TaskLiaisonSnapshot | null; return new Promise((resolve, reject) => { if (!liaison) { if (conditionFn(null)) { resolve(null); } else { reject(new Error(`Liaison was already null. ${errorMessage}`)); } return; } let lastSnapshot: CurrentSnapshotType = liaison.getCurrentSnapshot(); // Listener takes (prev, current) but conditionFn usually just needs current let listener: ((prev: TaskLiaisonSnapshot | null, current: TaskLiaisonSnapshot) => void) | null = null; const timer = setTimeout(() => { // Use offTransition if (listener && liaison) liaison.offTransition(listener); const currentSnap = liaison?.getCurrentSnapshot(); const finalStateDesc = currentSnap ? `State: ${currentSnap.liaisonState}, TaskId: ${currentSnap.task?.id}` : 'Liaison destroyed'; reject(new Error(`${errorMessage} within ${timeout}ms. Last snapshot: ${finalStateDesc}`)); }, timeout); // Listener takes prevSnapshot and currentSnapshot listener = (prevSnapshot: TaskLiaisonSnapshot | null, currentSnapshot: TaskLiaisonSnapshot) => { lastSnapshot = currentSnapshot; // Store current as last for logging/debugging if needed console.log(`waitForLiaisonSnapshot: Checking - State: ${currentSnapshot.liaisonState}, TaskId: ${currentSnapshot.task?.id}`); // Pass currentSnapshot to the condition function if (conditionFn(currentSnapshot)) { console.log("waitForLiaisonSnapshot: conditionFn(currentSnapshot) true"); clearTimeout(timer); // Use offTransition if (listener && liaison) liaison.offTransition(listener); resolve(currentSnapshot); } }; const initialSnapshot = liaison.getCurrentSnapshot(); // Check initial state against conditionFn if (conditionFn(initialSnapshot)) { console.log("waitForLiaisonSnapshot: conditionFn(initialSnapshot) true"); clearTimeout(timer); resolve(initialSnapshot); return; } // Use onTransition liaison.onTransition(listener); }); } // --- Test Suite --- describe('TaskLiaison (Integration with Joke Agent)', () => { // Define types for this test suite - REMOVED view types // type CurrentSummaryView = TestSummaryView; // type CurrentPromptView = DefaultPromptView; test('should instantiate TaskLiaison with defaults', () => { // Liaison takes no config or empty config // const config: TaskLiaisonConfig<CurrentSummaryView, CurrentPromptView> = {}; const liaison = new TaskLiaison({ agentEndpointUrl: JOKE_AGENT_URL }); expect(liaison).toBeDefined(); const initialSnap = liaison.getCurrentSnapshot(); expect(initialSnap.liaisonState).toBe('idle'); // REMOVED summaryView and promptView checks // expect(initialSnap.summaryView.label).toBe("Task"); // expect(initialSnap.summaryView.detail).toBe("Initializing..."); expect(initialSnap.task).toBeNull(); // expect(initialSnap.promptView).toBeNull(); liaison.closeTask(); }); // REMOVED test: 'should instantiate TaskLiaison with custom initial view' // test('should instantiate TaskLiaison with custom initial view', () => { ... }); // --- startTask Tests --- test('should start a simple tell-joke task and complete successfully', async () => { // No config needed // const config: TaskLiaisonConfig<CurrentSummaryView, CurrentPromptView> = { // initialSummaryView: createDefaultSummaryView("Tell Joke Test") as CurrentSummaryView // }; const liaison = new TaskLiaison({ agentEndpointUrl: JOKE_AGENT_URL }); // Use onTransition liaison.onTransition((prevSnapshot, currentSnapshot) => { console.log(`[Tell Joke Test Listener] State: ${currentSnapshot.liaisonState}, TaskId: ${currentSnapshot.task?.id}`); }); try { const initialMessage: Message = { role: 'user', parts: [{ type: 'text', text: 'Liaison integration joke test' }] }; const startParams: TaskSendParams = { message: initialMessage, metadata: { skillId: 'tell-joke' } }; const clientConfig: A2AClientConfig = { // Use explicit type agentEndpointUrl: JOKE_AGENT_URL, agentCardUrl: JOKE_AGENT_CARD_URL, getAuthHeaders: async () => ({}), pollIntervalMs: POLL_INTERVAL_MS }; console.log('[Tell Joke Test] Calling liaison.startTask...'); const startPromise = liaison.startTask(startParams, clientConfig); // Call waitForLiaisonSnapshot without generics await waitForLiaisonSnapshot(liaison, snap => snap?.liaisonState !== 'idle', 'Liaison did not move past idle state'); console.log(`[Tell Joke Test] Liaison state after start initiated: ${liaison.getCurrentSnapshot().liaisonState}`); await startPromise; // Wait for client connection/polling to start console.log(`[Tell Joke Test] liaison.startTask promise resolved. State: ${liaison.getCurrentSnapshot().liaisonState}`); console.log('[Tell Joke Test] Waiting for liaison completion snapshot...'); // Call waitForLiaisonSnapshot without generics const finalSnapshot = await waitForLiaisonSnapshot( liaison, snap => snap?.liaisonState === 'closed', 'Liaison did not reach the closed state', LONG_TEST_TIMEOUT ); expect(finalSnapshot?.liaisonState).toBe('closed'); expect(finalSnapshot?.task).toBeDefined(); const finalTask = finalSnapshot?.task; if (!finalTask) throw new Error("Test assertion failed: finalTask is undefined in snapshot"); expect(finalTask.status.state).toBe('completed'); expect(finalTask.artifacts).toHaveLength(1); const jokeArtifact = finalTask.artifacts?.[0]; expect(jokeArtifact?.name).toBe('joke-result'); let jokeText: string | undefined; const part = jokeArtifact?.parts?.[0]; if (part?.type === 'text') jokeText = part.text; expect(jokeText).toBeDefined(); expect((jokeText ?? '').length).toBeGreaterThan(5); console.log(`[Tell Joke Test] Received joke: ${jokeText}`); } finally { const currentSnapBeforeFinally = liaison?.getCurrentSnapshot(); if (liaison && currentSnapBeforeFinally && currentSnapBeforeFinally.liaisonState !== 'closed') { await liaison.closeTask(); // Call waitForLiaisonSnapshot without generics await waitForLiaisonSnapshot(liaison, snap => snap?.liaisonState === 'closed', 'Liaison state did not become closed after close'); } } }, LONG_TEST_TIMEOUT); // --- Input Required Test --- test('should handle input-required using provideInput', async () => { const topicToSend = "robots"; // No config needed // const config: TaskLiaisonConfig<CurrentSummaryView, CurrentPromptView> = { // // Relying on default strategies // initialSummaryView: { label: 'Input Test' } // }; const liaison = new TaskLiaison({ agentEndpointUrl: JOKE_AGENT_URL }); let finalSnapshotFromEvent: TaskLiaisonSnapshot | null = null; // Variable to store the snapshot from the event // Use onTransition liaison.onTransition((prevSnapshot, currentSnapshot) => { // Removed promptView from log console.log(`[Input Test Listener] State: ${currentSnapshot.liaisonState}, TaskState: ${currentSnapshot.task?.status?.state}`); // Capture the snapshot when the desired final state is reached if (currentSnapshot.liaisonState === 'closed') { console.log("[Input Test Listener] Capturing final closed snapshot from event."); finalSnapshotFromEvent = currentSnapshot; } }); try { const initialMessage: Message = { role: 'user', parts: [{ type: 'text', text: 'Tell me a joke about...' }] }; const startParams: TaskSendParams = { message: initialMessage, metadata: { skillId: 'jokeAboutTopic' } }; const clientConfig: A2AClientConfig = { agentEndpointUrl: JOKE_AGENT_URL, agentCardUrl: JOKE_AGENT_CARD_URL, getAuthHeaders: async () => ({}), pollIntervalMs: POLL_INTERVAL_MS }; console.log('[Input Test] Calling liaison.startTask...'); liaison.startTask(startParams, clientConfig); // Don't await fully here console.log('[Input Test] Waiting for awaiting-input state...'); // Call waitForLiaisonSnapshot without generics const inputSnapshot = await waitForLiaisonSnapshot( liaison, snap => snap?.liaisonState === 'awaiting-input', 'Liaison did not reach awaiting-input state', LONG_TEST_TIMEOUT ); console.log("Input Test: Awaiting-input state reached:" + JSON.stringify(inputSnapshot)); expect(inputSnapshot?.liaisonState).toBe('awaiting-input'); expect(inputSnapshot?.task?.status?.state).toBe('input-required'); // Check prompt text from task.status.message instead of promptView const promptMessage = inputSnapshot?.task?.status?.message; expect(promptMessage).toBeDefined(); const promptPart = promptMessage?.parts?.find(p => p.type === 'text') as TextPart | undefined; expect(promptPart).toBeDefined(); const promptText: string | undefined = promptPart?.text; // expect(inputSnapshot?.promptView).toBeDefined(); // expect(inputSnapshot?.promptView?.promptMessage).toBeDefined(); // const promptPart = inputSnapshot?.promptView?.promptMessage?.parts?.[0]; // let promptText: string | undefined; // if (promptPart?.type === 'text') promptText = promptPart.text; expect(promptText).toBeDefined(); expect(promptText).toContain('topic'); console.log(`[Input Test] Prompt text verified: ${promptText}`); // --- Simulate providing input --- console.log(`[Input Test] Calling provideInput with topic: ${topicToSend}`); const responseMessage: Message = { role: 'user', parts: [{ type: 'text', text: topicToSend }] }; await liaison.provideInput(responseMessage); console.log('[Input Test] provideInput finished.'); // --- Wait for final completion --- console.log('[Input Test] Waiting for final closed state (using waitForLiaisonSnapshot just for state)...'); // Use waitForLiaisonSnapshot just to ensure the state *becomes* closed await waitForLiaisonSnapshot( liaison, snap => snap?.liaisonState === 'closed', 'Liaison did not reach the closed state after input', LONG_TEST_TIMEOUT ); // Now, use the snapshot captured by the listener console.log('[Input Test] Asserting using snapshot captured from the onTransition event.'); // Use expect().not.toBeNull() and then non-null assertion expect(finalSnapshotFromEvent).not.toBeNull(); expect(finalSnapshotFromEvent!.liaisonState).toBe('closed'); const finalTask = finalSnapshotFromEvent!.task; if (!finalTask) throw new Error("Test assertion failed: finalTask is undefined in captured snapshot"); expect(finalTask.status.state).toBe('completed'); let jokeText: string | undefined; const jokePart = finalTask.artifacts?.[0]?.parts?.[0]; if (jokePart?.type === 'text') { jokeText = jokePart.text; } expect(jokeText).toBeDefined(); expect((jokeText ?? '').toLowerCase()).toContain(topicToSend); console.log(`[Input Test] Received topic joke: ${jokeText}`); } finally { } }, VERY_LONG_TEST_TIMEOUT); });

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/jmandel/health-record-mcp'

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