feat: implement Phase 6A integration tests (workflow validation)

Implemented comprehensive integration tests for workflow validation operations.

Test Coverage (12 scenarios):
- validate-workflow.test.ts: 12 test scenarios
  * Valid workflow with all 4 profiles (runtime, strict, ai-friendly, minimal)
  * Invalid workflow detection (bad node types, missing connections)
  * Selective validation (nodes only, connections only, expressions only)
  * Error handling (non-existent workflow, invalid parameters)
  * Response format verification

Infrastructure:
- Created node-repository utility for integration tests
- Provides singleton NodeRepository instance for validation tests
- Uses production nodes.db database

Test Results:
- All 83 integration tests passing (Phase 1-6A complete)
- Validation tests cover all 4 validation profiles
- Tests verify actual validation against real n8n instance

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2025-10-05 09:08:23 +02:00
parent c0d7145a5a
commit 3331b72df4
2 changed files with 465 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
/**
* Node Repository Utility for Integration Tests
*
* Provides a singleton NodeRepository instance for integration tests
* that require validation or autofix functionality.
*/
import path from 'path';
import { createDatabaseAdapter } from '../../../../src/database/database-adapter';
import { NodeRepository } from '../../../../src/database/node-repository';
let repositoryInstance: NodeRepository | null = null;
/**
* Get or create NodeRepository instance
* Uses the production nodes.db database
*/
export async function getNodeRepository(): Promise<NodeRepository> {
if (repositoryInstance) {
return repositoryInstance;
}
const dbPath = path.join(process.cwd(), 'data/nodes.db');
const db = await createDatabaseAdapter(dbPath);
repositoryInstance = new NodeRepository(db);
return repositoryInstance;
}
/**
* Reset repository instance (useful for test cleanup)
*/
export function resetNodeRepository(): void {
repositoryInstance = null;
}

View File

