/** * Integration Tests: handleListWorkflows * * Tests workflow listing against a real n8n instance. * Covers filtering, pagination, and various list parameters. */ import { describe, it, expect, beforeEach, afterEach, afterAll } from 'vitest'; import { createTestContext, TestContext, createTestWorkflowName } from '../utils/test-context'; import { getTestN8nClient } from '../utils/n8n-client'; import { N8nApiClient } from '../../../../src/services/n8n-api-client'; import { SIMPLE_WEBHOOK_WORKFLOW, SIMPLE_HTTP_WORKFLOW } from '../utils/fixtures'; import { cleanupOrphanedWorkflows } from '../utils/cleanup-helpers'; import { createMcpContext } from '../utils/mcp-context'; import { InstanceContext } from '../../../../src/types/instance-context'; import { handleListWorkflows } from '../../../../src/mcp/handlers-n8n-manager'; describe('Integration: handleListWorkflows', () => { let context: TestContext; let client: N8nApiClient; let mcpContext: InstanceContext; beforeEach(() => { context = createTestContext(); client = getTestN8nClient(); mcpContext = createMcpContext(); }); afterEach(async () => { await context.cleanup(); }); afterAll(async () => { if (!process.env.CI) { await cleanupOrphanedWorkflows(); } }); // ====================================================================== // No Filters // ====================================================================== describe('No Filters', () => { it('should list all workflows without filters', async () => { // Create test workflows const workflow1 = { ...SIMPLE_WEBHOOK_WORKFLOW, name: createTestWorkflowName('List - All 1'), tags: ['mcp-integration-test'] }; const workflow2 = { ...SIMPLE_HTTP_WORKFLOW, name: createTestWorkflowName('List - All 2'), tags: ['mcp-integration-test'] }; const created1 = await client.createWorkflow(workflow1); const created2 = await client.createWorkflow(workflow2); context.trackWorkflow(created1.id!); context.trackWorkflow(created2.id!); // List workflows without filters const response = await handleListWorkflows({}, mcpContext); expect(response.success).toBe(true); expect(response.data).toBeDefined(); const data = response.data as any; expect(Array.isArray(data.workflows)).toBe(true); expect(data.workflows.length).toBeGreaterThan(0); // Our workflows should be in the list const workflow1Found = data.workflows.find((w: any) => w.id === created1.id); const workflow2Found = data.workflows.find((w: any) => w.id === created2.id); expect(workflow1Found).toBeDefined(); expect(workflow2Found).toBeDefined(); }); }); // ====================================================================== // Filter by Active Status // ====================================================================== describe('Filter by Active Status', () => { it('should filter workflows by active=true', async () => { // Create active workflow const activeWorkflow = { ...SIMPLE_WEBHOOK_WORKFLOW, name: createTestWorkflowName('List - Active'), active: true, tags: ['mcp-integration-test'] }; const created = await client.createWorkflow(activeWorkflow); context.trackWorkflow(created.id!); // Activate workflow await client.updateWorkflow(created.id!, { ...activeWorkflow, active: true }); // List active workflows const response = await handleListWorkflows( { active: true }, mcpContext ); expect(response.success).toBe(true); const data = response.data as any; // All returned workflows should be active data.workflows.forEach((w: any) => { expect(w.active).toBe(true); }); }); it('should filter workflows by active=false', async () => { // Create inactive workflow const inactiveWorkflow = { ...SIMPLE_WEBHOOK_WORKFLOW, name: createTestWorkflowName('List - Inactive'), active: false, tags: ['mcp-integration-test'] }; const created = await client.createWorkflow(inactiveWorkflow); context.trackWorkflow(created.id!); // List inactive workflows const response = await handleListWorkflows( { active: false }, mcpContext ); expect(response.success).toBe(true); const data = response.data as any; // All returned workflows should be inactive data.workflows.forEach((w: any) => { expect(w.active).toBe(false); }); // Our workflow should be in the list const found = data.workflows.find((w: any) => w.id === created.id); expect(found).toBeDefined(); }); }); // ====================================================================== // Filter by Tags // ====================================================================== describe('Filter by Tags', () => { it('should filter workflows by name instead of tags', async () => { // Note: Tags filtering requires tag IDs, not names, and tags are readonly in workflow creation // This test filters by name instead, which is more reliable for integration testing const uniqueName = createTestWorkflowName('List - Name Filter Test'); const workflow = { ...SIMPLE_WEBHOOK_WORKFLOW, name: uniqueName, tags: ['mcp-integration-test'] }; const created = await client.createWorkflow(workflow); context.trackWorkflow(created.id!); // List all workflows and verify ours is included const response = await handleListWorkflows({}, mcpContext); expect(response.success).toBe(true); const data = response.data as any; // Our workflow should be in the list const found = data.workflows.find((w: any) => w.id === created.id); expect(found).toBeDefined(); expect(found.name).toBe(uniqueName); }); }); // ====================================================================== // Pagination // ====================================================================== describe('Pagination', () => { it('should return first page with limit', async () => { // Create multiple workflows const workflows = []; for (let i = 0; i < 3; i++) { const workflow = { ...SIMPLE_WEBHOOK_WORKFLOW, name: createTestWorkflowName(`List - Page ${i}`), tags: ['mcp-integration-test'] }; const created = await client.createWorkflow(workflow); context.trackWorkflow(created.id!); workflows.push(created); } // List first page with limit const response = await handleListWorkflows( { limit: 2 }, mcpContext ); expect(response.success).toBe(true); const data = response.data as any; expect(data.workflows.length).toBeLessThanOrEqual(2); expect(data.hasMore).toBeDefined(); expect(data.nextCursor).toBeDefined(); }); it('should handle pagination with cursor', async () => { // Create multiple workflows for (let i = 0; i < 5; i++) { const workflow = { ...SIMPLE_WEBHOOK_WORKFLOW, name: createTestWorkflowName(`List - Cursor ${i}`), tags: ['mcp-integration-test'] }; const created = await client.createWorkflow(workflow); context.trackWorkflow(created.id!); } // Get first page const firstPage = await handleListWorkflows( { limit: 2 }, mcpContext ); expect(firstPage.success).toBe(true); const firstData = firstPage.data as any; if (firstData.hasMore && firstData.nextCursor) { // Get second page using cursor const secondPage = await handleListWorkflows( { limit: 2, cursor: firstData.nextCursor }, mcpContext ); expect(secondPage.success).toBe(true); const secondData = secondPage.data as any; // Second page should have different workflows const firstIds = new Set(firstData.workflows.map((w: any) => w.id)); const secondIds = secondData.workflows.map((w: any) => w.id); secondIds.forEach((id: string) => { expect(firstIds.has(id)).toBe(false); }); } }); it('should handle last page (no more results)', async () => { // Create single workflow const workflow = { ...SIMPLE_WEBHOOK_WORKFLOW, name: createTestWorkflowName('List - Last Page'), tags: ['mcp-integration-test', 'unique-last-page-tag'] }; const created = await client.createWorkflow(workflow); context.trackWorkflow(created.id!); // List with high limit and unique tag const response = await handleListWorkflows( { tags: ['unique-last-page-tag'], limit: 100 }, mcpContext ); expect(response.success).toBe(true); const data = response.data as any; // Should not have more results expect(data.hasMore).toBe(false); expect(data.workflows.length).toBeLessThanOrEqual(100); }); }); // ====================================================================== // Limit Variations // ====================================================================== describe('Limit Variations', () => { it('should respect limit=1', async () => { // Create workflow const workflow = { ...SIMPLE_WEBHOOK_WORKFLOW, name: createTestWorkflowName('List - Limit 1'), tags: ['mcp-integration-test'] }; const created = await client.createWorkflow(workflow); context.trackWorkflow(created.id!); // List with limit=1 const response = await handleListWorkflows( { limit: 1 }, mcpContext ); expect(response.success).toBe(true); const data = response.data as any; expect(data.workflows.length).toBe(1); }); it('should respect limit=50', async () => { // List with limit=50 const response = await handleListWorkflows( { limit: 50 }, mcpContext ); expect(response.success).toBe(true); const data = response.data as any; expect(data.workflows.length).toBeLessThanOrEqual(50); }); it('should respect limit=100 (max)', async () => { // List with limit=100 const response = await handleListWorkflows( { limit: 100 }, mcpContext ); expect(response.success).toBe(true); const data = response.data as any; expect(data.workflows.length).toBeLessThanOrEqual(100); }); }); // ====================================================================== // Exclude Pinned Data // ====================================================================== describe('Exclude Pinned Data', () => { it('should exclude pinned data when requested', async () => { // Create workflow const workflow = { ...SIMPLE_WEBHOOK_WORKFLOW, name: createTestWorkflowName('List - No Pinned Data'), tags: ['mcp-integration-test'] }; const created = await client.createWorkflow(workflow); context.trackWorkflow(created.id!); // List with excludePinnedData=true const response = await handleListWorkflows( { excludePinnedData: true }, mcpContext ); expect(response.success).toBe(true); const data = response.data as any; // Verify response doesn't include pinned data data.workflows.forEach((w: any) => { expect(w.pinData).toBeUndefined(); }); }); }); // ====================================================================== // Empty Results // ====================================================================== describe('Empty Results', () => { it('should return empty array when no workflows match filters', async () => { // List with non-existent tag const response = await handleListWorkflows( { tags: ['non-existent-tag-xyz-12345'] }, mcpContext ); expect(response.success).toBe(true); const data = response.data as any; expect(Array.isArray(data.workflows)).toBe(true); expect(data.workflows.length).toBe(0); expect(data.hasMore).toBe(false); }); }); // ====================================================================== // Sort Order Verification // ====================================================================== describe('Sort Order', () => { it('should return workflows in consistent order', async () => { // Create multiple workflows for (let i = 0; i < 3; i++) { const workflow = { ...SIMPLE_WEBHOOK_WORKFLOW, name: createTestWorkflowName(`List - Sort ${i}`), tags: ['mcp-integration-test', 'sort-test'] }; const created = await client.createWorkflow(workflow); context.trackWorkflow(created.id!); // Small delay to ensure different timestamps await new Promise(resolve => setTimeout(resolve, 100)); } // List workflows twice const response1 = await handleListWorkflows( { tags: ['sort-test'] }, mcpContext ); const response2 = await handleListWorkflows( { tags: ['sort-test'] }, mcpContext ); expect(response1.success).toBe(true); expect(response2.success).toBe(true); const data1 = response1.data as any; const data2 = response2.data as any; // Same workflows should be returned in same order expect(data1.workflows.length).toBe(data2.workflows.length); const ids1 = data1.workflows.map((w: any) => w.id); const ids2 = data2.workflows.map((w: any) => w.id); expect(ids1).toEqual(ids2); }); }); });