UNPKG

vibe-coder-mcp

Version:

Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.

140 lines (139 loc) 8.64 kB
import { describe, it, expect, vi, beforeEach } from 'vitest'; import fs from 'fs-extra'; import * as toolRegistry from '../routing/toolRegistry.js'; import { loadWorkflowDefinitions, executeWorkflow } from './workflowExecutor.js'; import logger from '../../logger.js'; import { ConfigurationError } from '../../utils/errors.js'; vi.mock('fs-extra'); vi.mock('../routing/toolRegistry.js'); vi.spyOn(logger, 'info').mockImplementation(() => { }); vi.spyOn(logger, 'debug').mockImplementation(() => { }); vi.spyOn(logger, 'warn').mockImplementation(() => { }); vi.spyOn(logger, 'error').mockImplementation(() => { }); const mockConfig = { baseUrl: '', apiKey: '', geminiModel: '', perplexityModel: '' }; const mockWorkflowFileContent = JSON.stringify({ workflows: { testFlow: { description: "Test workflow", inputSchema: { inputParam: "string" }, steps: [ { id: "step1", toolName: "toolA", params: { p1: "{workflow.input.inputParam}" } }, { id: "step2", toolName: "toolB", params: { p2: "{steps.step1.output.content[0].text}" } } ], output: { finalMessage: "Step 2 output was: {steps.step2.output.content[0].text}" } }, failingFlow: { description: "Test workflow that fails", steps: [{ id: "failStep", toolName: "toolFail", params: {} }] } } }); const mockEmptyWorkflowFileContent = JSON.stringify({ workflows: {} }); const mockInvalidWorkflowFileContent = "{ invalid json"; describe('Workflow Executor', () => { const executeToolMock = vi.mocked(toolRegistry.executeTool); beforeEach(() => { vi.clearAllMocks(); vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue(mockEmptyWorkflowFileContent); loadWorkflowDefinitions('dummyPath'); }); describe('loadWorkflowDefinitions', () => { it('should load workflows from a valid file', () => { vi.mocked(fs.readFileSync).mockReturnValue(mockWorkflowFileContent); loadWorkflowDefinitions('validPath'); expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('Successfully loaded 2 workflow definitions.')); }); it('should handle missing workflow file', () => { vi.mocked(fs.existsSync).mockReturnValue(false); loadWorkflowDefinitions('missingPath'); expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('Workflow definition file not found')); }); it('should handle invalid JSON', () => { vi.mocked(fs.readFileSync).mockReturnValue(mockInvalidWorkflowFileContent); loadWorkflowDefinitions('invalidJsonPath'); expect(logger.error).toHaveBeenCalledWith(expect.objectContaining({ err: expect.any(SyntaxError) }), expect.any(String)); }); it('should handle invalid structure (missing workflows key)', () => { vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ not_workflows: {} })); loadWorkflowDefinitions('invalidStructPath'); expect(logger.error).toHaveBeenCalledWith(expect.objectContaining({ err: expect.any(ConfigurationError) }), expect.any(String)); expect(logger.error).toHaveBeenCalledWith(expect.objectContaining({ err: expect.objectContaining({ message: expect.stringContaining('"workflows" object missing') }) }), expect.any(String)); }); }); describe('executeWorkflow', () => { beforeEach(() => { vi.mocked(fs.readFileSync).mockReturnValue(mockWorkflowFileContent); loadWorkflowDefinitions('validPath'); }); it('should execute a workflow successfully', async () => { const step1Result = { content: [{ type: 'text', text: 'Step 1 Output' }], isError: false }; const step2Result = { content: [{ type: 'text', text: 'Step 2 Output' }], isError: false }; executeToolMock .mockResolvedValueOnce(step1Result) .mockResolvedValueOnce(step2Result); const workflowInput = { inputParam: 'Start Value' }; const result = await executeWorkflow('testFlow', workflowInput, mockConfig); expect(result.success).toBe(true); expect(result.message).toBe('Workflow "testFlow" completed successfully.'); expect(result.outputs?.finalMessage).toBe("Step 2 Output"); expect(executeToolMock).toHaveBeenCalledTimes(2); expect(executeToolMock).toHaveBeenNthCalledWith(1, 'toolA', { p1: 'Start Value' }, mockConfig); expect(executeToolMock).toHaveBeenNthCalledWith(2, 'toolB', { p2: 'Step 1 Output' }, mockConfig); expect(result.stepResults?.get('step1')).toBe(step1Result); expect(result.stepResults?.get('step2')).toBe(step2Result); }); it('should stop and return error if a step fails', async () => { const step1Result = { content: [{ type: 'text', text: 'Step 1 Output' }], isError: false }; const stepFailResult = { content: [{ type: 'text', text: 'Tool B Failed Msg' }], isError: true, errorDetails: { message: 'Tool B Failed Detail' } }; executeToolMock .mockResolvedValueOnce(step1Result) .mockResolvedValueOnce(stepFailResult); const workflowInput = { inputParam: 'Start Value' }; const result = await executeWorkflow('testFlow', workflowInput, mockConfig); expect(result.success).toBe(false); expect(result.message).toContain("failed at step 2 (toolB): Step 'step2' (Tool: toolB) failed: Tool B Failed Msg"); expect(result.error?.stepId).toBe('step2'); expect(result.error?.toolName).toBe('toolB'); expect(result.error?.message).toContain("Step 'step2' (Tool: toolB) failed: Tool B Failed Msg"); expect(result.error?.details?.toolResult).toEqual(stepFailResult); expect(executeToolMock).toHaveBeenCalledTimes(2); expect(result.stepResults?.size).toBe(2); expect(result.stepResults?.get('step1')).toBe(step1Result); expect(result.stepResults?.get('step2')).toBe(stepFailResult); }); it('should return error if parameter resolution fails', async () => { const brokenWorkflowContent = JSON.stringify({ workflows: { brokenFlow: { description: "Broken", steps: [{ id: "s2", toolName: "tB", params: { p: "{steps.s1.output.content[0].text}" } }] } } }); vi.mocked(fs.readFileSync).mockReturnValue(brokenWorkflowContent); loadWorkflowDefinitions('brokenPath'); const result = await executeWorkflow('brokenFlow', {}, mockConfig); expect(result.success).toBe(false); expect(result.message).toContain("failed at step 1 (tB)"); expect(result.message).toContain("Failed to resolve parameter 'p'"); expect(result.message).toContain("Output from step \"s1\" not found"); expect(result.error?.stepId).toBe('s2'); expect(result.error?.toolName).toBe('tB'); expect(executeToolMock).not.toHaveBeenCalled(); }); it('should return error if workflow definition not found', async () => { const result = await executeWorkflow('nonExistentFlow', {}, mockConfig); expect(result.success).toBe(false); expect(result.message).toBe('Workflow "nonExistentFlow" not found.'); expect(result.error?.message).toBe('Workflow "nonExistentFlow" not found.'); }); it('should handle resolving undefined paths gracefully in output', async () => { const workflowWithBadOutput = JSON.stringify({ workflows: { badOutputFlow: { description: "Test", steps: [{ id: "s1", toolName: "tA", params: {} }], output: { msg: "{steps.s1.output.nonexistent.path}" } } } }); vi.mocked(fs.readFileSync).mockReturnValue(workflowWithBadOutput); loadWorkflowDefinitions('badOutputPath'); executeToolMock.mockResolvedValue({ content: [{ type: 'text', text: 'Output' }], isError: false }); const result = await executeWorkflow('badOutputFlow', {}, mockConfig); expect(result.success).toBe(true); expect(result.outputs?.msg).toContain("Error: Failed to resolve output template"); expect(logger.warn).toHaveBeenCalledWith(expect.anything(), expect.stringContaining("Could not resolve output template key 'msg'")); }); }); });