/** * Integration tests for the FastAgent API * TypeScript port of test_api.py */ import { FastAgent } from '../../../src/fastAgent'; import { BaseAgent } from '../../../src/mcpAgent'; import { Prompt, PromptMessageMultipart } from '../../../src/core/prompt'; import * as directFactory from '../../../src/core/directFactory'; // Define a type for the message object to avoid TypeScript errors interface MessageContent { content?: Array<{ text?: string; type?: string; }>; role?: string; } // --- Define Mock LLMs --- const passthroughLlm = { send: jest.fn().mockImplementation((message: string | MessageContent) => { // If message is a string, return it directly if (typeof message === 'string') { return Promise.resolve(message); } // If message is an object with content, extract the text if (message && message.content && Array.isArray(message.content)) { const text = message.content .map((item) => (item.text || '').toString()) .join('\n'); return Promise.resolve(text); } // Fallback: stringify the message return Promise.resolve(JSON.stringify(message)); }) }; const playbackLlm = { send: jest.fn().mockImplementation(() => { return Promise.resolve(Prompt.assistant("assistant1")); }) }; const defaultLlm = { send: jest.fn() }; // Fallback // Mock the directFactory module jest.mock('../../../src/core/directFactory', () => ({ getModelFactory: jest.fn().mockImplementation((_context, modelName) => { // This mock should return a FACTORY FUNCTION return () => { // Determine which mock LLM to return based on the agent's model name if (modelName === 'passthrough') { return passthroughLlm; } else if (modelName === 'playback') { return playbackLlm; } // Default or throw error if unexpected model name console.warn(`Mock LLM requested for unexpected model: ${modelName}. Using default.`); return defaultLlm; }; }), })); // Helper function to convert PromptMessageMultipart to string for testing function stringifyPrompt(prompt: PromptMessageMultipart): string { if (prompt && prompt.content && Array.isArray(prompt.content)) { return prompt.content .map((item: any) => (item.text || '').toString()) .join('\n'); } return ''; } // --- Tests --- describe('FastAgent API Integration Tests', () => { let fast: FastAgent; beforeEach(() => { // Reset mocks before each test // Clear calls to the outer mock factory function (directFactory.getModelFactory as jest.Mock).mockClear(); // Clear calls on the individual mock LLM's send methods passthroughLlm.send.mockClear(); playbackLlm.send.mockClear(); defaultLlm.send.mockClear(); // Create a new FastAgent instance for each test fast = new FastAgent('testApiInstance'); // Prevent actual config loading during tests (fast as any).loadConfig = jest.fn(); // Mock the config (fast as any).context = { config: { default_model: 'passthrough', mcp: { servers: { prompts: { // Config for prompts server }, cwd_test: { // Config for cwd test server } } } } }; }); it('should process simple prompts', async () => { // Define agent with default model fast.agent({ name: 'agent1', instruction: 'You are a helpful AI Agent', model: 'passthrough' }, async (agent: BaseAgent) => { // Test direct send method const result1 = await agent.send('test1'); expect(result1).toBe('test1'); // Test accessing agent by name (if supported in TypeScript implementation) // This may need to be adjusted based on actual implementation if ((agent as any)['agent1'] && typeof (agent as any)['agent1'].send === 'function') { const result2 = await (agent as any)['agent1'].send('test2'); expect(result2).toBe('test2'); } // Test standard send method again const result3 = await agent.send('test3'); expect(result3).toBe('test3'); // Note: The function call syntax (agent('test4')) and agent.send('test5', 'agent1') // are not supported in the TypeScript implementation based on the BaseAgent interface // Assert mocks expect(passthroughLlm.send).toHaveBeenCalled(); }); // Run FastAgent await fast.run(); }); it('should process prompt messages', async () => { // Define agent with default model fast.agent({ name: 'agent1', instruction: 'You are a helpful AI Agent', model: 'passthrough' }, async (agent: BaseAgent) => { // Create a string version of the prompt for testing const promptText = 'test1'; const promptObj = Prompt.user(promptText); // Mock the LLM to return the string content when given a PromptMessageMultipart passthroughLlm.send.mockImplementationOnce(() => Promise.resolve(promptText)); // Test with PromptMessage - we need to cast agent to any to access agent1 if ((agent as any).agent1 && typeof (agent as any).agent1.send === 'function') { const result = await (agent as any).agent1.send(promptText); expect(result).toBe(promptText); } else { // Fallback if agent1 property doesn't exist const result = await agent.send(promptText); expect(result).toBe(promptText); } // Assert mocks expect(passthroughLlm.send).toHaveBeenCalled(); }); // Run FastAgent await fast.run(); }); it('should handle basic playback', async () => { // Define agent with playback model fast.agent({ name: 'agent1', instruction: 'You are a helpful AI Agent', model: 'playback', servers: ['prompts'] }, async (agent: BaseAgent) => { // We need to cast agent to any to access agent1 and apply_prompt if ((agent as any).agent1 && typeof (agent as any).agent1.applyPrompt === 'function') { // Apply prompt template await (agent as any).agent1.applyPrompt('playback'); // Test with any message (ignored by playback) const result = await (agent as any).agent1.send('ignored'); expect(result).toContain('assistant1'); } else { // Fallback if agent1 property doesn't exist // Use the agent's applyPrompt method directly await agent.applyPrompt('playback', {}); // Test with any message (ignored by playback) const result = await agent.send('ignored'); expect(result).toContain('assistant1'); } // Assert mocks expect(playbackLlm.send).toHaveBeenCalled(); }); // Run FastAgent await fast.run(); }); it('should handle default calls', async () => { // Define agent with passthrough model fast.agent({ name: 'agent1', instruction: 'You are a helpful AI Agent', model: 'passthrough' }, async (agent: BaseAgent) => { // Test with direct send method const result1 = await agent.send('message 1'); expect(result1).toBe('message 1'); // Test with indexed access if supported if ((agent as any)['agent1'] && typeof (agent as any)['agent1'].send === 'function') { const result2 = await (agent as any)['agent1'].send('message 2'); expect(result2).toBe('message 2'); } // Assert mocks expect(passthroughLlm.send).toHaveBeenCalled(); }); // Run FastAgent await fast.run(); }); it('should handle mixed message types', async () => { // Define agent with passthrough model fast.agent({ name: 'agent1', instruction: 'You are a helpful AI Agent', model: 'passthrough' }, async (agent: BaseAgent) => { // Test with string passthroughLlm.send.mockImplementationOnce(() => Promise.resolve('string message')); const stringResult = await agent.send('string message'); expect(stringResult).toBe('string message'); // Test with PromptMessage (using Prompt.user which creates a PromptMessageMultipart) // We need to mock the LLM to handle the PromptMessageMultipart const promptMessage = 'prompt message'; passthroughLlm.send.mockImplementationOnce(() => Promise.resolve(promptMessage)); // Since agent.send only accepts string, we'll test with the string directly const promptResult = await agent.send(promptMessage); expect(promptResult).toBe(promptMessage); // Test with PromptMessageMultipart directly const multipartMessage = 'multipart message'; passthroughLlm.send.mockImplementationOnce(() => Promise.resolve(multipartMessage)); // Since agent.send only accepts string, we'll test with the string directly const multipartResult = await agent.send(multipartMessage); expect(multipartResult).toBe(multipartMessage); // Test message history access (if available in TypeScript) passthroughLlm.send.mockImplementationOnce(() => Promise.resolve('checking history')); await agent.send('checking history'); // If message history is accessible in TypeScript if ((agent as any)._llm && (agent as any)._llm.messageHistory) { const messageHistory = (agent as any)._llm.messageHistory; // Basic assertions expect(messageHistory.length).toBeGreaterThanOrEqual(8); // 4 user messages + 4 assistant responses // Check our specific user messages are there (if structure allows) const userMessages = messageHistory .filter((msg: MessageContent) => msg.role === 'user') .map((msg: MessageContent) => { if (msg.content && Array.isArray(msg.content)) { return msg.content.map((item) => item.text || '').join('\n'); } return ''; }); expect(userMessages).toContain('string message'); expect(userMessages).toContain('prompt message'); expect(userMessages).toContain('multipart message'); expect(userMessages).toContain('checking history'); } // Assert mocks expect(passthroughLlm.send).toHaveBeenCalled(); }); // Run FastAgent await fast.run(); }); it('should specify cwd for server', async () => { // Define agent with playback model fast.agent({ name: 'agent1', instruction: 'You are a helpful AI Agent', model: 'playback', servers: ['cwd_test'] }, async (agent: BaseAgent) => { // Mock the response for this test playbackLlm.send.mockResolvedValueOnce('how may i help you'); // Apply prompt template - we need to cast agent to any to access agent1 if ((agent as any).agent1 && typeof (agent as any).agent1.applyPrompt === 'function') { await (agent as any).agent1.applyPrompt('multi'); // Test with message const result = await (agent as any).agent1.send('cwd_test'); expect(result.toLowerCase()).toContain('how may i'); } else { // Fallback if agent1 property doesn't exist await agent.applyPrompt('multi', {}); // Test with message const result = await agent.send('cwd_test'); expect(result.toLowerCase()).toContain('how may i'); } // Assert mocks expect(playbackLlm.send).toHaveBeenCalled(); }); // Run FastAgent await fast.run(); }); });