Skip to main content
Glama
RunAgentTool.integration.test.ts25.4 kB
/** * Unit tests for RunAgentTool class * * Tests the run_agent tool implementation including parameter validation, * agent execution integration, and MCP response formatting. */ import { AgentManager } from 'src/agents/AgentManager' import { AgentExecutor, createExecutionConfig } from 'src/execution/AgentExecutor' import { RunAgentTool } from 'src/tools/RunAgentTool' import type { ServerConfigInterface } from 'src/types' import { beforeEach, describe, expect, it, vi } from 'vitest' describe('RunAgentTool', () => { let runAgentTool: RunAgentTool let mockAgentExecutor: AgentExecutor let mockAgentManager: AgentManager let mockConfig: ServerConfigInterface beforeEach(() => { mockConfig = { serverName: 'test-server', serverVersion: '1.0.0', agentsDir: './test-agents', agentType: 'cursor', logLevel: 'info', executionTimeoutMs: 300000, } const executionConfig = createExecutionConfig('cursor') mockAgentExecutor = new AgentExecutor(executionConfig) mockAgentManager = new AgentManager(mockConfig) // This will fail initially as RunAgentTool is not implemented runAgentTool = new RunAgentTool(mockAgentExecutor, mockAgentManager) }) describe('parameter validation', () => { it('should validate required agent parameter', async () => { const params = { prompt: 'Test prompt', // Missing agent parameter } const result = (await runAgentTool.execute(params)) as any expect(result.content).toBeDefined() const textContent = result.content.find((c: any) => c.type === 'text') expect(textContent?.text).toMatch(/agent.*required|missing.*agent/i) }) it('should validate required prompt parameter', async () => { const params = { agent: 'test-agent', // Missing prompt parameter } const result = (await runAgentTool.execute(params)) as any expect(result.content).toBeDefined() const textContent = result.content.find((c: any) => c.type === 'text') expect(textContent?.text).toMatch(/prompt.*required|missing.*prompt/i) }) it('should validate empty agent parameter', async () => { const params = { agent: '', prompt: 'Test prompt', } const result = (await runAgentTool.execute(params)) as any expect(result.content).toBeDefined() const textContent = result.content.find((c: any) => c.type === 'text') expect(textContent?.text).toMatch(/agent.*required|invalid.*agent/i) }) it('should validate empty prompt parameter', async () => { const params = { agent: 'test-agent', prompt: '', } const result = (await runAgentTool.execute(params)) as any expect(result.content).toBeDefined() const textContent = result.content.find((c: any) => c.type === 'text') expect(textContent?.text).toMatch(/prompt.*required|invalid.*prompt/i) }) it('should accept valid optional parameters', async () => { const params = { agent: 'test-agent', prompt: 'Test prompt', cwd: process.cwd(), extra_args: ['--verbose', '--debug'], } // Mock the execution to avoid actual agent execution vi.spyOn(mockAgentExecutor, 'executeAgent').mockResolvedValue({ stdout: 'Test output', stderr: '', exitCode: 0, executionTime: 100, hasResult: false, resultJson: undefined, estimatedOutputSize: 1024, }) const result = await runAgentTool.execute(params) expect(result).toBeDefined() }) it('should accept valid session_id parameter', async () => { const params = { agent: 'test-agent', prompt: 'Test prompt with session', cwd: process.cwd(), session_id: 'test-session-123', } // Mock agent existence check vi.spyOn(mockAgentManager, 'getAgent').mockResolvedValue({ name: 'test-agent', description: 'Test agent', content: 'Test agent content', filePath: '/test/agents/test-agent.md', lastModified: new Date(), }) // Mock the execution to avoid actual agent execution vi.spyOn(mockAgentExecutor, 'executeAgent').mockResolvedValue({ stdout: 'Test output with session', stderr: '', exitCode: 0, executionTime: 100, hasResult: false, resultJson: undefined, estimatedOutputSize: 1024, }) const result = await runAgentTool.execute(params) expect(result).toBeDefined() expect(result.isError).not.toBe(true) }) it('should reject empty session_id parameter', async () => { const params = { agent: 'test-agent', prompt: 'Test prompt', cwd: process.cwd(), session_id: '', } const result = (await runAgentTool.execute(params)) as any expect(result.content).toBeDefined() const textContent = result.content.find((c: any) => c.type === 'text') expect(textContent?.text).toMatch(/session.*id.*empty|invalid.*session/i) }) it('should reject session_id with invalid characters', async () => { const params = { agent: 'test-agent', prompt: 'Test prompt', cwd: process.cwd(), session_id: 'session/with/../invalid', } const result = (await runAgentTool.execute(params)) as any expect(result.content).toBeDefined() const textContent = result.content.find((c: any) => c.type === 'text') expect(textContent?.text).toMatch(/session.*id.*invalid.*characters/i) }) it('should reject session_id that is too long', async () => { const params = { agent: 'test-agent', prompt: 'Test prompt', cwd: process.cwd(), session_id: 'a'.repeat(101), // 101 characters, exceeds max of 100 } const result = (await runAgentTool.execute(params)) as any expect(result.content).toBeDefined() const textContent = result.content.find((c: any) => c.type === 'text') expect(textContent?.text).toMatch(/session.*id.*too long/i) }) it('should work without session_id for backward compatibility', async () => { const params = { agent: 'test-agent', prompt: 'Test prompt without session', cwd: process.cwd(), // No session_id provided } // Mock agent existence check vi.spyOn(mockAgentManager, 'getAgent').mockResolvedValue({ name: 'test-agent', description: 'Test agent', content: 'Test agent content', filePath: '/test/agents/test-agent.md', lastModified: new Date(), }) // Mock the execution to avoid actual agent execution vi.spyOn(mockAgentExecutor, 'executeAgent').mockResolvedValue({ stdout: 'Test output without session', stderr: '', exitCode: 0, executionTime: 100, hasResult: false, resultJson: undefined, estimatedOutputSize: 1024, }) const result = await runAgentTool.execute(params) expect(result).toBeDefined() expect(result.isError).not.toBe(true) }) }) describe('agent execution', () => { beforeEach(() => { // Mock agent existence check vi.spyOn(mockAgentManager, 'getAgent').mockResolvedValue({ name: 'test-agent', description: 'Test agent', content: 'Test agent content', filePath: '/test/agents/test-agent.md', lastModified: new Date(), }) }) it('should execute agent with valid parameters', async () => { const params = { agent: 'test-agent', prompt: 'Test prompt', cwd: process.cwd(), } const mockResult = { stdout: 'Agent execution result', stderr: '', exitCode: 0, executionTime: 150, hasResult: false, resultJson: undefined, } vi.spyOn(mockAgentExecutor, 'executeAgent').mockResolvedValue(mockResult) const result = await runAgentTool.execute(params) expect(result).toBeDefined() expect(result.content).toBeDefined() expect(Array.isArray(result.content)).toBe(true) expect(result.content.length).toBeGreaterThan(0) const textContent = result.content.find((c) => c.type === 'text') expect(textContent).toBeDefined() expect(textContent?.text).toContain('Agent execution result') }) it('should handle agent execution with stderr output', async () => { const params = { agent: 'test-agent', prompt: 'Test prompt', cwd: process.cwd(), } const mockResult = { stdout: 'Warning output', stderr: 'Non-critical warning message', exitCode: 0, executionTime: 200, hasResult: false, resultJson: undefined, } vi.spyOn(mockAgentExecutor, 'executeAgent').mockResolvedValue(mockResult) const result = await runAgentTool.execute(params) const textContent = result.content.find((c) => c.type === 'text') // content[0].text is now JSON string (ADR-0003) const parsedContent = JSON.parse(textContent?.text || '{}') expect(parsedContent.result).toContain('Warning output') // structuredContent should have result field (ADR-0003) expect(result.structuredContent).toHaveProperty('result') expect((result.structuredContent as any).result).toContain('Warning output') // stderr is agent implementation detail and should NOT be in structuredContent expect(result.structuredContent).not.toHaveProperty('stderr') }) it('should handle agent execution failure', async () => { const params = { agent: 'failing-agent', prompt: 'Test prompt', cwd: process.cwd(), } const mockResult = { stdout: '', stderr: 'Agent execution failed', exitCode: 1, executionTime: 50, hasResult: false, resultJson: undefined, } vi.spyOn(mockAgentExecutor, 'executeAgent').mockResolvedValue(mockResult) const result = await runAgentTool.execute(params) const textContent = result.content.find((c) => c.type === 'text') // content[0].text is now JSON string (ADR-0003) const parsedContent = JSON.parse(textContent?.text || '{}') expect(parsedContent.result).toContain('failed') // exit code is in structured content with snake_case naming (ADR-0003) expect(result.structuredContent).toHaveProperty('exit_code', 1) }) it('should include execution metadata in response', async () => { const params = { agent: 'test-agent', prompt: 'Test prompt', cwd: process.cwd(), } const mockResult = { stdout: 'Success', stderr: '', exitCode: 0, executionTime: 300, hasResult: false, resultJson: undefined, } vi.spyOn(mockAgentExecutor, 'executeAgent').mockResolvedValue(mockResult) const result = await runAgentTool.execute(params) const textContent = result.content.find((c) => c.type === 'text') // content[0].text is now JSON string (ADR-0003) const parsedContent = JSON.parse(textContent?.text || '{}') expect(parsedContent.result).toContain('Success') // Metadata is in structured content with snake_case naming (ADR-0003) expect(result.structuredContent).toHaveProperty('execution_time', 300) expect(result.structuredContent).toHaveProperty('exit_code', 0) }) }) describe('nonexistent agent handling', () => { it('should handle nonexistent agent gracefully', async () => { const params = { agent: 'nonexistent-agent', prompt: 'Test prompt', cwd: process.cwd(), } // Mock agent not found vi.spyOn(mockAgentManager, 'getAgent').mockResolvedValue(undefined) const result = await runAgentTool.execute(params) expect(result).toBeDefined() expect(result.content).toBeDefined() const textContent = result.content.find((c) => c.type === 'text') // content[0].text is now JSON string (ADR-0003) const parsedContent = JSON.parse(textContent?.text || '{}') expect(parsedContent.error).toMatch(/agent.*not found|nonexistent.*agent/i) }) it('should include available agents in error message', async () => { const params = { agent: 'nonexistent-agent', prompt: 'Test prompt', cwd: process.cwd(), } // Mock agent not found vi.spyOn(mockAgentManager, 'getAgent').mockResolvedValue(undefined) vi.spyOn(mockAgentManager, 'listAgents').mockResolvedValue([ { name: 'agent1', description: 'First agent', content: 'Content 1', filePath: '/test/agent1.md', lastModified: new Date(), }, { name: 'agent2', description: 'Second agent', content: 'Content 2', filePath: '/test/agent2.md', lastModified: new Date(), }, ]) const result = await runAgentTool.execute(params) const textContent = result.content.find((c) => c.type === 'text') // content[0].text is now JSON string (ADR-0003) const parsedContent = JSON.parse(textContent?.text || '{}') expect(parsedContent.available_agents).toContain('agent1') expect(parsedContent.available_agents).toContain('agent2') }) }) describe('response formatting', () => { beforeEach(() => { vi.spyOn(mockAgentManager, 'getAgent').mockResolvedValue({ name: 'test-agent', description: 'Test agent', content: 'Test content', filePath: '/test/test-agent.md', lastModified: new Date(), }) }) it('should format successful execution response correctly', async () => { const params = { agent: 'test-agent', prompt: 'Test prompt', cwd: process.cwd(), } const mockResult = { stdout: 'Agent output', stderr: '', exitCode: 0, executionTime: 100, executionMethod: 'exec' as const, estimatedOutputSize: 1024, } vi.spyOn(mockAgentExecutor, 'executeAgent').mockResolvedValue(mockResult) const result = await runAgentTool.execute(params) expect(result).toMatchObject({ content: expect.arrayContaining([ expect.objectContaining({ type: 'text', text: expect.stringContaining('Agent output'), }), ]), }) }) it('should include structured execution details in response', async () => { const params = { agent: 'test-agent', prompt: 'Test with details', cwd: process.cwd(), } const mockResult = { stdout: 'Detailed output', stderr: 'Warning message', exitCode: 0, executionTime: 250, hasResult: false, resultJson: undefined, } vi.spyOn(mockAgentExecutor, 'executeAgent').mockResolvedValue(mockResult) const result = await runAgentTool.execute(params) const textContent = result.content.find((c) => c.type === 'text') // content[0].text is now JSON string (ADR-0003) const parsedContent = JSON.parse(textContent?.text || '{}') expect(parsedContent.result).toContain('Detailed output') // MCP-managed metadata with snake_case naming (ADR-0003) expect(result.structuredContent).toHaveProperty('agent', 'test-agent') expect(result.structuredContent).toHaveProperty('exit_code', 0) expect(result.structuredContent).toHaveProperty('execution_time', 250) expect(result.structuredContent).toHaveProperty('result') // Agent implementation details should NOT be in structuredContent expect(result.structuredContent).not.toHaveProperty('stderr') }) }) describe('error handling', () => { it('should handle agent executor errors gracefully', async () => { const params = { agent: 'test-agent', prompt: 'Test prompt', cwd: process.cwd(), } vi.spyOn(mockAgentManager, 'getAgent').mockResolvedValue({ name: 'test-agent', description: 'Test agent', content: 'Test content', filePath: '/test/test-agent.md', lastModified: new Date(), }) // Mock executor throwing an error vi.spyOn(mockAgentExecutor, 'executeAgent').mockRejectedValue( new Error('Execution failed unexpectedly') ) const result = await runAgentTool.execute(params) expect(result).toBeDefined() const textContent = result.content.find((c) => c.type === 'text') expect(textContent?.text).toMatch(/error|failed/i) }) it('should handle agent manager errors gracefully', async () => { const params = { agent: 'test-agent', prompt: 'Test prompt', cwd: process.cwd(), } // Mock agent manager throwing an error vi.spyOn(mockAgentManager, 'getAgent').mockRejectedValue(new Error('Failed to load agent')) const result = await runAgentTool.execute(params) expect(result).toBeDefined() const textContent = result.content.find((c) => c.type === 'text') expect(textContent?.text).toMatch(/error.*loading|failed.*load/i) }) }) describe('exit code interpretation with hasResult', () => { beforeEach(() => { vi.spyOn(mockAgentManager, 'getAgent').mockResolvedValue({ name: 'test-agent', description: 'Test agent', content: 'Test content', filePath: '/test/test-agent.md', lastModified: new Date(), }) }) it('should treat exit code 124 with hasResult=false as complete timeout', async () => { const params = { agent: 'timeout-agent', prompt: 'Test complete timeout', cwd: process.cwd(), } // Mock complete timeout without any result const mockResult = { stdout: '', stderr: 'Execution timeout: 300000ms', exitCode: 124, // Timeout executionTime: 300000, hasResult: false, resultJson: undefined, } vi.spyOn(mockAgentExecutor, 'executeAgent').mockResolvedValue(mockResult) const result = await runAgentTool.execute(params) const textContent = result.content.find((c) => c.type === 'text') // content[0].text is now JSON string (ADR-0003) const parsedContent = JSON.parse(textContent?.text || '{}') expect(parsedContent.result).toBe('Execution timeout: 300000ms') expect(parsedContent.exit_code).toBe(124) expect(parsedContent.status).toBe('error') // Check error status in structuredContent with snake_case naming expect(result.structuredContent).toMatchObject({ status: 'error', exit_code: 124, }) }) it('should treat exit code 0 as success regardless of hasResult', async () => { const params = { agent: 'normal-agent', prompt: 'Test normal completion', cwd: process.cwd(), } // Mock normal completion const mockResult = { stdout: 'Normal output', stderr: '', exitCode: 0, executionTime: 100, hasResult: false, // Even without JSON result resultJson: undefined, } vi.spyOn(mockAgentExecutor, 'executeAgent').mockResolvedValue(mockResult) const result = await runAgentTool.execute(params) const textContent = result.content.find((c) => c.type === 'text') // content[0].text is now JSON string (ADR-0003) const parsedContent = JSON.parse(textContent?.text || '{}') expect(parsedContent.result).toBe('Normal output') expect(parsedContent.exit_code).toBe(0) expect(parsedContent.status).toBe('success') // Check success status in structuredContent with snake_case naming expect(result.structuredContent).toMatchObject({ status: 'success', exit_code: 0, }) }) }) describe('session ID auto-generation', () => { it('should auto-generate session_id when not provided and SessionManager is available', async () => { const mockSessionManager = { loadSession: vi.fn().mockResolvedValue(null), saveSession: vi.fn().mockResolvedValue(undefined), } const toolWithSession = new RunAgentTool( mockAgentExecutor, mockAgentManager, mockSessionManager as any ) const params = { agent: 'test-agent', prompt: 'Test prompt', cwd: process.cwd(), // No session_id provided } const mockResult = { stdout: 'Success', stderr: '', exitCode: 0, executionTime: 100, hasResult: false, resultJson: undefined, } // Mock agent existence vi.spyOn(mockAgentManager, 'getAgent').mockResolvedValue({ name: 'test-agent', description: 'Test agent', content: 'test content', } as any) vi.spyOn(mockAgentExecutor, 'executeAgent').mockResolvedValue(mockResult) const result = await toolWithSession.execute(params) // Should have _meta.session_id in response expect(result).toHaveProperty('_meta') expect(result._meta).toHaveProperty('session_id') expect(typeof result._meta?.session_id).toBe('string') expect(result._meta?.session_id).toMatch( /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ ) // Should have saved session with auto-generated ID expect(mockSessionManager.saveSession).toHaveBeenCalledWith( expect.stringMatching(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/), expect.anything(), expect.anything() ) }) it('should use provided session_id instead of auto-generating', async () => { const mockSessionManager = { loadSession: vi.fn().mockResolvedValue(null), saveSession: vi.fn().mockResolvedValue(undefined), } const toolWithSession = new RunAgentTool( mockAgentExecutor, mockAgentManager, mockSessionManager as any ) const params = { agent: 'test-agent', prompt: 'Test prompt', cwd: process.cwd(), session_id: 'my-custom-session', } const mockResult = { stdout: 'Success', stderr: '', exitCode: 0, executionTime: 100, hasResult: false, resultJson: undefined, } // Mock agent existence vi.spyOn(mockAgentManager, 'getAgent').mockResolvedValue({ name: 'test-agent', description: 'Test agent', content: 'test content', } as any) vi.spyOn(mockAgentExecutor, 'executeAgent').mockResolvedValue(mockResult) const result = await toolWithSession.execute(params) // Should return the provided session_id expect(result._meta?.session_id).toBe('my-custom-session') // Should have saved with provided session_id expect(mockSessionManager.saveSession).toHaveBeenCalledWith( 'my-custom-session', expect.anything(), expect.anything() ) }) it('should not auto-generate session_id when SessionManager is not available', async () => { const toolWithoutSession = new RunAgentTool(mockAgentExecutor, mockAgentManager, undefined) const params = { agent: 'test-agent', prompt: 'Test prompt', cwd: process.cwd(), // No session_id provided } const mockResult = { stdout: 'Success', stderr: '', exitCode: 0, executionTime: 100, hasResult: false, resultJson: undefined, } vi.spyOn(mockAgentExecutor, 'executeAgent').mockResolvedValue(mockResult) const result = await toolWithoutSession.execute(params) // Should not have _meta.session_id when SessionManager is not available expect(result._meta).toBeUndefined() }) it('should include session_id in both _meta and structuredContent', async () => { const mockSessionManager = { loadSession: vi.fn().mockResolvedValue(null), saveSession: vi.fn().mockResolvedValue(undefined), } const toolWithSession = new RunAgentTool( mockAgentExecutor, mockAgentManager, mockSessionManager as any ) const params = { agent: 'test-agent', prompt: 'Test prompt', cwd: process.cwd(), session_id: 'test-session-123', } const mockResult = { stdout: 'Success', stderr: '', exitCode: 0, executionTime: 100, hasResult: false, resultJson: undefined, } // Mock agent existence vi.spyOn(mockAgentManager, 'getAgent').mockResolvedValue({ name: 'test-agent', description: 'Test agent', content: 'test content', } as any) vi.spyOn(mockAgentExecutor, 'executeAgent').mockResolvedValue(mockResult) const result = await toolWithSession.execute(params) // session_id in _meta and structuredContent (ADR-0003) expect(result._meta?.session_id).toBe('test-session-123') expect(result.structuredContent).toHaveProperty('session_id', 'test-session-123') }) }) })

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/shinpr/sub-agents-mcp'

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