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
JavaScript
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'"));
});
});
});