@@ -0,0 +1,430 @@
/**
* Integration Tests: handleValidateWorkflow
*
* Tests workflow validation against a real n8n instance.
* Covers validation profiles, validation types, and error detection.
*/
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 } from '../utils/fixtures';
import { cleanupOrphanedWorkflows } from '../utils/cleanup-helpers';
import { createMcpContext } from '../utils/mcp-context';
import { InstanceContext } from '../../../../src/types/instance-context';
import { handleValidateWorkflow } from '../../../../src/mcp/handlers-n8n-manager';
import { getNodeRepository } from '../utils/node-repository';
import { NodeRepository } from '../../../../src/database/node-repository';
describe('Integration: handleValidateWorkflow', () => {
let context: TestContext;
let client: N8nApiClient;
let mcpContext: InstanceContext;
let repository: NodeRepository;
beforeEach(async () => {
context = createTestContext();
client = getTestN8nClient();
mcpContext = createMcpContext();
repository = await getNodeRepository();
});
afterEach(async () => {
await context.cleanup();
});
afterAll(async () => {
if (!process.env.CI) {
await cleanupOrphanedWorkflows();
}
});
// ======================================================================
// Valid Workflow - All Profiles
// ======================================================================
describe('Valid Workflow', () => {
it('should validate valid workflow with default profile (runtime)', async () => {
// Create valid workflow
const workflow = {
...SIMPLE_WEBHOOK_WORKFLOW,
name: createTestWorkflowName('Validate - Valid Default'),
tags: ['mcp-integration-test']
};
const created = await client.createWorkflow(workflow);
context.trackWorkflow(created.id!);
// Validate with default profile
const response = await handleValidateWorkflow(
{ id: created.id },
repository,
mcpContext
);
expect(response.success).toBe(true);
const data = response.data as any;
// Verify response structure
expect(data.valid).toBe(true);
expect(data.errors).toBeUndefined(); // Only present if errors exist
expect(data.summary).toBeDefined();
expect(data.summary.errorCount).toBe(0);
});
it('should validate with strict profile', async () => {
const workflow = {
...SIMPLE_WEBHOOK_WORKFLOW,
name: createTestWorkflowName('Validate - Valid Strict'),
tags: ['mcp-integration-test']
};
const created = await client.createWorkflow(workflow);
context.trackWorkflow(created.id!);
const response = await handleValidateWorkflow(
{
id: created.id,
options: { profile: 'strict' }
},
repository,
mcpContext
);
expect(response.success).toBe(true);
const data = response.data as any;
expect(data.valid).toBe(true);
});
it('should validate with ai-friendly profile', async () => {
const workflow = {
...SIMPLE_WEBHOOK_WORKFLOW,
name: createTestWorkflowName('Validate - Valid AI Friendly'),
tags: ['mcp-integration-test']
};
const created = await client.createWorkflow(workflow);
context.trackWorkflow(created.id!);
const response = await handleValidateWorkflow(
{
id: created.id,
options: { profile: 'ai-friendly' }
},
repository,
mcpContext
);
expect(response.success).toBe(true);
const data = response.data as any;
expect(data.valid).toBe(true);
});
it('should validate with minimal profile', async () => {
const workflow = {
...SIMPLE_WEBHOOK_WORKFLOW,
name: createTestWorkflowName('Validate - Valid Minimal'),
tags: ['mcp-integration-test']
};
const created = await client.createWorkflow(workflow);
context.trackWorkflow(created.id!);
const response = await handleValidateWorkflow(
{
id: created.id,
options: { profile: 'minimal' }
},
repository,
mcpContext
);
expect(response.success).toBe(true);
const data = response.data as any;
expect(data.valid).toBe(true);
});
});
// ======================================================================
// Invalid Workflow - Error Detection
// ======================================================================
describe('Invalid Workflow Detection', () => {
it('should detect invalid node type', async () => {
// Create workflow with invalid node type
const workflow = {
name: createTestWorkflowName('Validate - Invalid Node Type'),
nodes: [
{
id: 'invalid-1',
name: 'Invalid Node',
type: 'invalid-node-type',
typeVersion: 1,
position: [250, 300] as [number, number],
parameters: {}
}
],
connections: {},
settings: {},
tags: ['mcp-integration-test']
};
const created = await client.createWorkflow(workflow);
context.trackWorkflow(created.id!);
const response = await handleValidateWorkflow(
{ id: created.id },
repository,
mcpContext
);
expect(response.success).toBe(true);
const data = response.data as any;
// Should detect error
expect(data.valid).toBe(false);
expect(data.errors).toBeDefined();
expect(data.errors.length).toBeGreaterThan(0);
expect(data.summary.errorCount).toBeGreaterThan(0);
// Error should mention invalid node type
const errorMessages = data.errors.map((e: any) => e.message).join(' ');
expect(errorMessages).toMatch(/invalid-node-type|not found|unknown/i);
});
it('should detect missing required connections', async () => {
// Create workflow with 2 nodes but no connections
const workflow = {
name: createTestWorkflowName('Validate - Missing Connections'),
nodes: [
{
id: 'webhook-1',
name: 'Webhook',
type: 'n8n-nodes-base.webhook',
typeVersion: 2,
position: [250, 300] as [number, number],
parameters: {
httpMethod: 'GET',
path: 'test'
}
},
{
id: 'set-1',
name: 'Set',
type: 'n8n-nodes-base.set',
typeVersion: 3.4,
position: [450, 300] as [number, number],
parameters: {
assignments: {
assignments: []
}
}
}
],
connections: {}, // Empty connections - Set node is unreachable
settings: {},
tags: ['mcp-integration-test']
};
const created = await client.createWorkflow(workflow);
context.trackWorkflow(created.id!);
const response = await handleValidateWorkflow(
{ id: created.id },
repository,
mcpContext
);
expect(response.success).toBe(true);
const data = response.data as any;
// Multi-node workflow with empty connections should produce warning/error
// (depending on validation profile)
expect(data.valid).toBe(false);
});
});
// ======================================================================
// Selective Validation
// ======================================================================
describe('Selective Validation', () => {
it('should validate nodes only (skip connections)', async () => {
const workflow = {
...SIMPLE_WEBHOOK_WORKFLOW,
name: createTestWorkflowName('Validate - Nodes Only'),
tags: ['mcp-integration-test']
};
const created = await client.createWorkflow(workflow);
context.trackWorkflow(created.id!);
const response = await handleValidateWorkflow(
{
id: created.id,
options: {
validateNodes: true,
validateConnections: false,
validateExpressions: false
}
},
repository,
mcpContext
);
expect(response.success).toBe(true);
const data = response.data as any;
expect(data.valid).toBe(true);
});
it('should validate connections only (skip nodes)', async () => {
const workflow = {
...SIMPLE_WEBHOOK_WORKFLOW,
name: createTestWorkflowName('Validate - Connections Only'),
tags: ['mcp-integration-test']
};
const created = await client.createWorkflow(workflow);
context.trackWorkflow(created.id!);
const response = await handleValidateWorkflow(
{
id: created.id,
options: {
validateNodes: false,
validateConnections: true,
validateExpressions: false
}
},
repository,
mcpContext
);
expect(response.success).toBe(true);
const data = response.data as any;
expect(data.valid).toBe(true);
});
it('should validate expressions only', async () => {
const workflow = {
...SIMPLE_WEBHOOK_WORKFLOW,
name: createTestWorkflowName('Validate - Expressions Only'),
tags: ['mcp-integration-test']
};
const created = await client.createWorkflow(workflow);
context.trackWorkflow(created.id!);
const response = await handleValidateWorkflow(
{
id: created.id,
options: {
validateNodes: false,
validateConnections: false,
validateExpressions: true
}
},
repository,
mcpContext
);
expect(response.success).toBe(true);
// Expression validation may pass even if workflow has other issues
expect(response.data).toBeDefined();
});
});
// ======================================================================
// Error Handling
// ======================================================================
describe('Error Handling', () => {
it('should handle non-existent workflow ID', async () => {
const response = await handleValidateWorkflow(
{ id: '99999999' },
repository,
mcpContext
);
expect(response.success).toBe(false);
expect(response.error).toBeDefined();
});
it('should handle invalid profile parameter', async () => {
const workflow = {
...SIMPLE_WEBHOOK_WORKFLOW,
name: createTestWorkflowName('Validate - Invalid Profile'),
tags: ['mcp-integration-test']
};
const created = await client.createWorkflow(workflow);
context.trackWorkflow(created.id!);
const response = await handleValidateWorkflow(
{
id: created.id,
options: { profile: 'invalid-profile' as any }
},
repository,
mcpContext
);
// Should either fail validation or use default profile
expect(response.success).toBe(false);
});
});
// ======================================================================
// Response Format Verification
// ======================================================================
describe('Response Format', () => {
it('should return complete validation response structure', async () => {
const workflow = {
...SIMPLE_WEBHOOK_WORKFLOW,
name: createTestWorkflowName('Validate - Response Format'),
tags: ['mcp-integration-test']
};
const created = await client.createWorkflow(workflow);
context.trackWorkflow(created.id!);
const response = await handleValidateWorkflow(
{ id: created.id },
repository,
mcpContext
);
expect(response.success).toBe(true);
const data = response.data as any;
// Verify required fields
expect(data).toHaveProperty('workflowId');
expect(data).toHaveProperty('workflowName');
expect(data).toHaveProperty('valid');
expect(data).toHaveProperty('summary');
// errors and warnings only present if they exist
// For valid workflow, they should be undefined
if (data.errors) {
expect(Array.isArray(data.errors)).toBe(true);
}
if (data.warnings) {
expect(Array.isArray(data.warnings)).toBe(true);
}
// Verify summary structure
expect(data.summary).toHaveProperty('errorCount');
expect(data.summary).toHaveProperty('warningCount');
expect(data.summary).toHaveProperty('totalNodes');
expect(data.summary).toHaveProperty('enabledNodes');
expect(data.summary).toHaveProperty('triggerNodes');
// Verify types
expect(typeof data.valid).toBe('boolean');
expect(typeof data.summary.errorCount).toBe('number');
expect(typeof data.summary.warningCount).toBe('number');
});
});
});