mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-01-30 22:42:04 +00:00
Compare commits
1 Commits
enhance/ht
...
feature/au
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7da0a2430 |
132
CHANGELOG.md
132
CHANGELOG.md
@@ -7,138 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### ✨ Enhancements
|
||||
|
||||
**Issue #361: Enhanced HTTP Request Node Validation Suggestions**
|
||||
|
||||
Added helpful suggestions for HTTP Request node best practices to prevent common production issues discovered through real-world workflow analysis.
|
||||
|
||||
#### What's New
|
||||
|
||||
1. **alwaysOutputData Suggestion**
|
||||
- Suggests adding `alwaysOutputData: true` at node level (not in parameters)
|
||||
- Prevents silent workflow failures when HTTP requests error
|
||||
- Ensures downstream error handling can process failed requests
|
||||
- Example suggestion: "Consider adding alwaysOutputData: true at node level for better error handling. This ensures the node produces output even when HTTP requests fail, allowing downstream error handling."
|
||||
|
||||
2. **responseFormat Suggestion for API Endpoints**
|
||||
- Suggests setting `options.response.response.responseFormat` for API endpoints
|
||||
- Prevents JSON parsing confusion
|
||||
- Triggered when URL contains `/api`, `/rest`, `supabase`, `firebase`, `googleapis`, or `.com/v` patterns
|
||||
- Example suggestion: "API endpoints should explicitly set options.response.response.responseFormat to 'json' or 'text' to prevent confusion about response parsing"
|
||||
|
||||
3. **Enhanced URL Protocol Validation**
|
||||
- Detects missing protocol in expression-based URLs
|
||||
- Warns about patterns like `=www.{{ $json.domain }}.com` (missing http://)
|
||||
- Warns about expressions without protocol: `={{ $json.domain }}/api/data`
|
||||
- Example warning: "URL expression appears to be missing http:// or https:// protocol"
|
||||
|
||||
#### Investigation Findings
|
||||
|
||||
This enhancement was developed after thorough investigation of issue #361:
|
||||
|
||||
**Key Discoveries:**
|
||||
- ✅ Mixed expression syntax `=literal{{ expression }}` **actually works in n8n** - the issue report's primary claim was incorrect
|
||||
- ✅ Real validation gaps identified: missing `alwaysOutputData` and `responseFormat` checks
|
||||
- ✅ Workflow analysis showed "?" icon in UI caused by missing required URL (already caught by validation)
|
||||
- ✅ Compared broken vs fixed workflows to identify actual production issues
|
||||
|
||||
**Testing Evidence:**
|
||||
- Analyzed workflow SwjKJsJhe8OsYfBk with mixed syntax - executions successful
|
||||
- Compared broken workflow (mBmkyj460i5rYTG4) with fixed workflow (hQI9pby3nSFtk4TV)
|
||||
- Identified that fixed workflow has `alwaysOutputData: true` and explicit `responseFormat: "json"`
|
||||
|
||||
#### Impact
|
||||
|
||||
- **Non-Breaking**: All changes are suggestions/warnings, not errors
|
||||
- **Profile-Aware**: Suggestions shown in all profiles for maximum helpfulness
|
||||
- **Actionable**: Clear guidance on how to implement best practices
|
||||
- **Production-Focused**: Addresses real workflow reliability concerns from actual broken workflows
|
||||
|
||||
#### Test Coverage
|
||||
|
||||
Added 8 new test cases covering:
|
||||
- alwaysOutputData suggestion for all HTTP Request nodes
|
||||
- responseFormat suggestion for API endpoint detection (various patterns)
|
||||
- responseFormat NOT suggested when already configured
|
||||
- URL protocol validation for expression-based URLs
|
||||
- Protocol warnings for missing http:// in expressions
|
||||
- No false positives when protocol is correctly included
|
||||
|
||||
#### Technical Details
|
||||
|
||||
**Files Modified:**
|
||||
- `src/services/enhanced-config-validator.ts` - Added `enhanceHttpRequestValidation()` implementation
|
||||
- `tests/unit/services/enhanced-config-validator.test.ts` - Added 8 comprehensive test cases
|
||||
|
||||
**Validation Flow:**
|
||||
1. Check for alwaysOutputData suggestion (all HTTP Request nodes)
|
||||
2. Detect API endpoints by URL patterns
|
||||
3. Check for explicit responseFormat configuration
|
||||
4. Validate expression-based URLs for protocol issues
|
||||
|
||||
#### Related
|
||||
|
||||
- **Issue**: #361 - validate_node_operation: Missing critical HTTP Request node configuration checks
|
||||
- **Analysis**: Deep investigation with @agent-Explore and @agent-n8n-mcp-tester
|
||||
- **Workflows Analyzed**:
|
||||
- SwjKJsJhe8OsYfBk (mixed syntax test)
|
||||
- mBmkyj460i5rYTG4 (broken workflow)
|
||||
- hQI9pby3nSFtk4TV (fixed workflow)
|
||||
|
||||
Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en
|
||||
|
||||
---
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
**Issue #360: Enhanced Warnings for If/Switch Node Connection Parameters**
|
||||
|
||||
Fixed issue where users could unintentionally place multiple If node connections on the same branch (TRUE/FALSE) when using `sourceIndex` parameter instead of the recommended `branch` parameter. The system now provides helpful warnings to guide users toward better practices.
|
||||
|
||||
#### What Was Fixed
|
||||
|
||||
1. **New Warning System**:
|
||||
- Warns when using `sourceIndex` with If nodes - suggests `branch="true"` or `branch="false"` instead
|
||||
- Warns when using `sourceIndex` with Switch nodes - suggests `case=N` instead
|
||||
- Explains the correct branch structure: `main[0]=TRUE branch, main[1]=FALSE branch`
|
||||
|
||||
2. **Enhanced Documentation**:
|
||||
- Added **CRITICAL** pitfalls to `n8n_update_partial_workflow` tool documentation
|
||||
- Clear guidance that using `sourceIndex=0` for multiple connections puts them ALL on the TRUE branch
|
||||
- Examples showing correct vs. incorrect usage
|
||||
|
||||
3. **Type System Improvements**:
|
||||
- Added `warnings` field to `WorkflowDiffResult` interface
|
||||
- Warnings are non-blocking (operations still succeed)
|
||||
- Differentiated from errors for better UX
|
||||
|
||||
#### Behavior
|
||||
|
||||
The existing `branch` parameter works correctly and has comprehensive test coverage:
|
||||
- `branch="true"` → routes to `main[0]` (TRUE path)
|
||||
- `branch="false"` → routes to `main[1]` (FALSE path)
|
||||
|
||||
The issue was that users who didn't know about the `branch` parameter would naturally use `sourceIndex`, which led to incorrect branch routing.
|
||||
|
||||
#### Example Warning
|
||||
|
||||
```
|
||||
Connection to If node "Check Condition" uses sourceIndex=0.
|
||||
Consider using branch="true" or branch="false" for better clarity.
|
||||
If node outputs: main[0]=TRUE branch, main[1]=FALSE branch.
|
||||
```
|
||||
|
||||
#### Test Coverage
|
||||
|
||||
- Added regression tests that reproduce the exact issue from #360
|
||||
- Verify warnings are generated for If and Switch nodes
|
||||
- Confirm existing smart parameter tests still pass
|
||||
|
||||
**Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en**
|
||||
|
||||
---
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
**Auto-Update Node Versions with Smart Migration**
|
||||
|
||||
BIN
data/nodes.db
BIN
data/nodes.db
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "n8n-mcp",
|
||||
"version": "2.22.2",
|
||||
"version": "2.22.0",
|
||||
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -138,7 +138,6 @@ export async function handleUpdatePartialWorkflow(
|
||||
error: 'Failed to apply diff operations',
|
||||
details: {
|
||||
errors: diffResult.errors,
|
||||
warnings: diffResult.warnings,
|
||||
operationsApplied: diffResult.operationsApplied,
|
||||
applied: diffResult.applied,
|
||||
failed: diffResult.failed
|
||||
@@ -155,9 +154,6 @@ export async function handleUpdatePartialWorkflow(
|
||||
data: {
|
||||
valid: true,
|
||||
operationsToApply: input.operations.length
|
||||
},
|
||||
details: {
|
||||
warnings: diffResult.warnings
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -256,8 +252,7 @@ export async function handleUpdatePartialWorkflow(
|
||||
workflowName: updatedWorkflow.name,
|
||||
applied: diffResult.applied,
|
||||
failed: diffResult.failed,
|
||||
errors: diffResult.errors,
|
||||
warnings: diffResult.warnings
|
||||
errors: diffResult.errors
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
@@ -272,8 +272,6 @@ Please choose a different name.
|
||||
'Use "updates" property for updateNode operations: {type: "updateNode", updates: {...}}',
|
||||
'Smart parameters (branch, case) only work with IF and Switch nodes - ignored for other node types',
|
||||
'Explicit sourceIndex overrides smart parameters (branch, case) if both provided',
|
||||
'**CRITICAL**: For If nodes, ALWAYS use branch="true"/"false" instead of sourceIndex. Using sourceIndex=0 for multiple connections will put them ALL on the TRUE branch (main[0]), breaking your workflow logic!',
|
||||
'**CRITICAL**: For Switch nodes, ALWAYS use case=N instead of sourceIndex. Using same sourceIndex for multiple connections will put them on the same case output.',
|
||||
'cleanStaleConnections removes ALL broken connections - cannot be selective',
|
||||
'replaceConnections overwrites entire connections object - all previous connections lost',
|
||||
'**Auto-sanitization behavior**: Binary operators (equals, contains) automatically have singleValue removed; unary operators (isEmpty, isNotEmpty) automatically get singleValue:true added',
|
||||
|
||||
@@ -401,59 +401,7 @@ export class EnhancedConfigValidator extends ConfigValidator {
|
||||
config: Record<string, any>,
|
||||
result: EnhancedValidationResult
|
||||
): void {
|
||||
const url = String(config.url || '');
|
||||
const options = config.options || {};
|
||||
|
||||
// 1. Suggest alwaysOutputData for better error handling (node-level property)
|
||||
// Note: We can't check if it exists (it's node-level, not in parameters),
|
||||
// but we can suggest it as a best practice
|
||||
if (!result.suggestions.some(s => typeof s === 'string' && s.includes('alwaysOutputData'))) {
|
||||
result.suggestions.push(
|
||||
'Consider adding alwaysOutputData: true at node level (not in parameters) for better error handling. ' +
|
||||
'This ensures the node produces output even when HTTP requests fail, allowing downstream error handling.'
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Suggest responseFormat for API endpoints
|
||||
const lowerUrl = url.toLowerCase();
|
||||
const isApiEndpoint =
|
||||
// Subdomain patterns (api.example.com)
|
||||
/^https?:\/\/api\./i.test(url) ||
|
||||
// Path patterns with word boundaries to prevent false positives like "therapist", "restaurant"
|
||||
/\/api[\/\?]|\/api$/i.test(url) ||
|
||||
/\/rest[\/\?]|\/rest$/i.test(url) ||
|
||||
// Known API service domains
|
||||
lowerUrl.includes('supabase.co') ||
|
||||
lowerUrl.includes('firebase') ||
|
||||
lowerUrl.includes('googleapis.com') ||
|
||||
// Versioned API paths (e.g., example.com/v1, example.com/v2)
|
||||
/\.com\/v\d+/i.test(url);
|
||||
|
||||
if (isApiEndpoint && !options.response?.response?.responseFormat) {
|
||||
result.suggestions.push(
|
||||
'API endpoints should explicitly set options.response.response.responseFormat to "json" or "text" ' +
|
||||
'to prevent confusion about response parsing. Example: ' +
|
||||
'{ "options": { "response": { "response": { "responseFormat": "json" } } } }'
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Enhanced URL protocol validation for expressions
|
||||
if (url && url.startsWith('=')) {
|
||||
// Expression-based URL - check for common protocol issues
|
||||
const expressionContent = url.slice(1); // Remove = prefix
|
||||
const lowerExpression = expressionContent.toLowerCase();
|
||||
|
||||
// Check for missing protocol in expression (case-insensitive)
|
||||
if (expressionContent.startsWith('www.') ||
|
||||
(expressionContent.includes('{{') && !lowerExpression.includes('http'))) {
|
||||
result.warnings.push({
|
||||
type: 'invalid_value',
|
||||
property: 'url',
|
||||
message: 'URL expression appears to be missing http:// or https:// protocol',
|
||||
suggestion: 'Include protocol in your expression. Example: ={{ "https://" + $json.domain + ".com" }}'
|
||||
});
|
||||
}
|
||||
}
|
||||
// Examples removed - validation provides error messages and fixes instead
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,8 +38,6 @@ const logger = new Logger({ prefix: '[WorkflowDiffEngine]' });
|
||||
export class WorkflowDiffEngine {
|
||||
// Track node name changes during operations for connection reference updates
|
||||
private renameMap: Map<string, string> = new Map();
|
||||
// Track warnings during operation processing
|
||||
private warnings: WorkflowDiffValidationError[] = [];
|
||||
|
||||
/**
|
||||
* Apply diff operations to a workflow
|
||||
@@ -49,9 +47,8 @@ export class WorkflowDiffEngine {
|
||||
request: WorkflowDiffRequest
|
||||
): Promise<WorkflowDiffResult> {
|
||||
try {
|
||||
// Reset tracking for this diff operation
|
||||
// Reset rename tracking for this diff operation
|
||||
this.renameMap.clear();
|
||||
this.warnings = [];
|
||||
|
||||
// Clone workflow to avoid modifying original
|
||||
const workflowCopy = JSON.parse(JSON.stringify(workflow));
|
||||
@@ -117,7 +114,6 @@ export class WorkflowDiffEngine {
|
||||
? 'Validation successful. All operations are valid.'
|
||||
: `Validation completed with ${errors.length} errors.`,
|
||||
errors: errors.length > 0 ? errors : undefined,
|
||||
warnings: this.warnings.length > 0 ? this.warnings : undefined,
|
||||
applied: appliedIndices,
|
||||
failed: failedIndices
|
||||
};
|
||||
@@ -130,7 +126,6 @@ export class WorkflowDiffEngine {
|
||||
operationsApplied: appliedIndices.length,
|
||||
message: `Applied ${appliedIndices.length} operations, ${failedIndices.length} failed (continueOnError mode)`,
|
||||
errors: errors.length > 0 ? errors : undefined,
|
||||
warnings: this.warnings.length > 0 ? this.warnings : undefined,
|
||||
applied: appliedIndices,
|
||||
failed: failedIndices
|
||||
};
|
||||
@@ -218,8 +213,7 @@ export class WorkflowDiffEngine {
|
||||
success: true,
|
||||
workflow: workflowCopy,
|
||||
operationsApplied,
|
||||
message: `Successfully applied ${operationsApplied} operations (${nodeOperations.length} node ops, ${otherOperations.length} other ops)`,
|
||||
warnings: this.warnings.length > 0 ? this.warnings : undefined
|
||||
message: `Successfully applied ${operationsApplied} operations (${nodeOperations.length} node ops, ${otherOperations.length} other ops)`
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -691,24 +685,6 @@ export class WorkflowDiffEngine {
|
||||
sourceIndex = operation.case;
|
||||
}
|
||||
|
||||
// Validation: Warn if using sourceIndex with If/Switch nodes without smart parameters
|
||||
if (sourceNode && operation.sourceIndex !== undefined && operation.branch === undefined && operation.case === undefined) {
|
||||
if (sourceNode.type === 'n8n-nodes-base.if') {
|
||||
this.warnings.push({
|
||||
operation: -1, // Not tied to specific operation index in request
|
||||
message: `Connection to If node "${operation.source}" uses sourceIndex=${operation.sourceIndex}. ` +
|
||||
`Consider using branch="true" or branch="false" for better clarity. ` +
|
||||
`If node outputs: main[0]=TRUE branch, main[1]=FALSE branch.`
|
||||
});
|
||||
} else if (sourceNode.type === 'n8n-nodes-base.switch') {
|
||||
this.warnings.push({
|
||||
operation: -1, // Not tied to specific operation index in request
|
||||
message: `Connection to Switch node "${operation.source}" uses sourceIndex=${operation.sourceIndex}. ` +
|
||||
`Consider using case=N for better clarity (case=0 for first output, case=1 for second, etc.).`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { sourceOutput, sourceIndex };
|
||||
}
|
||||
|
||||
|
||||
@@ -170,7 +170,6 @@ export interface WorkflowDiffResult {
|
||||
success: boolean;
|
||||
workflow?: any; // Updated workflow if successful
|
||||
errors?: WorkflowDiffValidationError[];
|
||||
warnings?: WorkflowDiffValidationError[]; // Non-blocking warnings (e.g., parameter suggestions)
|
||||
operationsApplied?: number;
|
||||
message?: string;
|
||||
applied?: number[]; // Indices of successfully applied operations (when continueOnError is true)
|
||||
|
||||
@@ -188,7 +188,6 @@ describe('handlers-workflow-diff', () => {
|
||||
operationsApplied: 1,
|
||||
message: 'Validation successful',
|
||||
errors: [],
|
||||
warnings: []
|
||||
});
|
||||
|
||||
const result = await handleUpdatePartialWorkflow(diffRequest, mockRepository);
|
||||
@@ -200,9 +199,6 @@ describe('handlers-workflow-diff', () => {
|
||||
valid: true,
|
||||
operationsToApply: 1,
|
||||
},
|
||||
details: {
|
||||
warnings: []
|
||||
}
|
||||
});
|
||||
|
||||
expect(mockApiClient.updateWorkflow).not.toHaveBeenCalled();
|
||||
|
||||
@@ -802,335 +802,4 @@ describe('EnhancedConfigValidator', () => {
|
||||
expect(result.errors[0].property).toBe('test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('enhanceHttpRequestValidation', () => {
|
||||
it('should suggest alwaysOutputData for HTTP Request nodes', () => {
|
||||
const nodeType = 'nodes-base.httpRequest';
|
||||
const config = {
|
||||
url: 'https://api.example.com/data',
|
||||
method: 'GET'
|
||||
};
|
||||
const properties = [
|
||||
{ name: 'url', type: 'string', required: true },
|
||||
{ name: 'method', type: 'options', required: false }
|
||||
];
|
||||
|
||||
const result = EnhancedConfigValidator.validateWithMode(
|
||||
nodeType,
|
||||
config,
|
||||
properties,
|
||||
'operation',
|
||||
'ai-friendly'
|
||||
);
|
||||
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.suggestions).toContainEqual(
|
||||
expect.stringContaining('alwaysOutputData: true at node level')
|
||||
);
|
||||
expect(result.suggestions).toContainEqual(
|
||||
expect.stringContaining('ensures the node produces output even when HTTP requests fail')
|
||||
);
|
||||
});
|
||||
|
||||
it('should suggest responseFormat for API endpoint URLs', () => {
|
||||
const nodeType = 'nodes-base.httpRequest';
|
||||
const config = {
|
||||
url: 'https://api.example.com/data',
|
||||
method: 'GET',
|
||||
options: {} // Empty options, no responseFormat
|
||||
};
|
||||
const properties = [
|
||||
{ name: 'url', type: 'string', required: true },
|
||||
{ name: 'method', type: 'options', required: false },
|
||||
{ name: 'options', type: 'collection', required: false }
|
||||
];
|
||||
|
||||
const result = EnhancedConfigValidator.validateWithMode(
|
||||
nodeType,
|
||||
config,
|
||||
properties,
|
||||
'operation',
|
||||
'ai-friendly'
|
||||
);
|
||||
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.suggestions).toContainEqual(
|
||||
expect.stringContaining('responseFormat')
|
||||
);
|
||||
expect(result.suggestions).toContainEqual(
|
||||
expect.stringContaining('options.response.response.responseFormat')
|
||||
);
|
||||
});
|
||||
|
||||
it('should suggest responseFormat for Supabase URLs', () => {
|
||||
const nodeType = 'nodes-base.httpRequest';
|
||||
const config = {
|
||||
url: 'https://xxciwnthnnywanbplqwg.supabase.co/rest/v1/messages',
|
||||
method: 'GET',
|
||||
options: {}
|
||||
};
|
||||
const properties = [
|
||||
{ name: 'url', type: 'string', required: true }
|
||||
];
|
||||
|
||||
const result = EnhancedConfigValidator.validateWithMode(
|
||||
nodeType,
|
||||
config,
|
||||
properties,
|
||||
'operation',
|
||||
'ai-friendly'
|
||||
);
|
||||
|
||||
expect(result.suggestions).toContainEqual(
|
||||
expect.stringContaining('responseFormat')
|
||||
);
|
||||
});
|
||||
|
||||
it('should NOT suggest responseFormat when already configured', () => {
|
||||
const nodeType = 'nodes-base.httpRequest';
|
||||
const config = {
|
||||
url: 'https://api.example.com/data',
|
||||
method: 'GET',
|
||||
options: {
|
||||
response: {
|
||||
response: {
|
||||
responseFormat: 'json'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const properties = [
|
||||
{ name: 'url', type: 'string', required: true },
|
||||
{ name: 'options', type: 'collection', required: false }
|
||||
];
|
||||
|
||||
const result = EnhancedConfigValidator.validateWithMode(
|
||||
nodeType,
|
||||
config,
|
||||
properties,
|
||||
'operation',
|
||||
'ai-friendly'
|
||||
);
|
||||
|
||||
const responseFormatSuggestion = result.suggestions.find(
|
||||
(s: string) => s.includes('responseFormat')
|
||||
);
|
||||
expect(responseFormatSuggestion).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should warn about missing protocol in expression-based URLs', () => {
|
||||
const nodeType = 'nodes-base.httpRequest';
|
||||
const config = {
|
||||
url: '=www.{{ $json.domain }}.com',
|
||||
method: 'GET'
|
||||
};
|
||||
const properties = [
|
||||
{ name: 'url', type: 'string', required: true }
|
||||
];
|
||||
|
||||
const result = EnhancedConfigValidator.validateWithMode(
|
||||
nodeType,
|
||||
config,
|
||||
properties,
|
||||
'operation',
|
||||
'ai-friendly'
|
||||
);
|
||||
|
||||
expect(result.warnings).toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: 'invalid_value',
|
||||
property: 'url',
|
||||
message: expect.stringContaining('missing http:// or https://')
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should warn about missing protocol in expressions with template markers', () => {
|
||||
const nodeType = 'nodes-base.httpRequest';
|
||||
const config = {
|
||||
url: '={{ $json.domain }}/api/data',
|
||||
method: 'GET'
|
||||
};
|
||||
const properties = [
|
||||
{ name: 'url', type: 'string', required: true }
|
||||
];
|
||||
|
||||
const result = EnhancedConfigValidator.validateWithMode(
|
||||
nodeType,
|
||||
config,
|
||||
properties,
|
||||
'operation',
|
||||
'ai-friendly'
|
||||
);
|
||||
|
||||
expect(result.warnings).toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: 'invalid_value',
|
||||
property: 'url',
|
||||
message: expect.stringContaining('missing http:// or https://')
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should NOT warn when expression includes http protocol', () => {
|
||||
const nodeType = 'nodes-base.httpRequest';
|
||||
const config = {
|
||||
url: '={{ "https://" + $json.domain + ".com" }}',
|
||||
method: 'GET'
|
||||
};
|
||||
const properties = [
|
||||
{ name: 'url', type: 'string', required: true }
|
||||
];
|
||||
|
||||
const result = EnhancedConfigValidator.validateWithMode(
|
||||
nodeType,
|
||||
config,
|
||||
properties,
|
||||
'operation',
|
||||
'ai-friendly'
|
||||
);
|
||||
|
||||
const urlWarning = result.warnings.find(
|
||||
(w: any) => w.property === 'url' && w.message.includes('protocol')
|
||||
);
|
||||
expect(urlWarning).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should NOT suggest responseFormat for non-API URLs', () => {
|
||||
const nodeType = 'nodes-base.httpRequest';
|
||||
const config = {
|
||||
url: 'https://example.com/page.html',
|
||||
method: 'GET',
|
||||
options: {}
|
||||
};
|
||||
const properties = [
|
||||
{ name: 'url', type: 'string', required: true }
|
||||
];
|
||||
|
||||
const result = EnhancedConfigValidator.validateWithMode(
|
||||
nodeType,
|
||||
config,
|
||||
properties,
|
||||
'operation',
|
||||
'ai-friendly'
|
||||
);
|
||||
|
||||
const responseFormatSuggestion = result.suggestions.find(
|
||||
(s: string) => s.includes('responseFormat')
|
||||
);
|
||||
expect(responseFormatSuggestion).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should detect missing protocol in expressions with uppercase HTTP', () => {
|
||||
const nodeType = 'nodes-base.httpRequest';
|
||||
const config = {
|
||||
url: '={{ "HTTP://" + $json.domain + ".com" }}',
|
||||
method: 'GET'
|
||||
};
|
||||
const properties = [
|
||||
{ name: 'url', type: 'string', required: true }
|
||||
];
|
||||
|
||||
const result = EnhancedConfigValidator.validateWithMode(
|
||||
nodeType,
|
||||
config,
|
||||
properties,
|
||||
'operation',
|
||||
'ai-friendly'
|
||||
);
|
||||
|
||||
// Should NOT warn because HTTP:// is present (case-insensitive)
|
||||
expect(result.warnings).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should NOT suggest responseFormat for false positive URLs', () => {
|
||||
const nodeType = 'nodes-base.httpRequest';
|
||||
const testUrls = [
|
||||
'https://example.com/therapist-directory',
|
||||
'https://restaurant-bookings.com/reserve',
|
||||
'https://forest-management.org/data'
|
||||
];
|
||||
|
||||
testUrls.forEach(url => {
|
||||
const config = {
|
||||
url,
|
||||
method: 'GET',
|
||||
options: {}
|
||||
};
|
||||
const properties = [
|
||||
{ name: 'url', type: 'string', required: true }
|
||||
];
|
||||
|
||||
const result = EnhancedConfigValidator.validateWithMode(
|
||||
nodeType,
|
||||
config,
|
||||
properties,
|
||||
'operation',
|
||||
'ai-friendly'
|
||||
);
|
||||
|
||||
const responseFormatSuggestion = result.suggestions.find(
|
||||
(s: string) => s.includes('responseFormat')
|
||||
);
|
||||
expect(responseFormatSuggestion).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should suggest responseFormat for case-insensitive API paths', () => {
|
||||
const nodeType = 'nodes-base.httpRequest';
|
||||
const testUrls = [
|
||||
'https://example.com/API/users',
|
||||
'https://example.com/Rest/data',
|
||||
'https://example.com/REST/v1/items'
|
||||
];
|
||||
|
||||
testUrls.forEach(url => {
|
||||
const config = {
|
||||
url,
|
||||
method: 'GET',
|
||||
options: {}
|
||||
};
|
||||
const properties = [
|
||||
{ name: 'url', type: 'string', required: true }
|
||||
];
|
||||
|
||||
const result = EnhancedConfigValidator.validateWithMode(
|
||||
nodeType,
|
||||
config,
|
||||
properties,
|
||||
'operation',
|
||||
'ai-friendly'
|
||||
);
|
||||
|
||||
expect(result.suggestions).toContainEqual(
|
||||
expect.stringContaining('responseFormat')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle null and undefined URLs gracefully', () => {
|
||||
const nodeType = 'nodes-base.httpRequest';
|
||||
const testConfigs = [
|
||||
{ url: null, method: 'GET' },
|
||||
{ url: undefined, method: 'GET' },
|
||||
{ url: '', method: 'GET' }
|
||||
];
|
||||
|
||||
testConfigs.forEach(config => {
|
||||
const properties = [
|
||||
{ name: 'url', type: 'string', required: true }
|
||||
];
|
||||
|
||||
expect(() => {
|
||||
EnhancedConfigValidator.validateWithMode(
|
||||
nodeType,
|
||||
config,
|
||||
properties,
|
||||
'operation',
|
||||
'ai-friendly'
|
||||
);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1418,113 +1418,6 @@ describe('WorkflowDiffEngine', () => {
|
||||
expect(result.workflow!.connections['Switch']['main'][2][0].node).toBe('Handler');
|
||||
expect(result.workflow!.connections['Switch']['main'][1]).toEqual([]);
|
||||
});
|
||||
|
||||
it('should warn when using sourceIndex with If node (issue #360)', async () => {
|
||||
const addIF: any = {
|
||||
type: 'addNode',
|
||||
node: {
|
||||
name: 'Check Condition',
|
||||
type: 'n8n-nodes-base.if',
|
||||
position: [400, 300]
|
||||
}
|
||||
};
|
||||
|
||||
const addSuccess: any = {
|
||||
type: 'addNode',
|
||||
node: {
|
||||
name: 'Success Handler',
|
||||
type: 'n8n-nodes-base.set',
|
||||
position: [600, 200]
|
||||
}
|
||||
};
|
||||
|
||||
const addError: any = {
|
||||
type: 'addNode',
|
||||
node: {
|
||||
name: 'Error Handler',
|
||||
type: 'n8n-nodes-base.set',
|
||||
position: [600, 400]
|
||||
}
|
||||
};
|
||||
|
||||
// BAD: Using sourceIndex with If node (reproduces issue #360)
|
||||
const connectSuccess: any = {
|
||||
type: 'addConnection',
|
||||
source: 'Check Condition',
|
||||
target: 'Success Handler',
|
||||
sourceIndex: 0 // Should use branch="true" instead
|
||||
};
|
||||
|
||||
const connectError: any = {
|
||||
type: 'addConnection',
|
||||
source: 'Check Condition',
|
||||
target: 'Error Handler',
|
||||
sourceIndex: 0 // Should use branch="false" instead - both will end up in main[0]!
|
||||
};
|
||||
|
||||
const request: WorkflowDiffRequest = {
|
||||
id: 'test-workflow',
|
||||
operations: [addIF, addSuccess, addError, connectSuccess, connectError]
|
||||
};
|
||||
|
||||
const result = await diffEngine.applyDiff(baseWorkflow, request);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
// Should produce warnings
|
||||
expect(result.warnings).toBeDefined();
|
||||
expect(result.warnings!.length).toBe(2);
|
||||
expect(result.warnings![0].message).toContain('Consider using branch="true" or branch="false"');
|
||||
expect(result.warnings![0].message).toContain('If node outputs: main[0]=TRUE branch, main[1]=FALSE branch');
|
||||
expect(result.warnings![1].message).toContain('Consider using branch="true" or branch="false"');
|
||||
|
||||
// Both connections end up in main[0] (the bug behavior)
|
||||
expect(result.workflow!.connections['Check Condition']['main'][0].length).toBe(2);
|
||||
expect(result.workflow!.connections['Check Condition']['main'][0][0].node).toBe('Success Handler');
|
||||
expect(result.workflow!.connections['Check Condition']['main'][0][1].node).toBe('Error Handler');
|
||||
});
|
||||
|
||||
it('should warn when using sourceIndex with Switch node', async () => {
|
||||
const addSwitch: any = {
|
||||
type: 'addNode',
|
||||
node: {
|
||||
name: 'Switch',
|
||||
type: 'n8n-nodes-base.switch',
|
||||
position: [400, 300]
|
||||
}
|
||||
};
|
||||
|
||||
const addHandler: any = {
|
||||
type: 'addNode',
|
||||
node: {
|
||||
name: 'Handler',
|
||||
type: 'n8n-nodes-base.set',
|
||||
position: [600, 300]
|
||||
}
|
||||
};
|
||||
|
||||
// BAD: Using sourceIndex with Switch node
|
||||
const connect: any = {
|
||||
type: 'addConnection',
|
||||
source: 'Switch',
|
||||
target: 'Handler',
|
||||
sourceIndex: 1 // Should use case=1 instead
|
||||
};
|
||||
|
||||
const request: WorkflowDiffRequest = {
|
||||
id: 'test-workflow',
|
||||
operations: [addSwitch, addHandler, connect]
|
||||
};
|
||||
|
||||
const result = await diffEngine.applyDiff(baseWorkflow, request);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
// Should produce warning
|
||||
expect(result.warnings).toBeDefined();
|
||||
expect(result.warnings!.length).toBe(1);
|
||||
expect(result.warnings![0].message).toContain('Consider using case=N for better clarity');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AddConnection with sourceIndex (Phase 0 Fix)', () => {
|
||||
|
||||
Reference in New Issue
Block a user