Skip to main content
Glama

McFlow

compile-all.test.ts14.4 kB
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { WorkflowDeployer } from '../src/deploy'; import { WorkflowCompiler } from '../src/workflow-compiler'; import fs from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; import { execSync } from 'child_process'; // Mock child_process to ensure no deployment happens vi.mock('child_process', () => ({ execSync: vi.fn() })); const __dirname = path.dirname(fileURLToPath(import.meta.url)); const realWorkflowsPath = path.join(__dirname, '..'); describe('Compile All Functionality', () => { let testWorkflowsPath: string; let deployer: WorkflowDeployer; let compiler: WorkflowCompiler; beforeEach(async () => { // Clear all mocks vi.clearAllMocks(); // Create unique test directory for each test testWorkflowsPath = path.join(__dirname, `test-compile-all-${Date.now()}-${Math.random().toString(36).substring(7)}`); // Create test directory structure await fs.mkdir(path.join(testWorkflowsPath, 'workflows', 'flows'), { recursive: true }); await fs.mkdir(path.join(testWorkflowsPath, 'workflows', 'nodes', 'code'), { recursive: true }); await fs.mkdir(path.join(testWorkflowsPath, 'workflows', 'nodes', 'prompts'), { recursive: true }); await fs.mkdir(path.join(testWorkflowsPath, 'workflows', 'dist'), { recursive: true }); deployer = new WorkflowDeployer({ workflowsPath: testWorkflowsPath, n8nUrl: 'http://localhost:5678', }); compiler = new WorkflowCompiler(testWorkflowsPath); }); afterEach(async () => { // Clean up test directory if (testWorkflowsPath) { await fs.rm(testWorkflowsPath, { recursive: true, force: true }).catch(() => {}); } }); describe('compileAll without deployment', () => { it('should compile all workflows without triggering any deployment', async () => { // Create test workflows with various features const workflowWithCode = { name: 'workflow-with-code', description: 'Test workflow with external code', nodes: [ { name: 'Code Node', type: 'n8n-nodes-base.code', parameters: { nodeContent: { jsCode: 'external-code' } } } ], connections: {} }; const workflowWithPrompt = { name: 'workflow-with-prompt', description: 'Test workflow with external prompt', nodes: [ { name: 'AI Node', type: 'n8n-nodes-base.openAi', parameters: { nodeContent: { prompt: 'external-prompt' } } } ], connections: {} }; const simpleWorkflow = { name: 'simple-workflow', description: 'Simple workflow without external files', nodes: [ { name: 'Webhook', type: 'n8n-nodes-base.webhook', parameters: { path: '/test' } } ], connections: {} }; // Create external files await fs.writeFile( path.join(testWorkflowsPath, 'workflows', 'nodes', 'code', 'external-code.js'), 'console.log("This is external code");' ); await fs.writeFile( path.join(testWorkflowsPath, 'workflows', 'nodes', 'prompts', 'external-prompt.md'), 'This is an external prompt for the AI' ); // Save workflows await fs.writeFile( path.join(testWorkflowsPath, 'workflows', 'flows', 'workflow-with-code.json'), JSON.stringify(workflowWithCode, null, 2) ); await fs.writeFile( path.join(testWorkflowsPath, 'workflows', 'flows', 'workflow-with-prompt.json'), JSON.stringify(workflowWithPrompt, null, 2) ); await fs.writeFile( path.join(testWorkflowsPath, 'workflows', 'flows', 'simple-workflow.json'), JSON.stringify(simpleWorkflow, null, 2) ); // Mock execSync to track if it's called (it shouldn't be) const mockedExecSync = execSync as unknown as ReturnType<typeof vi.fn>; // Compile all with output to files await deployer.compileAll(true); // Verify NO deployment commands were executed expect(mockedExecSync).not.toHaveBeenCalled(); // Verify compiled files exist in dist const distFiles = await fs.readdir(path.join(testWorkflowsPath, 'workflows', 'dist')); expect(distFiles).toContain('workflow-with-code.json'); expect(distFiles).toContain('workflow-with-prompt.json'); expect(distFiles).toContain('simple-workflow.json'); // Verify the compiled workflows have injected content const compiledWithCode = JSON.parse( await fs.readFile( path.join(testWorkflowsPath, 'workflows', 'dist', 'workflow-with-code.json'), 'utf-8' ) ); expect(compiledWithCode.nodes[0].parameters.jsCode).toBe('console.log("This is external code");'); expect(compiledWithCode.nodes[0].parameters.nodeContent).toBeUndefined(); const compiledWithPrompt = JSON.parse( await fs.readFile( path.join(testWorkflowsPath, 'workflows', 'dist', 'workflow-with-prompt.json'), 'utf-8' ) ); expect(compiledWithPrompt.nodes[0].parameters.prompt).toBe('=This is an external prompt for the AI'); expect(compiledWithPrompt.nodes[0].parameters.nodeContent).toBeUndefined(); // Verify metadata preservation expect(compiledWithCode.description).toBe('Test workflow with external code'); expect(compiledWithPrompt.description).toBe('Test workflow with external prompt'); expect(compiledWithCode.updatedAt).toBeDefined(); expect(compiledWithPrompt.updatedAt).toBeDefined(); }); it('should compile without saving to files when output is false', async () => { const workflow = { name: 'test-workflow', description: 'Test workflow', nodes: [], connections: {} }; // Save workflow await fs.writeFile( path.join(testWorkflowsPath, 'workflows', 'flows', 'test-workflow.json'), JSON.stringify(workflow, null, 2) ); // Mock execSync const mockedExecSync = execSync as unknown as ReturnType<typeof vi.fn>; // Compile all WITHOUT output to files (using compiler directly) const compiledWorkflows = await compiler.compileAll(false); // Verify NO deployment commands were executed expect(mockedExecSync).not.toHaveBeenCalled(); // Verify workflows are returned in memory expect(compiledWorkflows.size).toBe(1); expect(compiledWorkflows.has('test-workflow.json')).toBe(true); // Verify NO files were saved to dist const distFiles = await fs.readdir(path.join(testWorkflowsPath, 'workflows', 'dist')); expect(distFiles.length).toBe(0); }); it('should handle the real ai-assistant-example workflow', async () => { const realCompiler = new WorkflowCompiler(realWorkflowsPath); // Mock execSync to ensure no deployment const mockedExecSync = execSync as unknown as ReturnType<typeof vi.fn>; // Create a temporary dist directory for testing const tempDistPath = path.join(testWorkflowsPath, 'test-dist'); await fs.mkdir(tempDistPath, { recursive: true }); // Compile the real workflows (in memory only) const compiledWorkflows = await realCompiler.compileAll(false); // Verify NO deployment commands were executed expect(mockedExecSync).not.toHaveBeenCalled(); // Check if ai-assistant-example was compiled const aiAssistantWorkflow = compiledWorkflows.get('ai-assistant-example.json'); expect(aiAssistantWorkflow).toBeDefined(); if (aiAssistantWorkflow) { // Verify the workflow has been properly compiled expect(aiAssistantWorkflow.name).toBe('AI Assistant Example'); expect(aiAssistantWorkflow.description).toBeDefined(); expect(aiAssistantWorkflow.id).toBe('ai-assistant-example'); expect(aiAssistantWorkflow.updatedAt).toBeDefined(); // Check that external content was injected const hasInjectedContent = aiAssistantWorkflow.nodes.some(node => node.parameters?.jsCode || node.parameters?.prompt || node.parameters?.messages?.messageValues ); expect(hasInjectedContent).toBe(true); // Verify no nodeContent remains const hasNodeContent = aiAssistantWorkflow.nodes.some(node => node.parameters?.nodeContent ); expect(hasNodeContent).toBe(false); } }); it('should update timestamps on each compilation', async () => { const workflow = { name: 'timestamp-test', description: 'Testing timestamp updates', nodes: [], connections: {} }; // Save workflow await fs.writeFile( path.join(testWorkflowsPath, 'workflows', 'flows', 'timestamp-test.json'), JSON.stringify(workflow, null, 2) ); // First compilation await compiler.compileAll(true); const firstCompiled = JSON.parse( await fs.readFile( path.join(testWorkflowsPath, 'workflows', 'dist', 'timestamp-test.json'), 'utf-8' ) ); const firstTimestamp = firstCompiled.updatedAt; // Wait a bit to ensure different timestamp await new Promise(resolve => setTimeout(resolve, 10)); // Second compilation await compiler.compileAll(true); const secondCompiled = JSON.parse( await fs.readFile( path.join(testWorkflowsPath, 'workflows', 'dist', 'timestamp-test.json'), 'utf-8' ) ); const secondTimestamp = secondCompiled.updatedAt; // Timestamps should be different expect(firstTimestamp).toBeDefined(); expect(secondTimestamp).toBeDefined(); expect(secondTimestamp).not.toBe(firstTimestamp); // Description should be preserved expect(secondCompiled.description).toBe('Testing timestamp updates'); }); it('should compile multiple workflows with mixed external content', async () => { // Create workflows with different types of external content const workflows = [ { name: 'mixed-content-1', nodes: [ { name: 'Code Node', type: 'n8n-nodes-base.code', parameters: { nodeContent: { jsCode: 'script1' } } }, { name: 'AI Node', type: 'n8n-nodes-base.openAi', parameters: { nodeContent: { prompt: 'prompt1' } } } ], connections: {} }, { name: 'mixed-content-2', nodes: [ { name: 'LangChain Node', type: '@n8n/n8n-nodes-langchain.chainLlm', parameters: { nodeContent: { prompt: 'prompt2' } } } ], connections: {} } ]; // Create external files await fs.writeFile( path.join(testWorkflowsPath, 'workflows', 'nodes', 'code', 'script1.js'), 'const result = "Script 1";' ); await fs.writeFile( path.join(testWorkflowsPath, 'workflows', 'nodes', 'prompts', 'prompt1.md'), 'Prompt 1 content' ); await fs.writeFile( path.join(testWorkflowsPath, 'workflows', 'nodes', 'prompts', 'prompt2.txt'), 'Prompt 2 content' ); // Save workflows for (const workflow of workflows) { await fs.writeFile( path.join(testWorkflowsPath, 'workflows', 'flows', `${workflow.name}.json`), JSON.stringify(workflow, null, 2) ); } // Mock execSync const mockedExecSync = execSync as unknown as ReturnType<typeof vi.fn>; // Compile all const compiledWorkflows = await compiler.compileAll(true); // Verify no deployment expect(mockedExecSync).not.toHaveBeenCalled(); // Verify all workflows were compiled expect(compiledWorkflows.size).toBe(2); // Check first workflow const compiled1 = JSON.parse( await fs.readFile( path.join(testWorkflowsPath, 'workflows', 'dist', 'mixed-content-1.json'), 'utf-8' ) ); expect(compiled1.nodes[0].parameters.jsCode).toBe('const result = "Script 1";'); expect(compiled1.nodes[1].parameters.prompt).toBe('=Prompt 1 content'); // Check second workflow (LangChain structure) const compiled2 = JSON.parse( await fs.readFile( path.join(testWorkflowsPath, 'workflows', 'dist', 'mixed-content-2.json'), 'utf-8' ) ); expect(compiled2.nodes[0].parameters.messages.messageValues[0].message).toBe('=Prompt 2 content'); }); }); describe('compile vs deploy distinction', () => { it('should only compile when using compileAll, not deploy', async () => { const workflow = { name: 'compile-only-test', nodes: [], connections: {} }; await fs.writeFile( path.join(testWorkflowsPath, 'workflows', 'flows', 'compile-only-test.json'), JSON.stringify(workflow, null, 2) ); const mockedExecSync = execSync as unknown as ReturnType<typeof vi.fn>; // Call compileAll await deployer.compileAll(true); // Should NOT call any n8n import commands expect(mockedExecSync).not.toHaveBeenCalled(); }); it('should deploy when using deployWorkflow', async () => { const workflow = { name: 'deploy-test', nodes: [], connections: {} }; const workflowPath = path.join(testWorkflowsPath, 'workflows', 'flows', 'deploy-test.json'); await fs.writeFile(workflowPath, JSON.stringify(workflow, null, 2)); const mockedExecSync = execSync as unknown as ReturnType<typeof vi.fn>; mockedExecSync.mockReturnValue('Successfully imported 1 workflow'); // Call deployWorkflow await deployer.deployWorkflow(workflowPath); // Should call n8n import command expect(mockedExecSync).toHaveBeenCalledTimes(1); expect(mockedExecSync.mock.calls[0][0]).toContain('n8n import:workflow'); }); }); });

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/mckinleymedia/mcflow-mcp'

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