chore: import upstream snapshot with attribution
Build and Push Docker Images / Build Docker Image (push) Has been cancelled
Build and Push Docker Images / Build Railway Docker Image (push) Has been cancelled
Build and Publish n8n Docker Image / test-image (push) Has been cancelled
Dependency Compatibility Check / Fresh Install Dependency Check (push) Has been cancelled
Build and Publish n8n Docker Image / build-and-push (push) Has been cancelled
Build and Publish n8n Docker Image / create-release (push) Has been cancelled
Automated Release / Detect Version Change (push) Has been cancelled
Automated Release / Generate Release Notes (push) Has been cancelled
Automated Release / Create GitHub Release (push) Has been cancelled
Automated Release / Package MCPB Bundle (push) Has been cancelled
Automated Release / Build and Verify (push) Has been cancelled
Automated Release / Publish to NPM (push) Has been cancelled
Automated Release / Build and Push Docker Images (push) Has been cancelled
Automated Release / Update Documentation (push) Has been cancelled
Automated Release / Notify Release Completion (push) Has been cancelled
Secret Scan / secretlint (push) Has been cancelled
Test Suite / test (push) Has been cancelled
Test Suite / cjs-runtime (push) Has been cancelled
Test Suite / publish-results (push) Has been cancelled
Build and Push Docker Images / Build Docker Image (push) Has been cancelled
Build and Push Docker Images / Build Railway Docker Image (push) Has been cancelled
Build and Publish n8n Docker Image / test-image (push) Has been cancelled
Dependency Compatibility Check / Fresh Install Dependency Check (push) Has been cancelled
Build and Publish n8n Docker Image / build-and-push (push) Has been cancelled
Build and Publish n8n Docker Image / create-release (push) Has been cancelled
Automated Release / Detect Version Change (push) Has been cancelled
Automated Release / Generate Release Notes (push) Has been cancelled
Automated Release / Create GitHub Release (push) Has been cancelled
Automated Release / Package MCPB Bundle (push) Has been cancelled
Automated Release / Build and Verify (push) Has been cancelled
Automated Release / Publish to NPM (push) Has been cancelled
Automated Release / Build and Push Docker Images (push) Has been cancelled
Automated Release / Update Documentation (push) Has been cancelled
Automated Release / Notify Release Completion (push) Has been cancelled
Secret Scan / secretlint (push) Has been cancelled
Test Suite / test (push) Has been cancelled
Test Suite / cjs-runtime (push) Has been cancelled
Test Suite / publish-results (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,722 @@
|
||||
/**
|
||||
* Integration tests for AI node connection validation in workflow diff operations
|
||||
* Tests that AI nodes with AI-specific connection types (ai_languageModel, ai_memory, etc.)
|
||||
* are properly validated without requiring main connections
|
||||
*
|
||||
* Related to issue #357
|
||||
*/
|
||||
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { WorkflowDiffEngine } from '../../../src/services/workflow-diff-engine';
|
||||
|
||||
describe('AI Node Connection Validation', () => {
|
||||
describe('AI-specific connection types', () => {
|
||||
test('should accept workflow with ai_languageModel connections', async () => {
|
||||
const workflow = {
|
||||
id: 'test-workflow',
|
||||
name: 'AI Language Model Test',
|
||||
nodes: [
|
||||
{
|
||||
id: 'agent-node',
|
||||
name: 'AI Agent',
|
||||
type: '@n8n/n8n-nodes-langchain.agent',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'llm-node',
|
||||
name: 'OpenAI Chat Model',
|
||||
type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
|
||||
typeVersion: 1,
|
||||
position: [200, 0],
|
||||
parameters: {}
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
'OpenAI Chat Model': {
|
||||
ai_languageModel: [
|
||||
[{ node: 'AI Agent', type: 'ai_languageModel', index: 0 }]
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const engine = new WorkflowDiffEngine();
|
||||
const result = await engine.applyDiff(workflow as any, {
|
||||
id: workflow.id,
|
||||
operations: []
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.workflow).toBeDefined();
|
||||
});
|
||||
|
||||
test('should accept workflow with ai_memory connections', async () => {
|
||||
const workflow = {
|
||||
id: 'test-workflow',
|
||||
name: 'AI Memory Test',
|
||||
nodes: [
|
||||
{
|
||||
id: 'agent-node',
|
||||
name: 'AI Agent',
|
||||
type: '@n8n/n8n-nodes-langchain.agent',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'memory-node',
|
||||
name: 'Postgres Chat Memory',
|
||||
type: '@n8n/n8n-nodes-langchain.memoryPostgresChat',
|
||||
typeVersion: 1,
|
||||
position: [200, 0],
|
||||
parameters: {}
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
'Postgres Chat Memory': {
|
||||
ai_memory: [
|
||||
[{ node: 'AI Agent', type: 'ai_memory', index: 0 }]
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const engine = new WorkflowDiffEngine();
|
||||
const result = await engine.applyDiff(workflow as any, {
|
||||
id: workflow.id,
|
||||
operations: []
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.workflow).toBeDefined();
|
||||
});
|
||||
|
||||
test('should accept workflow with ai_embedding connections', async () => {
|
||||
const workflow = {
|
||||
id: 'test-workflow',
|
||||
name: 'AI Embedding Test',
|
||||
nodes: [
|
||||
{
|
||||
id: 'vectorstore-node',
|
||||
name: 'Vector Store',
|
||||
type: '@n8n/n8n-nodes-langchain.vectorStoreSupabase',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'embedding-node',
|
||||
name: 'Embeddings OpenAI',
|
||||
type: '@n8n/n8n-nodes-langchain.embeddingsOpenAi',
|
||||
typeVersion: 1,
|
||||
position: [200, 0],
|
||||
parameters: {}
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
'Embeddings OpenAI': {
|
||||
ai_embedding: [
|
||||
[{ node: 'Vector Store', type: 'ai_embedding', index: 0 }]
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const engine = new WorkflowDiffEngine();
|
||||
const result = await engine.applyDiff(workflow as any, {
|
||||
id: workflow.id,
|
||||
operations: []
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.workflow).toBeDefined();
|
||||
});
|
||||
|
||||
test('should accept workflow with ai_tool connections', async () => {
|
||||
const workflow = {
|
||||
id: 'test-workflow',
|
||||
name: 'AI Tool Test',
|
||||
nodes: [
|
||||
{
|
||||
id: 'agent-node',
|
||||
name: 'AI Agent',
|
||||
type: '@n8n/n8n-nodes-langchain.agent',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'vectorstore-node',
|
||||
name: 'Vector Store Tool',
|
||||
type: '@n8n/n8n-nodes-langchain.vectorStoreSupabase',
|
||||
typeVersion: 1,
|
||||
position: [200, 0],
|
||||
parameters: {}
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
'Vector Store Tool': {
|
||||
ai_tool: [
|
||||
[{ node: 'AI Agent', type: 'ai_tool', index: 0 }]
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const engine = new WorkflowDiffEngine();
|
||||
const result = await engine.applyDiff(workflow as any, {
|
||||
id: workflow.id,
|
||||
operations: []
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.workflow).toBeDefined();
|
||||
});
|
||||
|
||||
test('should accept workflow with ai_vectorStore connections', async () => {
|
||||
const workflow = {
|
||||
id: 'test-workflow',
|
||||
name: 'AI Vector Store Test',
|
||||
nodes: [
|
||||
{
|
||||
id: 'agent-node',
|
||||
name: 'AI Agent',
|
||||
type: '@n8n/n8n-nodes-langchain.agent',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'vectorstore-node',
|
||||
name: 'Supabase Vector Store',
|
||||
type: '@n8n/n8n-nodes-langchain.vectorStoreSupabase',
|
||||
typeVersion: 1,
|
||||
position: [200, 0],
|
||||
parameters: {}
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
'Supabase Vector Store': {
|
||||
ai_vectorStore: [
|
||||
[{ node: 'AI Agent', type: 'ai_vectorStore', index: 0 }]
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const engine = new WorkflowDiffEngine();
|
||||
const result = await engine.applyDiff(workflow as any, {
|
||||
id: workflow.id,
|
||||
operations: []
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.workflow).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mixed connection types', () => {
|
||||
test('should accept workflow mixing main and AI connections', async () => {
|
||||
const workflow = {
|
||||
id: 'test-workflow',
|
||||
name: 'Mixed Connections Test',
|
||||
nodes: [
|
||||
{
|
||||
id: 'webhook-node',
|
||||
name: 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'agent-node',
|
||||
name: 'AI Agent',
|
||||
type: '@n8n/n8n-nodes-langchain.agent',
|
||||
typeVersion: 1,
|
||||
position: [200, 0],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'llm-node',
|
||||
name: 'OpenAI Chat Model',
|
||||
type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
|
||||
typeVersion: 1,
|
||||
position: [200, 200],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'respond-node',
|
||||
name: 'Respond to Webhook',
|
||||
type: 'n8n-nodes-base.respondToWebhook',
|
||||
typeVersion: 1,
|
||||
position: [400, 0],
|
||||
parameters: {}
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
'Webhook': {
|
||||
main: [
|
||||
[{ node: 'AI Agent', type: 'main', index: 0 }]
|
||||
]
|
||||
},
|
||||
'AI Agent': {
|
||||
main: [
|
||||
[{ node: 'Respond to Webhook', type: 'main', index: 0 }]
|
||||
]
|
||||
},
|
||||
'OpenAI Chat Model': {
|
||||
ai_languageModel: [
|
||||
[{ node: 'AI Agent', type: 'ai_languageModel', index: 0 }]
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const engine = new WorkflowDiffEngine();
|
||||
const result = await engine.applyDiff(workflow as any, {
|
||||
id: workflow.id,
|
||||
operations: []
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.workflow).toBeDefined();
|
||||
});
|
||||
|
||||
test('should accept workflow with error connections alongside AI connections', async () => {
|
||||
const workflow = {
|
||||
id: 'test-workflow',
|
||||
name: 'Error + AI Connections Test',
|
||||
nodes: [
|
||||
{
|
||||
id: 'webhook-node',
|
||||
name: 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'agent-node',
|
||||
name: 'AI Agent',
|
||||
type: '@n8n/n8n-nodes-langchain.agent',
|
||||
typeVersion: 1,
|
||||
position: [200, 0],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'llm-node',
|
||||
name: 'OpenAI Chat Model',
|
||||
type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
|
||||
typeVersion: 1,
|
||||
position: [200, 200],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'error-handler',
|
||||
name: 'Error Handler',
|
||||
type: 'n8n-nodes-base.set',
|
||||
typeVersion: 1,
|
||||
position: [200, -200],
|
||||
parameters: {}
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
'Webhook': {
|
||||
main: [
|
||||
[{ node: 'AI Agent', type: 'main', index: 0 }]
|
||||
]
|
||||
},
|
||||
'AI Agent': {
|
||||
error: [
|
||||
[{ node: 'Error Handler', type: 'main', index: 0 }]
|
||||
]
|
||||
},
|
||||
'OpenAI Chat Model': {
|
||||
ai_languageModel: [
|
||||
[{ node: 'AI Agent', type: 'ai_languageModel', index: 0 }]
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const engine = new WorkflowDiffEngine();
|
||||
const result = await engine.applyDiff(workflow as any, {
|
||||
id: workflow.id,
|
||||
operations: []
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.workflow).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Complex AI workflow (Issue #357 scenario)', () => {
|
||||
test('should accept full AI agent workflow with RAG components', async () => {
|
||||
// Simplified version of the workflow from issue #357
|
||||
const workflow = {
|
||||
id: 'test-workflow',
|
||||
name: 'AI Agent with RAG',
|
||||
nodes: [
|
||||
{
|
||||
id: 'webhook-node',
|
||||
name: 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 2,
|
||||
position: [0, 0],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'code-node',
|
||||
name: 'Prepare Inputs',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 2,
|
||||
position: [200, 0],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'agent-node',
|
||||
name: 'AI Agent',
|
||||
type: '@n8n/n8n-nodes-langchain.agent',
|
||||
typeVersion: 1.7,
|
||||
position: [400, 0],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'llm-node',
|
||||
name: 'OpenAI Chat Model',
|
||||
type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
|
||||
typeVersion: 1,
|
||||
position: [400, 200],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'memory-node',
|
||||
name: 'Postgres Chat Memory',
|
||||
type: '@n8n/n8n-nodes-langchain.memoryPostgresChat',
|
||||
typeVersion: 1.1,
|
||||
position: [500, 200],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'embedding-node',
|
||||
name: 'Embeddings OpenAI',
|
||||
type: '@n8n/n8n-nodes-langchain.embeddingsOpenAi',
|
||||
typeVersion: 1,
|
||||
position: [600, 400],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'vectorstore-node',
|
||||
name: 'Supabase Vector Store',
|
||||
type: '@n8n/n8n-nodes-langchain.vectorStoreSupabase',
|
||||
typeVersion: 1.3,
|
||||
position: [600, 200],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'respond-node',
|
||||
name: 'Respond to Webhook',
|
||||
type: 'n8n-nodes-base.respondToWebhook',
|
||||
typeVersion: 1.1,
|
||||
position: [600, 0],
|
||||
parameters: {}
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
'Webhook': {
|
||||
main: [
|
||||
[{ node: 'Prepare Inputs', type: 'main', index: 0 }]
|
||||
]
|
||||
},
|
||||
'Prepare Inputs': {
|
||||
main: [
|
||||
[{ node: 'AI Agent', type: 'main', index: 0 }]
|
||||
]
|
||||
},
|
||||
'AI Agent': {
|
||||
main: [
|
||||
[{ node: 'Respond to Webhook', type: 'main', index: 0 }]
|
||||
]
|
||||
},
|
||||
'OpenAI Chat Model': {
|
||||
ai_languageModel: [
|
||||
[{ node: 'AI Agent', type: 'ai_languageModel', index: 0 }]
|
||||
]
|
||||
},
|
||||
'Postgres Chat Memory': {
|
||||
ai_memory: [
|
||||
[{ node: 'AI Agent', type: 'ai_memory', index: 0 }]
|
||||
]
|
||||
},
|
||||
'Embeddings OpenAI': {
|
||||
ai_embedding: [
|
||||
[{ node: 'Supabase Vector Store', type: 'ai_embedding', index: 0 }]
|
||||
]
|
||||
},
|
||||
'Supabase Vector Store': {
|
||||
ai_tool: [
|
||||
[{ node: 'AI Agent', type: 'ai_tool', index: 0 }]
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const engine = new WorkflowDiffEngine();
|
||||
const result = await engine.applyDiff(workflow as any, {
|
||||
id: workflow.id,
|
||||
operations: []
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.workflow).toBeDefined();
|
||||
expect(result.errors || []).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should successfully update AI workflow nodes without connection errors', async () => {
|
||||
// Test that we can update nodes in an AI workflow without triggering validation errors
|
||||
const workflow = {
|
||||
id: 'test-workflow',
|
||||
name: 'AI Workflow Update Test',
|
||||
nodes: [
|
||||
{
|
||||
id: 'webhook-node',
|
||||
name: 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 2,
|
||||
position: [0, 0],
|
||||
parameters: { path: 'test' }
|
||||
},
|
||||
{
|
||||
id: 'agent-node',
|
||||
name: 'AI Agent',
|
||||
type: '@n8n/n8n-nodes-langchain.agent',
|
||||
typeVersion: 1,
|
||||
position: [200, 0],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'llm-node',
|
||||
name: 'OpenAI Chat Model',
|
||||
type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
|
||||
typeVersion: 1,
|
||||
position: [200, 200],
|
||||
parameters: {}
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
'Webhook': {
|
||||
main: [
|
||||
[{ node: 'AI Agent', type: 'main', index: 0 }]
|
||||
]
|
||||
},
|
||||
'OpenAI Chat Model': {
|
||||
ai_languageModel: [
|
||||
[{ node: 'AI Agent', type: 'ai_languageModel', index: 0 }]
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const engine = new WorkflowDiffEngine();
|
||||
|
||||
// Update the webhook node (unrelated to AI nodes)
|
||||
const result = await engine.applyDiff(workflow as any, {
|
||||
id: workflow.id,
|
||||
operations: [
|
||||
{
|
||||
type: 'updateNode',
|
||||
nodeId: 'webhook-node',
|
||||
updates: {
|
||||
notes: 'Updated webhook configuration'
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.workflow).toBeDefined();
|
||||
expect(result.errors || []).toHaveLength(0);
|
||||
|
||||
// Verify the update was applied
|
||||
const updatedNode = result.workflow.nodes.find((n: any) => n.id === 'webhook-node');
|
||||
expect(updatedNode?.notes).toBe('Updated webhook configuration');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Node-only AI nodes (no main connections)', () => {
|
||||
test('should accept AI nodes with ONLY ai_languageModel connections', async () => {
|
||||
const workflow = {
|
||||
id: 'test-workflow',
|
||||
name: 'AI Node Without Main',
|
||||
nodes: [
|
||||
{
|
||||
id: 'agent-node',
|
||||
name: 'AI Agent',
|
||||
type: '@n8n/n8n-nodes-langchain.agent',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'llm-node',
|
||||
name: 'OpenAI Chat Model',
|
||||
type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
|
||||
typeVersion: 1,
|
||||
position: [200, 0],
|
||||
parameters: {}
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
// OpenAI Chat Model has NO main connections, ONLY ai_languageModel
|
||||
'OpenAI Chat Model': {
|
||||
ai_languageModel: [
|
||||
[{ node: 'AI Agent', type: 'ai_languageModel', index: 0 }]
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const engine = new WorkflowDiffEngine();
|
||||
const result = await engine.applyDiff(workflow as any, {
|
||||
id: workflow.id,
|
||||
operations: []
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.workflow).toBeDefined();
|
||||
expect(result.errors || []).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should accept AI nodes with ONLY ai_memory connections', async () => {
|
||||
const workflow = {
|
||||
id: 'test-workflow',
|
||||
name: 'Memory Node Without Main',
|
||||
nodes: [
|
||||
{
|
||||
id: 'agent-node',
|
||||
name: 'AI Agent',
|
||||
type: '@n8n/n8n-nodes-langchain.agent',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'memory-node',
|
||||
name: 'Postgres Chat Memory',
|
||||
type: '@n8n/n8n-nodes-langchain.memoryPostgresChat',
|
||||
typeVersion: 1,
|
||||
position: [200, 0],
|
||||
parameters: {}
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
// Memory node has NO main connections, ONLY ai_memory
|
||||
'Postgres Chat Memory': {
|
||||
ai_memory: [
|
||||
[{ node: 'AI Agent', type: 'ai_memory', index: 0 }]
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const engine = new WorkflowDiffEngine();
|
||||
const result = await engine.applyDiff(workflow as any, {
|
||||
id: workflow.id,
|
||||
operations: []
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.workflow).toBeDefined();
|
||||
expect(result.errors || []).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should accept embedding nodes with ONLY ai_embedding connections', async () => {
|
||||
const workflow = {
|
||||
id: 'test-workflow',
|
||||
name: 'Embedding Node Without Main',
|
||||
nodes: [
|
||||
{
|
||||
id: 'vectorstore-node',
|
||||
name: 'Vector Store',
|
||||
type: '@n8n/n8n-nodes-langchain.vectorStoreSupabase',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'embedding-node',
|
||||
name: 'Embeddings OpenAI',
|
||||
type: '@n8n/n8n-nodes-langchain.embeddingsOpenAi',
|
||||
typeVersion: 1,
|
||||
position: [200, 0],
|
||||
parameters: {}
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
// Embedding node has NO main connections, ONLY ai_embedding
|
||||
'Embeddings OpenAI': {
|
||||
ai_embedding: [
|
||||
[{ node: 'Vector Store', type: 'ai_embedding', index: 0 }]
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const engine = new WorkflowDiffEngine();
|
||||
const result = await engine.applyDiff(workflow as any, {
|
||||
id: workflow.id,
|
||||
operations: []
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.workflow).toBeDefined();
|
||||
expect(result.errors || []).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should accept vector store nodes with ONLY ai_tool connections', async () => {
|
||||
const workflow = {
|
||||
id: 'test-workflow',
|
||||
name: 'Vector Store Node Without Main',
|
||||
nodes: [
|
||||
{
|
||||
id: 'agent-node',
|
||||
name: 'AI Agent',
|
||||
type: '@n8n/n8n-nodes-langchain.agent',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'vectorstore-node',
|
||||
name: 'Supabase Vector Store',
|
||||
type: '@n8n/n8n-nodes-langchain.vectorStoreSupabase',
|
||||
typeVersion: 1,
|
||||
position: [200, 0],
|
||||
parameters: {}
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
// Vector store has NO main connections, ONLY ai_tool
|
||||
'Supabase Vector Store': {
|
||||
ai_tool: [
|
||||
[{ node: 'AI Agent', type: 'ai_tool', index: 0 }]
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const engine = new WorkflowDiffEngine();
|
||||
const result = await engine.applyDiff(workflow as any, {
|
||||
id: workflow.id,
|
||||
operations: []
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.workflow).toBeDefined();
|
||||
expect(result.errors || []).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,573 @@
|
||||
/**
|
||||
* Integration tests for auto-update connection references on node rename
|
||||
* Tests real-world workflow scenarios from Issue #353
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { WorkflowDiffEngine } from '@/services/workflow-diff-engine';
|
||||
import { validateWorkflowStructure } from '@/services/n8n-validation';
|
||||
import { WorkflowDiffRequest, UpdateNodeOperation } from '@/types/workflow-diff';
|
||||
import { Workflow, WorkflowNode } from '@/types/n8n-api';
|
||||
|
||||
describe('WorkflowDiffEngine - Node Rename Integration Tests', () => {
|
||||
let diffEngine: WorkflowDiffEngine;
|
||||
|
||||
beforeEach(() => {
|
||||
diffEngine = new WorkflowDiffEngine();
|
||||
});
|
||||
|
||||
describe('Real-world API endpoint workflow (Issue #353 scenario)', () => {
|
||||
let apiWorkflow: Workflow;
|
||||
|
||||
beforeEach(() => {
|
||||
// Complex real-world API endpoint workflow
|
||||
apiWorkflow = {
|
||||
id: 'api-workflow',
|
||||
name: 'POST /patients/:id/approaches - Add Approach',
|
||||
nodes: [
|
||||
{
|
||||
id: 'webhook-trigger',
|
||||
name: 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 2,
|
||||
position: [0, 0],
|
||||
parameters: {
|
||||
path: 'patients/{{$parameter["id"]/approaches',
|
||||
httpMethod: 'POST',
|
||||
responseMode: 'responseNode'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'validate-request',
|
||||
name: 'Validate Request',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 2,
|
||||
position: [200, 0],
|
||||
parameters: {
|
||||
mode: 'runOnceForAllItems',
|
||||
jsCode: '// Validation logic'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'check-auth',
|
||||
name: 'Check Authorization',
|
||||
type: 'n8n-nodes-base.if',
|
||||
typeVersion: 2,
|
||||
position: [400, 0],
|
||||
parameters: {
|
||||
conditions: {
|
||||
boolean: [{ value1: '={{$json.authorized}}', value2: true }]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'process-request',
|
||||
name: 'Process Request',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 2,
|
||||
position: [600, 0],
|
||||
parameters: {
|
||||
mode: 'runOnceForAllItems',
|
||||
jsCode: '// Processing logic'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'return-success',
|
||||
name: 'Return 200 OK',
|
||||
type: 'n8n-nodes-base.respondToWebhook',
|
||||
typeVersion: 1.1,
|
||||
position: [800, 0],
|
||||
parameters: {
|
||||
responseBody: '={{ {"success": true, "data": $json} }}',
|
||||
options: { responseCode: 200 }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'return-forbidden',
|
||||
name: 'Return 403 Forbidden1',
|
||||
type: 'n8n-nodes-base.respondToWebhook',
|
||||
typeVersion: 1.1,
|
||||
position: [600, 200],
|
||||
parameters: {
|
||||
responseBody: '={{ {"error": "Forbidden"} }}',
|
||||
options: { responseCode: 403 }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'handle-error',
|
||||
name: 'Handle Error',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 2,
|
||||
position: [400, 300],
|
||||
parameters: {
|
||||
mode: 'runOnceForAllItems',
|
||||
jsCode: '// Error handling'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'return-error',
|
||||
name: 'Return 500 Error',
|
||||
type: 'n8n-nodes-base.respondToWebhook',
|
||||
typeVersion: 1.1,
|
||||
position: [600, 300],
|
||||
parameters: {
|
||||
responseBody: '={{ {"error": "Internal Server Error"} }}',
|
||||
options: { responseCode: 500 }
|
||||
}
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
'Webhook': {
|
||||
main: [[{ node: 'Validate Request', type: 'main', index: 0 }]]
|
||||
},
|
||||
'Validate Request': {
|
||||
main: [[{ node: 'Check Authorization', type: 'main', index: 0 }]],
|
||||
error: [[{ node: 'Handle Error', type: 'main', index: 0 }]]
|
||||
},
|
||||
'Check Authorization': {
|
||||
main: [
|
||||
[{ node: 'Process Request', type: 'main', index: 0 }], // true branch
|
||||
[{ node: 'Return 403 Forbidden1', type: 'main', index: 0 }] // false branch
|
||||
],
|
||||
error: [[{ node: 'Handle Error', type: 'main', index: 0 }]]
|
||||
},
|
||||
'Process Request': {
|
||||
main: [[{ node: 'Return 200 OK', type: 'main', index: 0 }]],
|
||||
error: [[{ node: 'Handle Error', type: 'main', index: 0 }]]
|
||||
},
|
||||
'Handle Error': {
|
||||
main: [[{ node: 'Return 500 Error', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it('should successfully rename error response node and maintain all connections', async () => {
|
||||
// The exact operation from Issue #353
|
||||
const operation: UpdateNodeOperation = {
|
||||
type: 'updateNode',
|
||||
nodeId: 'return-forbidden',
|
||||
updates: {
|
||||
name: 'Return 404 Not Found',
|
||||
parameters: {
|
||||
responseBody: '={{ {"error": "Not Found"} }}',
|
||||
options: { responseCode: 404 }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const request: WorkflowDiffRequest = {
|
||||
id: 'api-workflow',
|
||||
operations: [operation]
|
||||
};
|
||||
|
||||
const result = await diffEngine.applyDiff(apiWorkflow, request);
|
||||
|
||||
// Should succeed
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.workflow).toBeDefined();
|
||||
|
||||
// Node should be renamed
|
||||
const renamedNode = result.workflow!.nodes.find((n: WorkflowNode) => n.id === 'return-forbidden');
|
||||
expect(renamedNode?.name).toBe('Return 404 Not Found');
|
||||
expect(renamedNode?.parameters.options?.responseCode).toBe(404);
|
||||
|
||||
// Connection from IF node should be updated
|
||||
expect(result.workflow!.connections['Check Authorization'].main[1][0].node).toBe('Return 404 Not Found');
|
||||
|
||||
// Validate workflow structure
|
||||
const validationErrors = validateWorkflowStructure(result.workflow!);
|
||||
expect(validationErrors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle multiple node renames in complex workflow', async () => {
|
||||
const operations: UpdateNodeOperation[] = [
|
||||
{
|
||||
type: 'updateNode',
|
||||
nodeId: 'return-forbidden',
|
||||
updates: { name: 'Return 404 Not Found' }
|
||||
},
|
||||
{
|
||||
type: 'updateNode',
|
||||
nodeId: 'return-success',
|
||||
updates: { name: 'Return 201 Created' }
|
||||
},
|
||||
{
|
||||
type: 'updateNode',
|
||||
nodeId: 'return-error',
|
||||
updates: { name: 'Return 500 Internal Server Error' }
|
||||
}
|
||||
];
|
||||
|
||||
const request: WorkflowDiffRequest = {
|
||||
id: 'api-workflow',
|
||||
operations
|
||||
};
|
||||
|
||||
const result = await diffEngine.applyDiff(apiWorkflow, request);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.workflow).toBeDefined();
|
||||
|
||||
// All nodes should be renamed
|
||||
expect(result.workflow!.nodes.find((n: WorkflowNode) => n.id === 'return-forbidden')?.name).toBe('Return 404 Not Found');
|
||||
expect(result.workflow!.nodes.find((n: WorkflowNode) => n.id === 'return-success')?.name).toBe('Return 201 Created');
|
||||
expect(result.workflow!.nodes.find((n: WorkflowNode) => n.id === 'return-error')?.name).toBe('Return 500 Internal Server Error');
|
||||
|
||||
// All connections should be updated
|
||||
expect(result.workflow!.connections['Check Authorization'].main[1][0].node).toBe('Return 404 Not Found');
|
||||
expect(result.workflow!.connections['Process Request'].main[0][0].node).toBe('Return 201 Created');
|
||||
expect(result.workflow!.connections['Handle Error'].main[0][0].node).toBe('Return 500 Internal Server Error');
|
||||
|
||||
// Validate entire workflow structure
|
||||
const validationErrors = validateWorkflowStructure(result.workflow!);
|
||||
expect(validationErrors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should maintain error connections after rename', async () => {
|
||||
const operation: UpdateNodeOperation = {
|
||||
type: 'updateNode',
|
||||
nodeId: 'validate-request',
|
||||
updates: { name: 'Validate Input' }
|
||||
};
|
||||
|
||||
const request: WorkflowDiffRequest = {
|
||||
id: 'api-workflow',
|
||||
operations: [operation]
|
||||
};
|
||||
|
||||
const result = await diffEngine.applyDiff(apiWorkflow, request);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.workflow).toBeDefined();
|
||||
|
||||
// Main connection should be updated
|
||||
expect(result.workflow!.connections['Validate Input']).toBeDefined();
|
||||
expect(result.workflow!.connections['Validate Input'].main[0][0].node).toBe('Check Authorization');
|
||||
|
||||
// Error connection should also be updated
|
||||
expect(result.workflow!.connections['Validate Input'].error[0][0].node).toBe('Handle Error');
|
||||
|
||||
// Validate workflow structure
|
||||
const validationErrors = validateWorkflowStructure(result.workflow!);
|
||||
expect(validationErrors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AI Agent workflow with tool connections', () => {
|
||||
let aiWorkflow: Workflow;
|
||||
|
||||
beforeEach(() => {
|
||||
aiWorkflow = {
|
||||
id: 'ai-workflow',
|
||||
name: 'AI Customer Support Agent',
|
||||
nodes: [
|
||||
{
|
||||
id: 'webhook-1',
|
||||
name: 'Customer Query',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 2,
|
||||
position: [0, 0],
|
||||
parameters: { path: 'support', httpMethod: 'POST' }
|
||||
},
|
||||
{
|
||||
id: 'agent-1',
|
||||
name: 'Support Agent',
|
||||
type: '@n8n/n8n-nodes-langchain.agent',
|
||||
typeVersion: 1,
|
||||
position: [200, 0],
|
||||
parameters: { promptTemplate: 'Help the customer with: {{$json.query}}' }
|
||||
},
|
||||
{
|
||||
id: 'tool-http',
|
||||
name: 'Knowledge Base API',
|
||||
type: '@n8n/n8n-nodes-langchain.toolHttpRequest',
|
||||
typeVersion: 1,
|
||||
position: [200, 100],
|
||||
parameters: { url: 'https://kb.example.com/search' }
|
||||
},
|
||||
{
|
||||
id: 'tool-code',
|
||||
name: 'Custom Logic Tool',
|
||||
type: '@n8n/n8n-nodes-langchain.toolCode',
|
||||
typeVersion: 1,
|
||||
position: [200, 200],
|
||||
parameters: { code: '// Custom logic' }
|
||||
},
|
||||
{
|
||||
id: 'response-1',
|
||||
name: 'Send Response',
|
||||
type: 'n8n-nodes-base.respondToWebhook',
|
||||
typeVersion: 1.1,
|
||||
position: [400, 0],
|
||||
parameters: {}
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
'Customer Query': {
|
||||
main: [[{ node: 'Support Agent', type: 'main', index: 0 }]]
|
||||
},
|
||||
'Support Agent': {
|
||||
main: [[{ node: 'Send Response', type: 'main', index: 0 }]],
|
||||
ai_tool: [
|
||||
[
|
||||
{ node: 'Knowledge Base API', type: 'ai_tool', index: 0 },
|
||||
{ node: 'Custom Logic Tool', type: 'ai_tool', index: 0 }
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// SKIPPED: Pre-existing validation bug - validateWorkflowStructure() doesn't recognize
|
||||
// AI connections (ai_tool, ai_languageModel, etc.) as valid, causing false positives.
|
||||
// The rename feature works correctly - connections ARE updated. Validation is the issue.
|
||||
// TODO: Fix validateWorkflowStructure() to check all connection types, not just 'main'
|
||||
it.skip('should update AI tool connections when renaming agent', async () => {
|
||||
const operation: UpdateNodeOperation = {
|
||||
type: 'updateNode',
|
||||
nodeId: 'agent-1',
|
||||
updates: { name: 'AI Support Assistant' }
|
||||
};
|
||||
|
||||
const request: WorkflowDiffRequest = {
|
||||
id: 'ai-workflow',
|
||||
operations: [operation]
|
||||
};
|
||||
|
||||
const result = await diffEngine.applyDiff(aiWorkflow, request);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.workflow).toBeDefined();
|
||||
|
||||
// Agent should be renamed
|
||||
expect(result.workflow!.nodes.find((n: WorkflowNode) => n.id === 'agent-1')?.name).toBe('AI Support Assistant');
|
||||
|
||||
// All connections should be updated
|
||||
expect(result.workflow!.connections['AI Support Assistant']).toBeDefined();
|
||||
expect(result.workflow!.connections['AI Support Assistant'].main[0][0].node).toBe('Send Response');
|
||||
expect(result.workflow!.connections['AI Support Assistant'].ai_tool[0]).toHaveLength(2);
|
||||
expect(result.workflow!.connections['AI Support Assistant'].ai_tool[0][0].node).toBe('Knowledge Base API');
|
||||
expect(result.workflow!.connections['AI Support Assistant'].ai_tool[0][1].node).toBe('Custom Logic Tool');
|
||||
|
||||
// Validate workflow structure
|
||||
const validationErrors = validateWorkflowStructure(result.workflow!);
|
||||
expect(validationErrors).toHaveLength(0);
|
||||
});
|
||||
|
||||
// SKIPPED: Pre-existing validation bug - validateWorkflowStructure() doesn't recognize
|
||||
// AI connections (ai_tool, ai_languageModel, etc.) as valid, causing false positives.
|
||||
// The rename feature works correctly - connections ARE updated. Validation is the issue.
|
||||
// TODO: Fix validateWorkflowStructure() to check all connection types, not just 'main'
|
||||
it.skip('should update AI tool connections when renaming tool', async () => {
|
||||
const operation: UpdateNodeOperation = {
|
||||
type: 'updateNode',
|
||||
nodeId: 'tool-http',
|
||||
updates: { name: 'Documentation Search' }
|
||||
};
|
||||
|
||||
const request: WorkflowDiffRequest = {
|
||||
id: 'ai-workflow',
|
||||
operations: [operation]
|
||||
};
|
||||
|
||||
const result = await diffEngine.applyDiff(aiWorkflow, request);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.workflow).toBeDefined();
|
||||
|
||||
// Tool should be renamed
|
||||
expect(result.workflow!.nodes.find((n: WorkflowNode) => n.id === 'tool-http')?.name).toBe('Documentation Search');
|
||||
|
||||
// AI tool connection should reference new name
|
||||
expect(result.workflow!.connections['Support Agent'].ai_tool[0][0].node).toBe('Documentation Search');
|
||||
// Other tool should remain unchanged
|
||||
expect(result.workflow!.connections['Support Agent'].ai_tool[0][1].node).toBe('Custom Logic Tool');
|
||||
|
||||
// Validate workflow structure
|
||||
const validationErrors = validateWorkflowStructure(result.workflow!);
|
||||
expect(validationErrors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multi-branch workflow with IF and Switch nodes', () => {
|
||||
let multiBranchWorkflow: Workflow;
|
||||
|
||||
beforeEach(() => {
|
||||
multiBranchWorkflow = {
|
||||
id: 'multi-branch-workflow',
|
||||
name: 'Order Processing Workflow',
|
||||
nodes: [
|
||||
{
|
||||
id: 'webhook-1',
|
||||
name: 'New Order',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 2,
|
||||
position: [0, 0],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'if-1',
|
||||
name: 'Check Payment Status',
|
||||
type: 'n8n-nodes-base.if',
|
||||
typeVersion: 2,
|
||||
position: [200, 0],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'switch-1',
|
||||
name: 'Route by Order Type',
|
||||
type: 'n8n-nodes-base.switch',
|
||||
typeVersion: 3,
|
||||
position: [400, 0],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'process-digital',
|
||||
name: 'Process Digital Order',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 2,
|
||||
position: [600, 0],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'process-physical',
|
||||
name: 'Process Physical Order',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 2,
|
||||
position: [600, 100],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'process-service',
|
||||
name: 'Process Service Order',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 2,
|
||||
position: [600, 200],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'reject-payment',
|
||||
name: 'Reject Payment',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 2,
|
||||
position: [400, 300],
|
||||
parameters: {}
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
'New Order': {
|
||||
main: [[{ node: 'Check Payment Status', type: 'main', index: 0 }]]
|
||||
},
|
||||
'Check Payment Status': {
|
||||
main: [
|
||||
[{ node: 'Route by Order Type', type: 'main', index: 0 }], // paid
|
||||
[{ node: 'Reject Payment', type: 'main', index: 0 }] // not paid
|
||||
]
|
||||
},
|
||||
'Route by Order Type': {
|
||||
main: [
|
||||
[{ node: 'Process Digital Order', type: 'main', index: 0 }], // case 0: digital
|
||||
[{ node: 'Process Physical Order', type: 'main', index: 0 }], // case 1: physical
|
||||
[{ node: 'Process Service Order', type: 'main', index: 0 }] // case 2: service
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it('should update all branch connections when renaming IF node', async () => {
|
||||
const operation: UpdateNodeOperation = {
|
||||
type: 'updateNode',
|
||||
nodeId: 'if-1',
|
||||
updates: { name: 'Validate Payment' }
|
||||
};
|
||||
|
||||
const request: WorkflowDiffRequest = {
|
||||
id: 'multi-branch-workflow',
|
||||
operations: [operation]
|
||||
};
|
||||
|
||||
const result = await diffEngine.applyDiff(multiBranchWorkflow, request);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.workflow).toBeDefined();
|
||||
|
||||
// IF node should be renamed
|
||||
expect(result.workflow!.nodes.find((n: WorkflowNode) => n.id === 'if-1')?.name).toBe('Validate Payment');
|
||||
|
||||
// Both branches should be updated
|
||||
expect(result.workflow!.connections['Validate Payment']).toBeDefined();
|
||||
expect(result.workflow!.connections['Validate Payment'].main[0][0].node).toBe('Route by Order Type');
|
||||
expect(result.workflow!.connections['Validate Payment'].main[1][0].node).toBe('Reject Payment');
|
||||
|
||||
// Validate workflow structure
|
||||
const validationErrors = validateWorkflowStructure(result.workflow!);
|
||||
expect(validationErrors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should update all case connections when renaming Switch node', async () => {
|
||||
const operation: UpdateNodeOperation = {
|
||||
type: 'updateNode',
|
||||
nodeId: 'switch-1',
|
||||
updates: { name: 'Order Type Router' }
|
||||
};
|
||||
|
||||
const request: WorkflowDiffRequest = {
|
||||
id: 'multi-branch-workflow',
|
||||
operations: [operation]
|
||||
};
|
||||
|
||||
const result = await diffEngine.applyDiff(multiBranchWorkflow, request);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.workflow).toBeDefined();
|
||||
|
||||
// Switch node should be renamed
|
||||
expect(result.workflow!.nodes.find((n: WorkflowNode) => n.id === 'switch-1')?.name).toBe('Order Type Router');
|
||||
|
||||
// All three cases should be updated
|
||||
expect(result.workflow!.connections['Order Type Router']).toBeDefined();
|
||||
expect(result.workflow!.connections['Order Type Router'].main).toHaveLength(3);
|
||||
expect(result.workflow!.connections['Order Type Router'].main[0][0].node).toBe('Process Digital Order');
|
||||
expect(result.workflow!.connections['Order Type Router'].main[1][0].node).toBe('Process Physical Order');
|
||||
expect(result.workflow!.connections['Order Type Router'].main[2][0].node).toBe('Process Service Order');
|
||||
|
||||
// Validate workflow structure
|
||||
const validationErrors = validateWorkflowStructure(result.workflow!);
|
||||
expect(validationErrors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should update specific case target when renamed', async () => {
|
||||
const operation: UpdateNodeOperation = {
|
||||
type: 'updateNode',
|
||||
nodeId: 'process-digital',
|
||||
updates: { name: 'Send Digital Download Link' }
|
||||
};
|
||||
|
||||
const request: WorkflowDiffRequest = {
|
||||
id: 'multi-branch-workflow',
|
||||
operations: [operation]
|
||||
};
|
||||
|
||||
const result = await diffEngine.applyDiff(multiBranchWorkflow, request);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.workflow).toBeDefined();
|
||||
|
||||
// Digital order node should be renamed
|
||||
expect(result.workflow!.nodes.find((n: WorkflowNode) => n.id === 'process-digital')?.name).toBe('Send Digital Download Link');
|
||||
|
||||
// Case 0 connection should be updated
|
||||
expect(result.workflow!.connections['Route by Order Type'].main[0][0].node).toBe('Send Digital Download Link');
|
||||
// Other cases should remain unchanged
|
||||
expect(result.workflow!.connections['Route by Order Type'].main[1][0].node).toBe('Process Physical Order');
|
||||
expect(result.workflow!.connections['Route by Order Type'].main[2][0].node).toBe('Process Service Order');
|
||||
|
||||
// Validate workflow structure
|
||||
const validationErrors = validateWorkflowStructure(result.workflow!);
|
||||
expect(validationErrors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user