From 5702a64a013871332618379aae08280ded4b9ec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Romuald=20Cz=C5=82onkowski?= <56956555+czlonkowski@users.noreply.github.com> Date: Fri, 24 Oct 2025 00:11:35 +0200 Subject: [PATCH] fix: AI node connection validation in partial workflow updates (#357) (#358) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: AI node connection validation in partial workflow updates (#357) Fix critical validation issue where n8n_update_partial_workflow incorrectly required 'main' connections for AI nodes that exclusively use AI-specific connection types (ai_languageModel, ai_memory, ai_embedding, ai_vectorStore, ai_tool). Problem: - Workflows containing AI nodes could not be updated via n8n_update_partial_workflow - Validation incorrectly expected ALL nodes to have 'main' connections - AI nodes only have AI-specific connection types, never 'main' Root Cause: - Zod schema in src/services/n8n-validation.ts defined 'main' as required field - Schema didn't support AI-specific connection types Fixed: - Made 'main' connection optional in Zod schema - Added support for all AI connection types: ai_tool, ai_languageModel, ai_memory, ai_embedding, ai_vectorStore - Created comprehensive test suite (13 tests) covering all AI connection scenarios - Updated documentation to clarify AI nodes don't require 'main' connections Testing: - All 13 new integration tests passing - Tested with actual workflow 019Vrw56aROeEzVj from issue #357 - Zero breaking changes (making required fields optional is always safe) Files Changed: - src/services/n8n-validation.ts - Fixed Zod schema - tests/integration/workflow-diff/ai-node-connection-validation.test.ts - New test suite - src/mcp/tool-docs/workflow_management/n8n-update-partial-workflow.ts - Updated docs - package.json - Version bump to 2.21.1 - CHANGELOG.md - Comprehensive release notes Closes #357 🤖 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude Conceived by Romuald Członkowski - www.aiadvisors.pl/en * fix: Add missing id parameter in test file and JSDoc comment Address code review feedback from PR #358: - Add 'id' field to all applyDiff calls in test file (fixes TypeScript errors) - Add JSDoc comment explaining why 'main' is optional in schema - Ensures TypeScript compilation succeeds Changes: - tests/integration/workflow-diff/ai-node-connection-validation.test.ts: Added id parameter to all 13 test cases - src/services/n8n-validation.ts: Added JSDoc explaining optional main connections Testing: - npm run typecheck: PASS ✅ - npm run build: PASS ✅ - All 13 tests: PASS ✅ 🤖 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Claude --- CHANGELOG.md | 133 ++++ data/nodes.db | Bin 62623744 -> 62623744 bytes package.json | 2 +- .../n8n-update-partial-workflow.ts | 4 + src/services/n8n-validation.ts | 32 +- .../ai-node-connection-validation.test.ts | 722 ++++++++++++++++++ 6 files changed, 883 insertions(+), 10 deletions(-) create mode 100644 tests/integration/workflow-diff/ai-node-connection-validation.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 6028516..fe79984 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,139 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.21.1] - 2025-10-23 + +### 🐛 Bug Fixes + +**Issue #357: Fix AI Node Connection Validation in Partial Workflow Updates** + +Fixed critical validation issue where `n8n_update_partial_workflow` incorrectly required `main` connections for AI nodes that exclusively use AI-specific connection types (`ai_languageModel`, `ai_memory`, `ai_embedding`, `ai_vectorStore`, `ai_tool`). + +#### Problem + +Workflows containing AI nodes (OpenAI Chat Model, Postgres Chat Memory, Embeddings OpenAI, Supabase Vector Store) could not be updated via `n8n_update_partial_workflow`, even for trivial changes to unrelated nodes. The validation logic incorrectly expected ALL nodes to have `main` connections, causing false positive errors: + +``` +Invalid connections: [ + { + "code": "invalid_type", + "expected": "array", + "received": "undefined", + "path": ["OpenAI Chat Model", "main"], + "message": "Required" + } +] +``` + +**Impact**: Users could not update any workflows containing AI Agent nodes via MCP tools, forcing manual updates through the n8n UI. + +#### Root Cause + +The Zod schema in `src/services/n8n-validation.ts` (lines 27-39) defined `main` connections as a **required field** for all nodes, without support for AI-specific connection types: + +```typescript +// BEFORE (Broken): +export const workflowConnectionSchema = z.record( + z.object({ + main: z.array(...), // Required - WRONG for AI nodes! + }) +); +``` + +AI nodes use specialized connection types exclusively: +- **ai_languageModel** - Language models (OpenAI, Anthropic, etc.) +- **ai_memory** - Memory systems (Postgres Chat Memory, etc.) +- **ai_embedding** - Embedding models (Embeddings OpenAI, etc.) +- **ai_vectorStore** - Vector stores (Supabase Vector Store, etc.) +- **ai_tool** - Tools for AI agents + +These nodes **never have `main` connections** - they only have their AI-specific connection types. + +#### Fixed + +**1. Updated Zod Schema** (`src/services/n8n-validation.ts` lines 27-49): +```typescript +// AFTER (Fixed): +const connectionArraySchema = z.array( + z.array( + z.object({ + node: z.string(), + type: z.string(), + index: z.number(), + }) + ) +); + +export const workflowConnectionSchema = z.record( + z.object({ + main: connectionArraySchema.optional(), // Now optional + error: connectionArraySchema.optional(), // Error connections + ai_tool: connectionArraySchema.optional(), // AI tool connections + ai_languageModel: connectionArraySchema.optional(), // Language model connections + ai_memory: connectionArraySchema.optional(), // Memory connections + ai_embedding: connectionArraySchema.optional(), // Embedding connections + ai_vectorStore: connectionArraySchema.optional(), // Vector store connections + }) +); +``` + +**2. Comprehensive Test Suite** (New file: `tests/integration/workflow-diff/ai-node-connection-validation.test.ts`): +- 13 test scenarios covering all AI connection types +- Tests for AI nodes with ONLY AI-specific connections (no `main`) +- Tests for mixed workflows (regular nodes + AI nodes) +- Tests for the exact scenario from issue #357 +- All tests passing ✅ + +**3. Updated Documentation** (`src/mcp/tool-docs/workflow_management/n8n-update-partial-workflow.ts`): +- Added clarification that AI nodes do NOT require `main` connections +- Documented fix for issue #357 +- Updated best practices for AI workflows + +#### Testing + +**Before Fix**: +- ❌ `n8n_validate_workflow`: Returns `valid: true` (correct) +- ❌ `n8n_update_partial_workflow`: FAILS with "main connections required" errors +- ❌ Cannot update workflows containing AI nodes at all + +**After Fix**: +- ✅ `n8n_validate_workflow`: Returns `valid: true` (still correct) +- ✅ `n8n_update_partial_workflow`: SUCCEEDS without validation errors +- ✅ AI nodes correctly recognized with AI-specific connection types only +- ✅ All 13 new integration tests passing +- ✅ Tested with actual workflow `019Vrw56aROeEzVj` from issue #357 + +#### Impact + +**Zero Breaking Changes**: +- Making required fields optional is always backward compatible +- All existing workflows continue working +- Validation now correctly matches n8n's actual connection model + +**Fixes**: +- Users can now update AI workflows via `n8n_update_partial_workflow` +- AI nodes no longer generate false positive validation errors +- Consistent validation between `n8n_validate_workflow` and `n8n_update_partial_workflow` + +#### Files Changed + +**Modified (3 files)**: +- `src/services/n8n-validation.ts` - Fixed Zod schema to support all connection types +- `src/mcp/tool-docs/workflow_management/n8n-update-partial-workflow.ts` - Updated documentation +- `package.json` - Version bump to 2.21.1 + +**Added (1 file)**: +- `tests/integration/workflow-diff/ai-node-connection-validation.test.ts` - Comprehensive test suite (13 tests) + +#### References + +- **Issue**: #357 - n8n_update_partial_workflow incorrectly validates AI nodes requiring 'main' connections +- **Workflow**: `019Vrw56aROeEzVj` (WOO_Workflow_21_POST_Chat_Send_AI_Agent) +- **Investigation**: Deep code analysis by Explore agent identified exact root cause in Zod schema +- **Confirmation**: n8n-mcp-tester agent verified fix with real workflow + +Conceived by Romuald Członkowski - [www.aiadvisors.pl/en](https://www.aiadvisors.pl/en) + ## [2.21.0] - 2025-10-23 ### ✨ Features diff --git a/data/nodes.db b/data/nodes.db index 194c467f9851dcac1f0b5679c41744e9593f4152..6b94534e136bd2e05e4a0a567212e71018f2b636 100644 GIT binary patch delta 5103 zcmZA1cT^PT9*6OrS*jpKtf+tmD=47Y8+OGmVlRk-s1yNv0TdS%JN5?lUI5+27$wDM z5@VvN8k3mB#5AL*F+K5qAJ4h>k2`yw&w0I?>5@+LHY5B9e?mkhKoGdvm79GxB8=@^91;x(; z46lOg&;4B!OCk#U+_jWGd>(7oHxnvZYnVwXX|pqp+8)+gX6o!r*6!A7X3W%#Ofw)a z!8DYqtjb9_D;MRe+?2bjq&$?Ts;s<}xAIZGs*3VcRaG@rUDZ%ERW0SOYO6Y`uHttC zRgkKu>Z=B-p$b-wRAbddHC4@2a}}aORSVTpg{g2Ap<1cdDpIvkZB;u*g4sUeRQcm% z57W;q4+=VC#G2k-)<82;kKZCCb;?xTi_f|Cs)OpNIyrpKbuPJPxE1oVE2EVu_G%YTEMqHvUD=04Xb2M(<{DPUcB)^O5s=BEt)!nfqzsLDT#wAC)k{*|> zPHv_dV6*qI*?ZdTy=?a0HhUkNy|2yQ&t~s$vq#(P1MK#JPCF_)doH(lM|123k%uk7*mY$lDv>+}sDK+KAX|9G7gV1TFVVH(#b#nLc@TrkkF>a6= ztcIweYM2_XV$=u~t469(YP1@o#;S2@yqcgUs!1wNO;+)0ikhktRHB-ulGJoHL(Npl zDn+HLG&M`5J1WLy_{WZCfQPOCGjSe2+!byl5IcI8lI>b!bMT~IHpi|UdpSFfm7)obc?by>ZkuBfZ( zP4$*~TU}G{sO#!o^`5$+ZmRdy2kJxhk@{GDqHd|%>Qi+`eWpHFU#Pq4OZAodTHRCM zsBhJM^_}`&{h)qSKdA@mXZ27$QWfg4dZM1HU(_@8tNKm-uKrMes=w6V>L2y5dY&KO z)>x;&1S>d!Gq`{&xPd!V0uS(n%HRdw-~+x;1^l2YRD(A7}028d>1kT_BuHXjlPzgN16Dorjc!Ll4LKX0Xs!$E8Lk*}2wZI>0Lmj9K0T2j5 zP!H-u184}r&sY#;nQ0o=3-YD zC&SawozJ};0-?|XT0$6vLj<&f)({D8K(m{ByB)NL4$u)gL1*X!U7;I9L3ii@J)sx$ zhCa|2`ayq)h5;}T2Ekw$0z+XK42KvP0kJR=M!{$p17l$vjE4y@5hg(#Oon)v0#hLY z5@8x7!E~4bGa(sLAQjSJ7NkQ4WWsEi19M>>%!dWA5Ej8=SOQs)4LPtBmO(Bo2OH$U z3RnrNARku48dwYKU_ESrjZgraU^8rit*{NY!w%R9yI?o$fkG&Py|54V!vQ!5hu|8E!38&yRoPlB}fl@dN=fDmQD1-Cx5?p|n;UZjua(D$^h1cM9xD0Q=6}Sp- z!dvh*T!VMuI=l<-!40?x@52Z1A$$ZM!zXYHZo{W=2R?((;S0D6U&2@LHQa-5;9Iy4 z-@*6r1N;a-!2|di9>OE2fXDCzp29Ei41R^*;CJ`~{)E5aZ} z42_^MG=Zkj44Oj-ghC5w31JWp5zq=+LnO3;w$KjRLkH*xouD&xfv(UEqM$qUfS%9` zdP5)R3;m!!M8g0W2!miS41u9A42DAtjDT1e38P>%jDfK*4#vX-m~4kkl9Oo6G8 z0EsXSl3+T_fSHgCDUb?jFbmQl12SPY%z?Qu59Y%HSO|+?F)V>B$c7wP3dVGXQ>b+8^bz(y#5O|TiZz*g7>+hGUngk7*3_CO&N!Cu%0`{4i_ghOx` oj=)hk2FKw9oP<+w8qPp5lt3w*g>zsB2b95iYq?&3)|K}E0QnXFvj6}9 delta 4804 zcmXxkWl+>#8;9}TA0QG6hy@m6D;8q6*xiYVVs~I;E-ES_b|H2L=&o36cQ+O{R)TCX-KLlgS)qGW*Yq2?#W-$veAjes7LX zbWcriGCxnWe|g~I9aGg*#nsU?(Zq2|7gOKl8oTXZ&e)6pvO0$)l!&W!e{^EXzt)Do zX|Jn87NhroLBofcv$;9=7z2k7WA^j?|0OcaFe^hjC`aX_(kN$@R=Frw<)++KI+b2| zC{N|3GAM7AQDst@RTh<1`6yqNO=VX(R8Eyk`KjE>U*%DGRX&wp6;K6LAyrrvQ30x` zDyE975~`#MRHamqDy_<>vZ|ad%qSnY&FZ*oqd{l5!ezS+ITte_M2S*pfY`0j0E!LVAYb}el zw#8b}vP(+hHDLM5cx` zR!vk>)l4;4EmTX@O0`yPR9n?fwO1Wfi0Y_1sm`j4>Z-b_P}N=aP+_X4>ZN+CKB}+k zr~0b_YM>gV2CE^q)R3XRp?0s8osGw7BHx}Es)niIYJ>_`Bh@H1T8&X-)i~SR6XUb? zHmBH}N<=&`+r9L{HioTYP(&NExxFp6bi_%si}Mzfn<=w{gIi>1*$6d3O;nTAWHm)i zRTdSgrm5*_hKf=%)hsnzMXNa~M$J{RYMz>}7N~`4ky@;lsHJL|TCP^8IJHv6t5s^X zTBFvgb!xrZpf;*aYO~s+wyJGvyV{|4ssxp&cB$RUs%&bHN>Y2(KDA#RPzRM=9a4wY z5p`4@Q^(Z_byA&Dr_~vCR-IGl)dh7?T~e3T6?IizQ`glEbyM9^$?CScqwcDE>b`oQ z9;!#`v3jDOs%PrCdZAvbSL(HTqu#1_Dn-3lsp^CJs6MIB>WliSzNzo(hx)00so(04 z`m6p$g_bo(D=@$T9Ki|FfHR~87jOkPaEEk|9z4JkydVR3Lq^C1nIQ{g1t0K*Y>*vt zKu*X7evljdArIt*e2^asKtU)3g`o%pKv5_L#i0b0gg__-K~NgX!2iCa98-Cy02QGU zRE8>06{S1^I$$KfQ7IK7Q+%)3d>+Q ztbjOJ3GuKBR>K-t3+rG#Y=Dih2{ywP*b3WVJM4g+kN}CW3wDDQY_JECU@z>0{cr#d zf*lUQVK@Ru;TRl;6L1nv!D%=HXW<;2hYN5KF2QBE0$1T0T!$NQ6K+8=+=e@F7w*A* zcmNOK5j=(`@D!fGb9ezS;T61wH}DqTK?=NwRQLcN;S+p@FYpzZ`FdRleIE;i*FdD|dSQrQ6Ap$1AM3@AV zVG2wI3q-;+m<}@_3TDDAm<`b|2V!6@#KJt74+~%+EP}OkPNrs4%~%%a33DPLwE#_;R!s2 zXYd?ez)N@qui*{6g?Eqw?;#aFz(@E5pWzF9g>Ud3e!x%o1;61B{Dpr;RCuz31~VAo z0FK}UX}}rMf(y8U8@NL{NDm(130{x^ydfiGg3OQwvVsrzLN>?_IUpzG0zb$N{*VXq zLO#e31)v}lg2GS)0-z`qgW^yENdjWuPpSgYr-TDnccw3{{{iRDWV=jDpcH2FAiT7!MIJ0VcvEm<&^3 zDp(*AronWW0Z}j$X2EQThB*)eb0HSy!F*T%3tw%02^TwY=$kc6}G{4*a15s0TN*s>;@~?U=JiY*tPx~rd$65TVu92 diff --git a/package.json b/package.json index 0e1e154..12a1368 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "n8n-mcp", - "version": "2.21.0", + "version": "2.21.1", "description": "Integration between n8n workflow automation and Model Context Protocol (MCP)", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/mcp/tool-docs/workflow_management/n8n-update-partial-workflow.ts b/src/mcp/tool-docs/workflow_management/n8n-update-partial-workflow.ts index c959b9d..0acfbe5 100644 --- a/src/mcp/tool-docs/workflow_management/n8n-update-partial-workflow.ts +++ b/src/mcp/tool-docs/workflow_management/n8n-update-partial-workflow.ts @@ -81,6 +81,10 @@ Full support for all 8 AI connection types used in n8n AI workflows: - Multiple tools: Batch multiple \`sourceOutput: "ai_tool"\` connections to one AI Agent - Vector retrieval: Chain ai_embedding → ai_vectorStore → ai_tool → AI Agent +**Important Notes**: +- **AI nodes do NOT require main connections**: Nodes like OpenAI Chat Model, Postgres Chat Memory, Embeddings OpenAI, and Supabase Vector Store use AI-specific connection types exclusively. They should ONLY have connections like \`ai_languageModel\`, \`ai_memory\`, \`ai_embedding\`, or \`ai_tool\` - NOT \`main\` connections. +- **Fixed in v2.21.1**: Validation now correctly recognizes AI nodes that only have AI-specific connections without requiring \`main\` connections (resolves issue #357). + **Best Practices**: - Always specify \`sourceOutput\` for AI connections (defaults to "main" if omitted) - Connect language model BEFORE creating/enabling AI Agent (validation requirement) diff --git a/src/services/n8n-validation.ts b/src/services/n8n-validation.ts index 8f3481b..2001d96 100644 --- a/src/services/n8n-validation.ts +++ b/src/services/n8n-validation.ts @@ -24,17 +24,31 @@ export const workflowNodeSchema = z.object({ executeOnce: z.boolean().optional(), }); +// Connection array schema used by all connection types +const connectionArraySchema = z.array( + z.array( + z.object({ + node: z.string(), + type: z.string(), + index: z.number(), + }) + ) +); + +/** + * Workflow connection schema supporting all connection types. + * Note: 'main' is optional because AI nodes exclusively use AI-specific + * connection types (ai_languageModel, ai_memory, etc.) without main connections. + */ export const workflowConnectionSchema = z.record( z.object({ - main: z.array( - z.array( - z.object({ - node: z.string(), - type: z.string(), - index: z.number(), - }) - ) - ), + main: connectionArraySchema.optional(), + error: connectionArraySchema.optional(), + ai_tool: connectionArraySchema.optional(), + ai_languageModel: connectionArraySchema.optional(), + ai_memory: connectionArraySchema.optional(), + ai_embedding: connectionArraySchema.optional(), + ai_vectorStore: connectionArraySchema.optional(), }) ); diff --git a/tests/integration/workflow-diff/ai-node-connection-validation.test.ts b/tests/integration/workflow-diff/ai-node-connection-validation.test.ts new file mode 100644 index 0000000..a8aedb5 --- /dev/null +++ b/tests/integration/workflow-diff/ai-node-connection-validation.test.ts @@ -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); + }); + }); +});