agentic-qe 2.5.6 → 2.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/n8n/n8n-base-agent.md +376 -0
- package/.claude/agents/n8n/n8n-bdd-scenario-tester.md +613 -0
- package/.claude/agents/n8n/n8n-chaos-tester.md +654 -0
- package/.claude/agents/n8n/n8n-ci-orchestrator.md +850 -0
- package/.claude/agents/n8n/n8n-compliance-validator.md +685 -0
- package/.claude/agents/n8n/n8n-expression-validator.md +560 -0
- package/.claude/agents/n8n/n8n-integration-test.md +602 -0
- package/.claude/agents/n8n/n8n-monitoring-validator.md +589 -0
- package/.claude/agents/n8n/n8n-node-validator.md +455 -0
- package/.claude/agents/n8n/n8n-performance-tester.md +630 -0
- package/.claude/agents/n8n/n8n-security-auditor.md +786 -0
- package/.claude/agents/n8n/n8n-trigger-test.md +500 -0
- package/.claude/agents/n8n/n8n-unit-tester.md +633 -0
- package/.claude/agents/n8n/n8n-version-comparator.md +567 -0
- package/.claude/agents/n8n/n8n-workflow-executor.md +392 -0
- package/.claude/skills/n8n-expression-testing/SKILL.md +434 -0
- package/.claude/skills/n8n-integration-testing-patterns/SKILL.md +540 -0
- package/.claude/skills/n8n-security-testing/SKILL.md +599 -0
- package/.claude/skills/n8n-trigger-testing-strategies/SKILL.md +541 -0
- package/.claude/skills/n8n-workflow-testing-fundamentals/SKILL.md +447 -0
- package/CHANGELOG.md +41 -0
- package/README.md +7 -4
- package/dist/agents/n8n/N8nAPIClient.d.ts +121 -0
- package/dist/agents/n8n/N8nAPIClient.d.ts.map +1 -0
- package/dist/agents/n8n/N8nAPIClient.js +367 -0
- package/dist/agents/n8n/N8nAPIClient.js.map +1 -0
- package/dist/agents/n8n/N8nAuditPersistence.d.ts +120 -0
- package/dist/agents/n8n/N8nAuditPersistence.d.ts.map +1 -0
- package/dist/agents/n8n/N8nAuditPersistence.js +473 -0
- package/dist/agents/n8n/N8nAuditPersistence.js.map +1 -0
- package/dist/agents/n8n/N8nBDDScenarioTesterAgent.d.ts +159 -0
- package/dist/agents/n8n/N8nBDDScenarioTesterAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nBDDScenarioTesterAgent.js +697 -0
- package/dist/agents/n8n/N8nBDDScenarioTesterAgent.js.map +1 -0
- package/dist/agents/n8n/N8nBaseAgent.d.ts +126 -0
- package/dist/agents/n8n/N8nBaseAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nBaseAgent.js +446 -0
- package/dist/agents/n8n/N8nBaseAgent.js.map +1 -0
- package/dist/agents/n8n/N8nCIOrchestratorAgent.d.ts +164 -0
- package/dist/agents/n8n/N8nCIOrchestratorAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nCIOrchestratorAgent.js +610 -0
- package/dist/agents/n8n/N8nCIOrchestratorAgent.js.map +1 -0
- package/dist/agents/n8n/N8nChaosTesterAgent.d.ts +205 -0
- package/dist/agents/n8n/N8nChaosTesterAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nChaosTesterAgent.js +729 -0
- package/dist/agents/n8n/N8nChaosTesterAgent.js.map +1 -0
- package/dist/agents/n8n/N8nComplianceValidatorAgent.d.ts +228 -0
- package/dist/agents/n8n/N8nComplianceValidatorAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nComplianceValidatorAgent.js +986 -0
- package/dist/agents/n8n/N8nComplianceValidatorAgent.js.map +1 -0
- package/dist/agents/n8n/N8nContractTesterAgent.d.ts +213 -0
- package/dist/agents/n8n/N8nContractTesterAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nContractTesterAgent.js +989 -0
- package/dist/agents/n8n/N8nContractTesterAgent.js.map +1 -0
- package/dist/agents/n8n/N8nExpressionValidatorAgent.d.ts +99 -0
- package/dist/agents/n8n/N8nExpressionValidatorAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nExpressionValidatorAgent.js +632 -0
- package/dist/agents/n8n/N8nExpressionValidatorAgent.js.map +1 -0
- package/dist/agents/n8n/N8nFailureModeTesterAgent.d.ts +238 -0
- package/dist/agents/n8n/N8nFailureModeTesterAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nFailureModeTesterAgent.js +956 -0
- package/dist/agents/n8n/N8nFailureModeTesterAgent.js.map +1 -0
- package/dist/agents/n8n/N8nIdempotencyTesterAgent.d.ts +242 -0
- package/dist/agents/n8n/N8nIdempotencyTesterAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nIdempotencyTesterAgent.js +992 -0
- package/dist/agents/n8n/N8nIdempotencyTesterAgent.js.map +1 -0
- package/dist/agents/n8n/N8nIntegrationTestAgent.d.ts +104 -0
- package/dist/agents/n8n/N8nIntegrationTestAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nIntegrationTestAgent.js +653 -0
- package/dist/agents/n8n/N8nIntegrationTestAgent.js.map +1 -0
- package/dist/agents/n8n/N8nMonitoringValidatorAgent.d.ts +210 -0
- package/dist/agents/n8n/N8nMonitoringValidatorAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nMonitoringValidatorAgent.js +669 -0
- package/dist/agents/n8n/N8nMonitoringValidatorAgent.js.map +1 -0
- package/dist/agents/n8n/N8nNodeValidatorAgent.d.ts +142 -0
- package/dist/agents/n8n/N8nNodeValidatorAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nNodeValidatorAgent.js +1090 -0
- package/dist/agents/n8n/N8nNodeValidatorAgent.js.map +1 -0
- package/dist/agents/n8n/N8nPerformanceTesterAgent.d.ts +198 -0
- package/dist/agents/n8n/N8nPerformanceTesterAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nPerformanceTesterAgent.js +653 -0
- package/dist/agents/n8n/N8nPerformanceTesterAgent.js.map +1 -0
- package/dist/agents/n8n/N8nReplayabilityTesterAgent.d.ts +245 -0
- package/dist/agents/n8n/N8nReplayabilityTesterAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nReplayabilityTesterAgent.js +952 -0
- package/dist/agents/n8n/N8nReplayabilityTesterAgent.js.map +1 -0
- package/dist/agents/n8n/N8nSecretsHygieneAuditorAgent.d.ts +325 -0
- package/dist/agents/n8n/N8nSecretsHygieneAuditorAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nSecretsHygieneAuditorAgent.js +1187 -0
- package/dist/agents/n8n/N8nSecretsHygieneAuditorAgent.js.map +1 -0
- package/dist/agents/n8n/N8nSecurityAuditorAgent.d.ts +91 -0
- package/dist/agents/n8n/N8nSecurityAuditorAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nSecurityAuditorAgent.js +825 -0
- package/dist/agents/n8n/N8nSecurityAuditorAgent.js.map +1 -0
- package/dist/agents/n8n/N8nTestHarness.d.ts +131 -0
- package/dist/agents/n8n/N8nTestHarness.d.ts.map +1 -0
- package/dist/agents/n8n/N8nTestHarness.js +456 -0
- package/dist/agents/n8n/N8nTestHarness.js.map +1 -0
- package/dist/agents/n8n/N8nTriggerTestAgent.d.ts +119 -0
- package/dist/agents/n8n/N8nTriggerTestAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nTriggerTestAgent.js +652 -0
- package/dist/agents/n8n/N8nTriggerTestAgent.js.map +1 -0
- package/dist/agents/n8n/N8nUnitTesterAgent.d.ts +130 -0
- package/dist/agents/n8n/N8nUnitTesterAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nUnitTesterAgent.js +522 -0
- package/dist/agents/n8n/N8nUnitTesterAgent.js.map +1 -0
- package/dist/agents/n8n/N8nVersionComparatorAgent.d.ts +201 -0
- package/dist/agents/n8n/N8nVersionComparatorAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nVersionComparatorAgent.js +645 -0
- package/dist/agents/n8n/N8nVersionComparatorAgent.js.map +1 -0
- package/dist/agents/n8n/N8nWorkflowExecutorAgent.d.ts +120 -0
- package/dist/agents/n8n/N8nWorkflowExecutorAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nWorkflowExecutorAgent.js +347 -0
- package/dist/agents/n8n/N8nWorkflowExecutorAgent.js.map +1 -0
- package/dist/agents/n8n/index.d.ts +119 -0
- package/dist/agents/n8n/index.d.ts.map +1 -0
- package/dist/agents/n8n/index.js +298 -0
- package/dist/agents/n8n/index.js.map +1 -0
- package/dist/agents/n8n/types.d.ts +486 -0
- package/dist/agents/n8n/types.d.ts.map +1 -0
- package/dist/agents/n8n/types.js +8 -0
- package/dist/agents/n8n/types.js.map +1 -0
- package/dist/cli/init/agents.d.ts.map +1 -1
- package/dist/cli/init/agents.js +29 -0
- package/dist/cli/init/agents.js.map +1 -1
- package/dist/cli/init/skills.d.ts.map +1 -1
- package/dist/cli/init/skills.js +7 -1
- package/dist/cli/init/skills.js.map +1 -1
- package/dist/core/memory/HNSWVectorMemory.js +1 -1
- package/dist/mcp/server-instructions.d.ts +1 -1
- package/dist/mcp/server-instructions.js +1 -1
- package/docs/reference/agents.md +91 -2
- package/docs/reference/skills.md +97 -2
- package/package.json +2 -2
|
@@ -0,0 +1,956 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* N8nFailureModeTesterAgent
|
|
4
|
+
*
|
|
5
|
+
* Error handling and retry semantics testing for n8n workflows:
|
|
6
|
+
* - Retry/backoff behavior validation
|
|
7
|
+
* - Partial failure handling in loops
|
|
8
|
+
* - Error branch testing
|
|
9
|
+
* - "Continue on fail" behavior
|
|
10
|
+
* - Dead-letter patterns
|
|
11
|
+
* - Timeout handling
|
|
12
|
+
* - Error propagation analysis
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.N8nFailureModeTesterAgent = void 0;
|
|
16
|
+
const N8nBaseAgent_1 = require("./N8nBaseAgent");
|
|
17
|
+
const N8nTestHarness_1 = require("./N8nTestHarness");
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Error Handling Patterns
|
|
20
|
+
// ============================================================================
|
|
21
|
+
const ERROR_PRONE_NODE_TYPES = [
|
|
22
|
+
'n8n-nodes-base.httpRequest',
|
|
23
|
+
'n8n-nodes-base.webhook',
|
|
24
|
+
'n8n-nodes-base.executeCommand',
|
|
25
|
+
'n8n-nodes-base.function',
|
|
26
|
+
'n8n-nodes-base.code',
|
|
27
|
+
'n8n-nodes-base.postgres',
|
|
28
|
+
'n8n-nodes-base.mysql',
|
|
29
|
+
'n8n-nodes-base.mongodb',
|
|
30
|
+
'n8n-nodes-base.redis',
|
|
31
|
+
'n8n-nodes-base.ftp',
|
|
32
|
+
'n8n-nodes-base.ssh',
|
|
33
|
+
'n8n-nodes-base.awsLambda',
|
|
34
|
+
];
|
|
35
|
+
const LOOP_NODE_TYPES = [
|
|
36
|
+
'n8n-nodes-base.splitInBatches',
|
|
37
|
+
'n8n-nodes-base.loop',
|
|
38
|
+
];
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// Agent Implementation
|
|
41
|
+
// ============================================================================
|
|
42
|
+
class N8nFailureModeTesterAgent extends N8nBaseAgent_1.N8nBaseAgent {
|
|
43
|
+
constructor(config) {
|
|
44
|
+
const capabilities = [
|
|
45
|
+
{
|
|
46
|
+
name: 'retry-analysis',
|
|
47
|
+
version: '1.0.0',
|
|
48
|
+
description: 'Analyze retry configurations and backoff behavior',
|
|
49
|
+
parameters: {},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'error-branch-testing',
|
|
53
|
+
version: '1.0.0',
|
|
54
|
+
description: 'Test error branch handling',
|
|
55
|
+
parameters: {},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'continue-on-fail-analysis',
|
|
59
|
+
version: '1.0.0',
|
|
60
|
+
description: 'Analyze continue-on-fail behavior and risks',
|
|
61
|
+
parameters: {},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'fault-injection',
|
|
65
|
+
version: '1.0.0',
|
|
66
|
+
description: 'Inject faults to test error handling',
|
|
67
|
+
parameters: {},
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
super({
|
|
71
|
+
...config,
|
|
72
|
+
type: 'n8n-failure-mode-tester',
|
|
73
|
+
capabilities: [...capabilities, ...(config.capabilities || [])],
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
async performTask(task) {
|
|
77
|
+
const failureTask = task;
|
|
78
|
+
if (failureTask.type !== 'failure-mode-test') {
|
|
79
|
+
throw new Error(`Unsupported task type: ${failureTask.type}`);
|
|
80
|
+
}
|
|
81
|
+
return this.testFailureModes(failureTask.target, failureTask.options);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Test failure modes in workflow
|
|
85
|
+
*/
|
|
86
|
+
async testFailureModes(workflowId, options, providedWorkflow) {
|
|
87
|
+
const workflow = providedWorkflow || await this.getWorkflow(workflowId);
|
|
88
|
+
const findings = [];
|
|
89
|
+
const recommendations = [];
|
|
90
|
+
// 1. Analyze retry configurations
|
|
91
|
+
const retryAnalysis = this.analyzeRetryConfigurations(workflow);
|
|
92
|
+
findings.push(...this.retryConfigToFindings(retryAnalysis));
|
|
93
|
+
// 2. Analyze error branches
|
|
94
|
+
const errorBranchAnalysis = this.analyzeErrorBranches(workflow);
|
|
95
|
+
findings.push(...this.errorBranchToFindings(errorBranchAnalysis, workflow));
|
|
96
|
+
// 3. Analyze continue-on-fail settings
|
|
97
|
+
const continueOnFailAnalysis = this.analyzeContinueOnFail(workflow);
|
|
98
|
+
findings.push(...this.continueOnFailToFindings(continueOnFailAnalysis));
|
|
99
|
+
// 4. Check for partial failure risks in loops
|
|
100
|
+
if (options?.testPartialFailures !== false) {
|
|
101
|
+
findings.push(...this.checkPartialFailureRisks(workflow));
|
|
102
|
+
}
|
|
103
|
+
// 5. Check for timeout risks
|
|
104
|
+
if (options?.testTimeouts !== false) {
|
|
105
|
+
findings.push(...this.checkTimeoutRisks(workflow));
|
|
106
|
+
}
|
|
107
|
+
// 6. Check for dead-letter patterns
|
|
108
|
+
findings.push(...this.checkDeadLetterPatterns(workflow));
|
|
109
|
+
// 7. Analyze DLQ patterns (enhanced)
|
|
110
|
+
const dlqAnalysis = this.analyzeDLQPatterns(workflow);
|
|
111
|
+
// 8. Execute fault injection tests if requested and not dry run
|
|
112
|
+
let faultInjectionResults;
|
|
113
|
+
if (options?.injectFaults && !options.dryRun) {
|
|
114
|
+
faultInjectionResults = await this.executeFaultInjectionTests(workflowId, options.injectFaults, workflow);
|
|
115
|
+
// Add findings from fault injection results
|
|
116
|
+
for (const faultResult of faultInjectionResults) {
|
|
117
|
+
if (!faultResult.errorHandled && faultResult.executed) {
|
|
118
|
+
findings.push({
|
|
119
|
+
type: 'missing-error-handler',
|
|
120
|
+
severity: 'high',
|
|
121
|
+
node: faultResult.fault.targetNode,
|
|
122
|
+
message: `Fault injection test failed: ${faultResult.fault.faultType} was not handled`,
|
|
123
|
+
details: faultResult.details,
|
|
124
|
+
suggestion: 'Add error handling (retry, error branch, or continue-on-fail) for this node',
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (faultResult.dataIntegrity === 'corrupted') {
|
|
128
|
+
findings.push({
|
|
129
|
+
type: 'cascade-failure',
|
|
130
|
+
severity: 'critical',
|
|
131
|
+
node: faultResult.fault.targetNode,
|
|
132
|
+
message: `Data corruption detected after ${faultResult.fault.faultType} fault`,
|
|
133
|
+
details: `Error propagated to: ${faultResult.errorPropagation.join(' -> ')}`,
|
|
134
|
+
suggestion: 'Add data validation and rollback mechanisms',
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Generate recommendations
|
|
140
|
+
recommendations.push(...this.generateRecommendations(findings, retryAnalysis, errorBranchAnalysis, continueOnFailAnalysis));
|
|
141
|
+
recommendations.push(...dlqAnalysis.recommendations);
|
|
142
|
+
// Calculate scores
|
|
143
|
+
const errorHandlingScore = this.calculateErrorHandlingScore(errorBranchAnalysis, retryAnalysis);
|
|
144
|
+
const resilienceScore = this.calculateResilienceScore(findings);
|
|
145
|
+
const score = Math.round((errorHandlingScore + resilienceScore) / 2);
|
|
146
|
+
const result = {
|
|
147
|
+
workflowId: workflow.id || workflowId,
|
|
148
|
+
workflowName: workflow.name,
|
|
149
|
+
testDate: new Date().toISOString(),
|
|
150
|
+
passed: findings.filter(f => f.severity === 'critical' || f.severity === 'high').length === 0,
|
|
151
|
+
score,
|
|
152
|
+
errorHandlingScore,
|
|
153
|
+
resilienceScore,
|
|
154
|
+
findings,
|
|
155
|
+
retryAnalysis,
|
|
156
|
+
errorBranchAnalysis,
|
|
157
|
+
continueOnFailAnalysis,
|
|
158
|
+
recommendations,
|
|
159
|
+
faultInjectionResults,
|
|
160
|
+
dlqAnalysis,
|
|
161
|
+
};
|
|
162
|
+
// Store result
|
|
163
|
+
await this.storeTestResult(`failure-mode-test:${workflowId}`, result);
|
|
164
|
+
// Emit event
|
|
165
|
+
this.emitEvent('failure-mode.test.completed', {
|
|
166
|
+
workflowId,
|
|
167
|
+
passed: result.passed,
|
|
168
|
+
errorHandlingScore,
|
|
169
|
+
resilienceScore,
|
|
170
|
+
findingCount: findings.length,
|
|
171
|
+
});
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Analyze retry configurations
|
|
176
|
+
*/
|
|
177
|
+
analyzeRetryConfigurations(workflow) {
|
|
178
|
+
const nodesWithRetry = [];
|
|
179
|
+
const nodesWithoutRetry = [];
|
|
180
|
+
const retryConfigurations = [];
|
|
181
|
+
for (const node of workflow.nodes) {
|
|
182
|
+
const isErrorProne = ERROR_PRONE_NODE_TYPES.some(t => node.type.includes(t));
|
|
183
|
+
const settings = node.parameters;
|
|
184
|
+
// Check for retry settings
|
|
185
|
+
const hasRetry = settings.options && typeof settings.options === 'object' &&
|
|
186
|
+
('retry' in settings.options || 'retryOnFail' in settings.options);
|
|
187
|
+
if (hasRetry) {
|
|
188
|
+
nodesWithRetry.push(node.name);
|
|
189
|
+
const retryOptions = (settings.options || {});
|
|
190
|
+
const retrySubOptions = (retryOptions.retry || {});
|
|
191
|
+
const issues = [];
|
|
192
|
+
// Analyze retry configuration
|
|
193
|
+
const maxRetries = ((retryOptions.maxTries ?? retrySubOptions.maxTries) || 3);
|
|
194
|
+
const waitBetween = ((retryOptions.waitBetweenTries ?? retrySubOptions.waitBetweenTries) || 1000);
|
|
195
|
+
if (maxRetries > 10) {
|
|
196
|
+
issues.push('Excessive retry count may cause long execution times');
|
|
197
|
+
}
|
|
198
|
+
if (maxRetries < 2 && isErrorProne) {
|
|
199
|
+
issues.push('Low retry count for error-prone node type');
|
|
200
|
+
}
|
|
201
|
+
if (waitBetween < 500) {
|
|
202
|
+
issues.push('Short wait between retries may trigger rate limits');
|
|
203
|
+
}
|
|
204
|
+
retryConfigurations.push({
|
|
205
|
+
node: node.name,
|
|
206
|
+
maxRetries,
|
|
207
|
+
backoffType: 'fixed', // n8n uses fixed by default
|
|
208
|
+
waitBetween,
|
|
209
|
+
issues,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
else if (isErrorProne) {
|
|
213
|
+
nodesWithoutRetry.push(node.name);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const errorProneNodes = workflow.nodes.filter(n => ERROR_PRONE_NODE_TYPES.some(t => n.type.includes(t))).length;
|
|
217
|
+
const overallScore = errorProneNodes > 0
|
|
218
|
+
? Math.round((nodesWithRetry.length / errorProneNodes) * 100)
|
|
219
|
+
: 100;
|
|
220
|
+
return {
|
|
221
|
+
nodesWithRetry,
|
|
222
|
+
nodesWithoutRetry,
|
|
223
|
+
retryConfigurations,
|
|
224
|
+
overallScore,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Analyze error branches
|
|
229
|
+
*/
|
|
230
|
+
analyzeErrorBranches(workflow) {
|
|
231
|
+
const nodesWithErrorBranch = [];
|
|
232
|
+
const nodesWithoutErrorBranch = [];
|
|
233
|
+
const errorHandlers = [];
|
|
234
|
+
const orphanedErrorBranches = [];
|
|
235
|
+
for (const node of workflow.nodes) {
|
|
236
|
+
const isErrorProne = ERROR_PRONE_NODE_TYPES.some(t => node.type.includes(t));
|
|
237
|
+
// Check if node has error output connection
|
|
238
|
+
const connections = workflow.connections[node.name];
|
|
239
|
+
const hasErrorBranch = connections?.main && connections.main.length > 1;
|
|
240
|
+
if (hasErrorBranch) {
|
|
241
|
+
nodesWithErrorBranch.push(node.name);
|
|
242
|
+
// Check if error branch leads somewhere
|
|
243
|
+
const errorOutputConnections = connections.main[1];
|
|
244
|
+
if (!errorOutputConnections || errorOutputConnections.length === 0) {
|
|
245
|
+
orphanedErrorBranches.push(node.name);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
for (const conn of errorOutputConnections) {
|
|
249
|
+
errorHandlers.push({
|
|
250
|
+
triggerNode: node.name,
|
|
251
|
+
handlerNode: conn.node,
|
|
252
|
+
handlesAllErrors: true, // Would need runtime analysis to determine
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
else if (isErrorProne) {
|
|
258
|
+
nodesWithoutErrorBranch.push(node.name);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const errorProneNodes = workflow.nodes.filter(n => ERROR_PRONE_NODE_TYPES.some(t => n.type.includes(t))).length;
|
|
262
|
+
const errorBranchCoverage = errorProneNodes > 0
|
|
263
|
+
? Math.round((nodesWithErrorBranch.length / errorProneNodes) * 100)
|
|
264
|
+
: 100;
|
|
265
|
+
return {
|
|
266
|
+
nodesWithErrorBranch,
|
|
267
|
+
nodesWithoutErrorBranch,
|
|
268
|
+
errorBranchCoverage,
|
|
269
|
+
orphanedErrorBranches,
|
|
270
|
+
errorHandlers,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Analyze continue-on-fail settings
|
|
275
|
+
*/
|
|
276
|
+
analyzeContinueOnFail(workflow) {
|
|
277
|
+
const nodesWithContinueOnFail = [];
|
|
278
|
+
const silentFailureRisks = [];
|
|
279
|
+
const dataIntegrityRisks = [];
|
|
280
|
+
for (const node of workflow.nodes) {
|
|
281
|
+
const settings = node.parameters;
|
|
282
|
+
const hasContinueOnFail = settings.continueOnFail === true ||
|
|
283
|
+
(settings.options && settings.options.continueOnFail === true);
|
|
284
|
+
if (hasContinueOnFail) {
|
|
285
|
+
nodesWithContinueOnFail.push(node.name);
|
|
286
|
+
// Check if there's error handling after this node
|
|
287
|
+
const connections = workflow.connections[node.name];
|
|
288
|
+
const hasErrorHandling = connections?.main && connections.main.length > 1;
|
|
289
|
+
if (!hasErrorHandling) {
|
|
290
|
+
silentFailureRisks.push(node.name);
|
|
291
|
+
}
|
|
292
|
+
// Find downstream nodes that might receive bad data
|
|
293
|
+
const downstream = this.getDownstreamNodes(workflow, node.name);
|
|
294
|
+
if (downstream.length > 0) {
|
|
295
|
+
dataIntegrityRisks.push({
|
|
296
|
+
node: node.name,
|
|
297
|
+
risk: 'Continue on fail may pass incomplete/bad data downstream',
|
|
298
|
+
affectedDownstream: downstream.map(n => n.name),
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
nodesWithContinueOnFail,
|
|
305
|
+
silentFailureRisks,
|
|
306
|
+
dataIntegrityRisks,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Check for partial failure risks in loops
|
|
311
|
+
*/
|
|
312
|
+
checkPartialFailureRisks(workflow) {
|
|
313
|
+
const findings = [];
|
|
314
|
+
// Find loop nodes
|
|
315
|
+
const loopNodes = workflow.nodes.filter(n => LOOP_NODE_TYPES.some(t => n.type.includes(t)));
|
|
316
|
+
for (const loopNode of loopNodes) {
|
|
317
|
+
// Check if nodes inside loop have error handling
|
|
318
|
+
const downstream = this.getDownstreamNodes(workflow, loopNode.name);
|
|
319
|
+
const errorProneInLoop = downstream.filter(n => ERROR_PRONE_NODE_TYPES.some(t => n.type.includes(t)));
|
|
320
|
+
for (const errorProneNode of errorProneInLoop) {
|
|
321
|
+
const connections = workflow.connections[errorProneNode.name];
|
|
322
|
+
const hasErrorBranch = connections?.main && connections.main.length > 1;
|
|
323
|
+
if (!hasErrorBranch) {
|
|
324
|
+
findings.push({
|
|
325
|
+
type: 'partial-failure-risk',
|
|
326
|
+
severity: 'high',
|
|
327
|
+
node: errorProneNode.name,
|
|
328
|
+
message: `Error-prone node in loop without error handling`,
|
|
329
|
+
details: `Node "${errorProneNode.name}" is inside loop "${loopNode.name}" but has no error branch. A single item failure could stop the entire loop.`,
|
|
330
|
+
suggestion: 'Add error handling or use "continue on fail" with error logging',
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return findings;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Check for timeout risks
|
|
339
|
+
*/
|
|
340
|
+
checkTimeoutRisks(workflow) {
|
|
341
|
+
const findings = [];
|
|
342
|
+
for (const node of workflow.nodes) {
|
|
343
|
+
// Check HTTP nodes for timeout settings
|
|
344
|
+
if (node.type.includes('httpRequest')) {
|
|
345
|
+
const settings = node.parameters;
|
|
346
|
+
const options = settings.options;
|
|
347
|
+
const timeout = options?.timeout;
|
|
348
|
+
if (!timeout) {
|
|
349
|
+
findings.push({
|
|
350
|
+
type: 'timeout-risk',
|
|
351
|
+
severity: 'medium',
|
|
352
|
+
node: node.name,
|
|
353
|
+
message: 'HTTP request without explicit timeout',
|
|
354
|
+
details: 'Missing timeout may cause workflow to hang indefinitely',
|
|
355
|
+
suggestion: 'Set explicit timeout in node options',
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
else if (timeout > 300000) { // 5 minutes
|
|
359
|
+
findings.push({
|
|
360
|
+
type: 'timeout-risk',
|
|
361
|
+
severity: 'low',
|
|
362
|
+
node: node.name,
|
|
363
|
+
message: 'HTTP request with very long timeout',
|
|
364
|
+
details: `Timeout set to ${timeout / 1000}s which may cause long hangs`,
|
|
365
|
+
suggestion: 'Consider shorter timeout with retry logic',
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// Check execute command nodes
|
|
370
|
+
if (node.type.includes('executeCommand')) {
|
|
371
|
+
findings.push({
|
|
372
|
+
type: 'timeout-risk',
|
|
373
|
+
severity: 'medium',
|
|
374
|
+
node: node.name,
|
|
375
|
+
message: 'Execute command without timeout control',
|
|
376
|
+
details: 'Shell commands can hang indefinitely',
|
|
377
|
+
suggestion: 'Add timeout to command or use n8n timeout settings',
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return findings;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Check for dead-letter patterns
|
|
385
|
+
*/
|
|
386
|
+
checkDeadLetterPatterns(workflow) {
|
|
387
|
+
const findings = [];
|
|
388
|
+
// Check if workflow has any error notification/logging at the end
|
|
389
|
+
const hasErrorWorkflow = workflow.settings?.errorWorkflow;
|
|
390
|
+
const hasErrorNotification = workflow.nodes.some(n => n.type.includes('email') || n.type.includes('slack') || n.type.includes('telegram'));
|
|
391
|
+
// Check for nodes that send to DLQ or error queue
|
|
392
|
+
const hasDLQPattern = workflow.nodes.some(n => {
|
|
393
|
+
const params = JSON.stringify(n.parameters).toLowerCase();
|
|
394
|
+
return params.includes('dlq') || params.includes('dead') || params.includes('error-queue');
|
|
395
|
+
});
|
|
396
|
+
if (!hasErrorWorkflow && !hasErrorNotification && !hasDLQPattern) {
|
|
397
|
+
findings.push({
|
|
398
|
+
type: 'dlq-missing',
|
|
399
|
+
severity: 'medium',
|
|
400
|
+
node: 'workflow',
|
|
401
|
+
message: 'No dead-letter or error notification pattern detected',
|
|
402
|
+
details: 'Failed executions may go unnoticed without error notifications',
|
|
403
|
+
suggestion: 'Add error workflow in workflow settings or add notification node for failures',
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
return findings;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Execute fault injection tests
|
|
410
|
+
*/
|
|
411
|
+
async executeFaultInjection(workflow, faults) {
|
|
412
|
+
const findings = [];
|
|
413
|
+
// Note: Actual fault injection would require modifying workflow or using n8n's testing features
|
|
414
|
+
// This is a placeholder for the concept
|
|
415
|
+
for (const fault of faults) {
|
|
416
|
+
const targetNode = workflow.nodes.find(n => n.name === fault.targetNode);
|
|
417
|
+
if (!targetNode) {
|
|
418
|
+
findings.push({
|
|
419
|
+
type: 'missing-error-handler',
|
|
420
|
+
severity: 'low',
|
|
421
|
+
node: fault.targetNode,
|
|
422
|
+
message: `Fault injection target "${fault.targetNode}" not found`,
|
|
423
|
+
details: 'Cannot inject fault to non-existent node',
|
|
424
|
+
suggestion: 'Check node name spelling',
|
|
425
|
+
});
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
// Check if node has error handling for the injected fault type
|
|
429
|
+
const connections = workflow.connections[fault.targetNode];
|
|
430
|
+
const hasErrorBranch = connections?.main && connections.main.length > 1;
|
|
431
|
+
if (!hasErrorBranch) {
|
|
432
|
+
findings.push({
|
|
433
|
+
type: 'missing-error-handler',
|
|
434
|
+
severity: 'high',
|
|
435
|
+
node: fault.targetNode,
|
|
436
|
+
message: `No error handler for ${fault.faultType} fault`,
|
|
437
|
+
details: `If ${fault.faultType} occurs, the workflow will fail without graceful handling`,
|
|
438
|
+
suggestion: 'Add error branch to handle this failure mode',
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return findings;
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Convert retry analysis to findings
|
|
446
|
+
*/
|
|
447
|
+
retryConfigToFindings(analysis) {
|
|
448
|
+
const findings = [];
|
|
449
|
+
for (const node of analysis.nodesWithoutRetry) {
|
|
450
|
+
findings.push({
|
|
451
|
+
type: 'retry-misconfiguration',
|
|
452
|
+
severity: 'medium',
|
|
453
|
+
node,
|
|
454
|
+
message: 'Error-prone node without retry configuration',
|
|
455
|
+
details: 'Transient failures will cause immediate workflow failure',
|
|
456
|
+
suggestion: 'Add retry settings in node options',
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
for (const config of analysis.retryConfigurations) {
|
|
460
|
+
for (const issue of config.issues) {
|
|
461
|
+
findings.push({
|
|
462
|
+
type: 'retry-misconfiguration',
|
|
463
|
+
severity: 'low',
|
|
464
|
+
node: config.node,
|
|
465
|
+
message: issue,
|
|
466
|
+
details: `Current retry config: ${config.maxRetries} retries, ${config.waitBetween}ms wait`,
|
|
467
|
+
suggestion: 'Adjust retry settings based on expected failure modes',
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return findings;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Convert error branch analysis to findings
|
|
475
|
+
*/
|
|
476
|
+
errorBranchToFindings(analysis, workflow) {
|
|
477
|
+
const findings = [];
|
|
478
|
+
for (const node of analysis.nodesWithoutErrorBranch) {
|
|
479
|
+
findings.push({
|
|
480
|
+
type: 'missing-error-handler',
|
|
481
|
+
severity: 'medium',
|
|
482
|
+
node,
|
|
483
|
+
message: 'Error-prone node without error branch',
|
|
484
|
+
details: 'Errors will propagate to workflow level without graceful handling',
|
|
485
|
+
suggestion: 'Add error branch to handle failures gracefully',
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
for (const orphan of analysis.orphanedErrorBranches) {
|
|
489
|
+
findings.push({
|
|
490
|
+
type: 'missing-error-handler',
|
|
491
|
+
severity: 'low',
|
|
492
|
+
node: orphan,
|
|
493
|
+
message: 'Error branch exists but is not connected',
|
|
494
|
+
details: 'Error output has no target node',
|
|
495
|
+
suggestion: 'Connect error branch to an error handler node',
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
return findings;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Convert continue-on-fail analysis to findings
|
|
502
|
+
*/
|
|
503
|
+
continueOnFailToFindings(analysis) {
|
|
504
|
+
const findings = [];
|
|
505
|
+
for (const node of analysis.silentFailureRisks) {
|
|
506
|
+
findings.push({
|
|
507
|
+
type: 'silent-failure',
|
|
508
|
+
severity: 'high',
|
|
509
|
+
node,
|
|
510
|
+
message: 'Continue on fail without error tracking',
|
|
511
|
+
details: 'Failures will be silently ignored without logging or notification',
|
|
512
|
+
suggestion: 'Add error logging/notification when using continue on fail',
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
for (const risk of analysis.dataIntegrityRisks) {
|
|
516
|
+
findings.push({
|
|
517
|
+
type: 'cascade-failure',
|
|
518
|
+
severity: 'medium',
|
|
519
|
+
node: risk.node,
|
|
520
|
+
message: risk.risk,
|
|
521
|
+
details: `May affect: ${risk.affectedDownstream.join(', ')}`,
|
|
522
|
+
suggestion: 'Add data validation after nodes with continue on fail',
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
return findings;
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Calculate error handling score
|
|
529
|
+
*/
|
|
530
|
+
calculateErrorHandlingScore(errorBranchAnalysis, retryAnalysis) {
|
|
531
|
+
const errorBranchScore = errorBranchAnalysis.errorBranchCoverage;
|
|
532
|
+
const retryScore = retryAnalysis.overallScore;
|
|
533
|
+
return Math.round((errorBranchScore + retryScore) / 2);
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Calculate resilience score
|
|
537
|
+
*/
|
|
538
|
+
calculateResilienceScore(findings) {
|
|
539
|
+
const criticalCount = findings.filter(f => f.severity === 'critical').length;
|
|
540
|
+
const highCount = findings.filter(f => f.severity === 'high').length;
|
|
541
|
+
const mediumCount = findings.filter(f => f.severity === 'medium').length;
|
|
542
|
+
return Math.max(0, 100 - (criticalCount * 25) - (highCount * 15) - (mediumCount * 5));
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Generate recommendations
|
|
546
|
+
*/
|
|
547
|
+
generateRecommendations(findings, retryAnalysis, errorBranchAnalysis, continueOnFailAnalysis) {
|
|
548
|
+
const recommendations = [];
|
|
549
|
+
if (retryAnalysis.nodesWithoutRetry.length > 0) {
|
|
550
|
+
recommendations.push(`Add retry configuration to ${retryAnalysis.nodesWithoutRetry.length} error-prone nodes`);
|
|
551
|
+
}
|
|
552
|
+
if (errorBranchAnalysis.errorBranchCoverage < 50) {
|
|
553
|
+
recommendations.push('Error branch coverage is low - add error handling to critical nodes');
|
|
554
|
+
}
|
|
555
|
+
if (continueOnFailAnalysis.silentFailureRisks.length > 0) {
|
|
556
|
+
recommendations.push('Add error logging when using "continue on fail" to avoid silent failures');
|
|
557
|
+
}
|
|
558
|
+
const dlqFindings = findings.filter(f => f.type === 'dlq-missing');
|
|
559
|
+
if (dlqFindings.length > 0) {
|
|
560
|
+
recommendations.push('Consider adding error workflow or notification for failed executions');
|
|
561
|
+
}
|
|
562
|
+
return recommendations;
|
|
563
|
+
}
|
|
564
|
+
// ============================================================================
|
|
565
|
+
// Active Fault Injection Testing
|
|
566
|
+
// ============================================================================
|
|
567
|
+
/**
|
|
568
|
+
* Execute fault injection tests against a live workflow
|
|
569
|
+
* This actually runs the workflow with injected faults to verify error handling
|
|
570
|
+
*/
|
|
571
|
+
async executeFaultInjectionTests(workflowId, faults, providedWorkflow) {
|
|
572
|
+
const results = [];
|
|
573
|
+
const workflow = providedWorkflow || await this.getWorkflow(workflowId);
|
|
574
|
+
// If no faults specified, auto-generate faults for error-prone nodes
|
|
575
|
+
const faultsToTest = faults || this.generateDefaultFaults(workflow);
|
|
576
|
+
if (faultsToTest.length === 0) {
|
|
577
|
+
return results;
|
|
578
|
+
}
|
|
579
|
+
// Create test harness
|
|
580
|
+
const harness = new N8nTestHarness_1.N8nTestHarness(this.n8nConfig);
|
|
581
|
+
try {
|
|
582
|
+
for (const fault of faultsToTest) {
|
|
583
|
+
const result = await this.executeSingleFaultTest(harness, workflowId, fault, workflow);
|
|
584
|
+
results.push(result);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
finally {
|
|
588
|
+
await harness.cleanup();
|
|
589
|
+
}
|
|
590
|
+
return results;
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Generate default faults for error-prone nodes
|
|
594
|
+
*/
|
|
595
|
+
generateDefaultFaults(workflow) {
|
|
596
|
+
const faults = [];
|
|
597
|
+
for (const node of workflow.nodes) {
|
|
598
|
+
const isErrorProne = ERROR_PRONE_NODE_TYPES.some(t => node.type.includes(t));
|
|
599
|
+
if (!isErrorProne)
|
|
600
|
+
continue;
|
|
601
|
+
// Add error fault
|
|
602
|
+
faults.push({
|
|
603
|
+
targetNode: node.name,
|
|
604
|
+
faultType: 'error',
|
|
605
|
+
errorMessage: `Simulated error in ${node.name}`,
|
|
606
|
+
});
|
|
607
|
+
// Add timeout fault for HTTP nodes
|
|
608
|
+
if (node.type.includes('httpRequest')) {
|
|
609
|
+
faults.push({
|
|
610
|
+
targetNode: node.name,
|
|
611
|
+
faultType: 'timeout',
|
|
612
|
+
delay: 5000,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
// Add empty response fault
|
|
616
|
+
faults.push({
|
|
617
|
+
targetNode: node.name,
|
|
618
|
+
faultType: 'empty-response',
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
return faults;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Execute a single fault injection test
|
|
625
|
+
*/
|
|
626
|
+
async executeSingleFaultTest(harness, workflowId, fault, workflow) {
|
|
627
|
+
const startTime = Date.now();
|
|
628
|
+
// Convert to harness format
|
|
629
|
+
const faultConfig = {
|
|
630
|
+
targetNode: fault.targetNode,
|
|
631
|
+
faultType: fault.faultType === 'rate-limit' ? 'rate-limit' : fault.faultType,
|
|
632
|
+
probability: fault.probability,
|
|
633
|
+
delay: fault.delay,
|
|
634
|
+
errorMessage: fault.errorMessage,
|
|
635
|
+
};
|
|
636
|
+
try {
|
|
637
|
+
const testResult = await harness.executeWithFaults(workflowId, [faultConfig]);
|
|
638
|
+
// Analyze the result
|
|
639
|
+
return this.analyzeFaultTestResult(fault, testResult, workflow, Date.now() - startTime);
|
|
640
|
+
}
|
|
641
|
+
catch (error) {
|
|
642
|
+
return {
|
|
643
|
+
fault,
|
|
644
|
+
executed: false,
|
|
645
|
+
errorHandled: false,
|
|
646
|
+
retryAttempts: 0,
|
|
647
|
+
finalStatus: 'crashed',
|
|
648
|
+
executionTime: Date.now() - startTime,
|
|
649
|
+
errorPropagation: [],
|
|
650
|
+
dataIntegrity: 'corrupted',
|
|
651
|
+
details: `Failed to execute fault test: ${error.message}`,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Analyze the result of a fault injection test
|
|
657
|
+
*/
|
|
658
|
+
analyzeFaultTestResult(fault, testResult, workflow, executionTime) {
|
|
659
|
+
const execution = testResult.execution;
|
|
660
|
+
if (!execution) {
|
|
661
|
+
return {
|
|
662
|
+
fault,
|
|
663
|
+
executed: true,
|
|
664
|
+
errorHandled: false,
|
|
665
|
+
retryAttempts: 0,
|
|
666
|
+
finalStatus: testResult.error?.includes('timeout') ? 'timeout' : 'crashed',
|
|
667
|
+
executionTime,
|
|
668
|
+
errorPropagation: [],
|
|
669
|
+
dataIntegrity: 'corrupted',
|
|
670
|
+
details: testResult.error || 'Execution failed with no result',
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
// Analyze execution data
|
|
674
|
+
const runData = execution.data?.resultData?.runData || {};
|
|
675
|
+
const errorNodes = this.findErrorNodes(runData);
|
|
676
|
+
const retryAttempts = this.countRetryAttempts(runData, fault.targetNode);
|
|
677
|
+
// Check if error was handled
|
|
678
|
+
const hasErrorBranch = this.checkErrorBranchUsed(workflow, fault.targetNode, runData);
|
|
679
|
+
const hasContinueOnFail = this.checkContinueOnFailUsed(workflow, fault.targetNode, runData);
|
|
680
|
+
const errorHandled = hasErrorBranch || hasContinueOnFail || retryAttempts > 0;
|
|
681
|
+
// Determine final status
|
|
682
|
+
let finalStatus = 'crashed';
|
|
683
|
+
if (execution.finished && !execution.data?.resultData?.error) {
|
|
684
|
+
finalStatus = errorHandled ? 'recovered' : 'failed-gracefully';
|
|
685
|
+
}
|
|
686
|
+
else if (errorHandled) {
|
|
687
|
+
finalStatus = 'failed-gracefully';
|
|
688
|
+
}
|
|
689
|
+
// Check data integrity
|
|
690
|
+
const dataIntegrity = this.assessDataIntegrity(workflow, runData, fault.targetNode);
|
|
691
|
+
// Track error propagation
|
|
692
|
+
const errorPropagation = this.traceErrorPropagation(workflow, fault.targetNode, runData);
|
|
693
|
+
return {
|
|
694
|
+
fault,
|
|
695
|
+
executed: true,
|
|
696
|
+
errorHandled,
|
|
697
|
+
retryAttempts,
|
|
698
|
+
finalStatus,
|
|
699
|
+
executionTime,
|
|
700
|
+
errorPropagation,
|
|
701
|
+
dataIntegrity,
|
|
702
|
+
details: this.generateFaultTestDetails(fault, errorHandled, retryAttempts, finalStatus),
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Find nodes that had errors in execution
|
|
707
|
+
*/
|
|
708
|
+
findErrorNodes(runData) {
|
|
709
|
+
const errorNodes = [];
|
|
710
|
+
for (const [nodeName, runs] of Object.entries(runData)) {
|
|
711
|
+
const nodeRuns = runs;
|
|
712
|
+
if (nodeRuns.some(run => run.error)) {
|
|
713
|
+
errorNodes.push(nodeName);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return errorNodes;
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Count retry attempts for a node
|
|
720
|
+
*/
|
|
721
|
+
countRetryAttempts(runData, nodeName) {
|
|
722
|
+
const nodeRuns = runData[nodeName];
|
|
723
|
+
if (!nodeRuns)
|
|
724
|
+
return 0;
|
|
725
|
+
// Multiple runs of the same node indicate retries
|
|
726
|
+
return Math.max(0, nodeRuns.length - 1);
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Check if error branch was used
|
|
730
|
+
*/
|
|
731
|
+
checkErrorBranchUsed(workflow, faultNode, runData) {
|
|
732
|
+
const connections = workflow.connections[faultNode];
|
|
733
|
+
if (!connections?.main || connections.main.length < 2)
|
|
734
|
+
return false;
|
|
735
|
+
// Check if error output (index 1) was executed
|
|
736
|
+
const errorOutputConnections = connections.main[1];
|
|
737
|
+
if (!errorOutputConnections)
|
|
738
|
+
return false;
|
|
739
|
+
return errorOutputConnections.some(conn => {
|
|
740
|
+
const connectedNodeRuns = runData[conn.node];
|
|
741
|
+
return connectedNodeRuns && connectedNodeRuns.length > 0;
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Check if continue-on-fail was used
|
|
746
|
+
*/
|
|
747
|
+
checkContinueOnFailUsed(workflow, faultNode, runData) {
|
|
748
|
+
const node = workflow.nodes.find(n => n.name === faultNode);
|
|
749
|
+
if (!node)
|
|
750
|
+
return false;
|
|
751
|
+
const params = node.parameters;
|
|
752
|
+
const hasContinueOnFail = params.continueOnFail === true ||
|
|
753
|
+
(params.options && params.options.continueOnFail === true);
|
|
754
|
+
if (!hasContinueOnFail)
|
|
755
|
+
return false;
|
|
756
|
+
// Check if downstream nodes executed despite error
|
|
757
|
+
const connections = workflow.connections[faultNode];
|
|
758
|
+
if (!connections?.main?.[0])
|
|
759
|
+
return false;
|
|
760
|
+
return connections.main[0].some(conn => {
|
|
761
|
+
const connectedNodeRuns = runData[conn.node];
|
|
762
|
+
return connectedNodeRuns && connectedNodeRuns.length > 0;
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Assess data integrity after fault
|
|
767
|
+
*/
|
|
768
|
+
assessDataIntegrity(workflow, runData, faultNode) {
|
|
769
|
+
// Get downstream nodes
|
|
770
|
+
const downstream = this.getDownstreamNodes(workflow, faultNode);
|
|
771
|
+
// Check if any downstream nodes ran
|
|
772
|
+
const downstreamRan = downstream.filter(node => {
|
|
773
|
+
const runs = runData[node.name];
|
|
774
|
+
return runs && runs.length > 0;
|
|
775
|
+
});
|
|
776
|
+
if (downstreamRan.length === 0) {
|
|
777
|
+
// Error stopped propagation - data preserved (nothing bad happened downstream)
|
|
778
|
+
return 'preserved';
|
|
779
|
+
}
|
|
780
|
+
// Check if downstream received valid data
|
|
781
|
+
let hasValidData = true;
|
|
782
|
+
let hasCorruptedData = false;
|
|
783
|
+
for (const node of downstreamRan) {
|
|
784
|
+
const runs = runData[node.name];
|
|
785
|
+
for (const run of runs) {
|
|
786
|
+
if (run.error) {
|
|
787
|
+
hasCorruptedData = true;
|
|
788
|
+
}
|
|
789
|
+
const outputData = run.data?.main?.[0]?.[0]?.json;
|
|
790
|
+
if (outputData === undefined || outputData === null) {
|
|
791
|
+
hasValidData = false;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
if (hasCorruptedData)
|
|
796
|
+
return 'corrupted';
|
|
797
|
+
if (!hasValidData)
|
|
798
|
+
return 'partial-loss';
|
|
799
|
+
return 'preserved';
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Trace how error propagated through workflow
|
|
803
|
+
*/
|
|
804
|
+
traceErrorPropagation(workflow, faultNode, runData) {
|
|
805
|
+
const propagation = [faultNode];
|
|
806
|
+
const visited = new Set([faultNode]);
|
|
807
|
+
const queue = [faultNode];
|
|
808
|
+
while (queue.length > 0) {
|
|
809
|
+
const current = queue.shift();
|
|
810
|
+
const connections = workflow.connections[current];
|
|
811
|
+
if (!connections?.main)
|
|
812
|
+
continue;
|
|
813
|
+
for (const outputs of connections.main) {
|
|
814
|
+
if (!outputs)
|
|
815
|
+
continue;
|
|
816
|
+
for (const conn of outputs) {
|
|
817
|
+
if (visited.has(conn.node))
|
|
818
|
+
continue;
|
|
819
|
+
visited.add(conn.node);
|
|
820
|
+
// Check if this node was affected by the error
|
|
821
|
+
const runs = runData[conn.node];
|
|
822
|
+
if (runs?.some(r => r.error)) {
|
|
823
|
+
propagation.push(conn.node);
|
|
824
|
+
queue.push(conn.node);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
return propagation;
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Generate human-readable details for fault test
|
|
833
|
+
*/
|
|
834
|
+
generateFaultTestDetails(fault, errorHandled, retryAttempts, finalStatus) {
|
|
835
|
+
const parts = [];
|
|
836
|
+
parts.push(`Injected ${fault.faultType} fault into "${fault.targetNode}".`);
|
|
837
|
+
if (retryAttempts > 0) {
|
|
838
|
+
parts.push(`Node retried ${retryAttempts} time(s).`);
|
|
839
|
+
}
|
|
840
|
+
if (errorHandled) {
|
|
841
|
+
parts.push('Error was handled by workflow.');
|
|
842
|
+
}
|
|
843
|
+
else {
|
|
844
|
+
parts.push('Error was NOT handled - no error branch or retry configured.');
|
|
845
|
+
}
|
|
846
|
+
parts.push(`Final status: ${finalStatus}.`);
|
|
847
|
+
return parts.join(' ');
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Analyze Dead Letter Queue patterns
|
|
851
|
+
*/
|
|
852
|
+
analyzeDLQPatterns(workflow) {
|
|
853
|
+
const dlqNodes = [];
|
|
854
|
+
const unhandledFailurePaths = [];
|
|
855
|
+
const recommendations = [];
|
|
856
|
+
// Look for DLQ-like patterns
|
|
857
|
+
for (const node of workflow.nodes) {
|
|
858
|
+
const nameLower = node.name.toLowerCase();
|
|
859
|
+
const typeLower = node.type.toLowerCase();
|
|
860
|
+
// Common DLQ indicators
|
|
861
|
+
if (nameLower.includes('dlq') ||
|
|
862
|
+
nameLower.includes('dead') ||
|
|
863
|
+
nameLower.includes('failed') ||
|
|
864
|
+
nameLower.includes('error-queue') ||
|
|
865
|
+
nameLower.includes('retry-queue')) {
|
|
866
|
+
dlqNodes.push(node.name);
|
|
867
|
+
}
|
|
868
|
+
// Check for error notification patterns
|
|
869
|
+
if ((typeLower.includes('slack') || typeLower.includes('email') || typeLower.includes('telegram')) &&
|
|
870
|
+
(nameLower.includes('error') || nameLower.includes('alert') || nameLower.includes('notify'))) {
|
|
871
|
+
dlqNodes.push(node.name);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
// Find error-prone nodes without error handling leading to DLQ
|
|
875
|
+
for (const node of workflow.nodes) {
|
|
876
|
+
const isErrorProne = ERROR_PRONE_NODE_TYPES.some(t => node.type.includes(t));
|
|
877
|
+
if (!isErrorProne)
|
|
878
|
+
continue;
|
|
879
|
+
const connections = workflow.connections[node.name];
|
|
880
|
+
const hasErrorBranch = connections?.main && connections.main.length > 1;
|
|
881
|
+
if (!hasErrorBranch) {
|
|
882
|
+
unhandledFailurePaths.push(node.name);
|
|
883
|
+
}
|
|
884
|
+
else {
|
|
885
|
+
// Check if error branch leads to DLQ
|
|
886
|
+
const errorOutputs = connections?.main?.[1] || [];
|
|
887
|
+
const leadsToDLQ = errorOutputs.some(conn => dlqNodes.includes(conn.node) ||
|
|
888
|
+
this.eventuallyLeadsTo(workflow, conn.node, dlqNodes));
|
|
889
|
+
if (!leadsToDLQ) {
|
|
890
|
+
unhandledFailurePaths.push(node.name);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
// Determine poison message handling
|
|
895
|
+
let poisonMessageHandling = 'missing';
|
|
896
|
+
if (dlqNodes.length > 0) {
|
|
897
|
+
poisonMessageHandling = unhandledFailurePaths.length === 0 ? 'present' : 'partial';
|
|
898
|
+
}
|
|
899
|
+
// Generate recommendations
|
|
900
|
+
if (dlqNodes.length === 0) {
|
|
901
|
+
recommendations.push('Add a dead-letter queue pattern for failed message handling');
|
|
902
|
+
recommendations.push('Consider adding error notification (Slack, Email) for critical failures');
|
|
903
|
+
}
|
|
904
|
+
if (unhandledFailurePaths.length > 0) {
|
|
905
|
+
recommendations.push(`Add error branches for: ${unhandledFailurePaths.slice(0, 3).join(', ')}${unhandledFailurePaths.length > 3 ? '...' : ''}`);
|
|
906
|
+
}
|
|
907
|
+
return {
|
|
908
|
+
hasDLQPattern: dlqNodes.length > 0,
|
|
909
|
+
dlqNodes,
|
|
910
|
+
unhandledFailurePaths,
|
|
911
|
+
poisonMessageHandling,
|
|
912
|
+
recommendations,
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Check if a node eventually leads to any of the target nodes
|
|
917
|
+
*/
|
|
918
|
+
eventuallyLeadsTo(workflow, startNode, targetNodes) {
|
|
919
|
+
const visited = new Set();
|
|
920
|
+
const queue = [startNode];
|
|
921
|
+
while (queue.length > 0) {
|
|
922
|
+
const current = queue.shift();
|
|
923
|
+
if (visited.has(current))
|
|
924
|
+
continue;
|
|
925
|
+
visited.add(current);
|
|
926
|
+
if (targetNodes.includes(current)) {
|
|
927
|
+
return true;
|
|
928
|
+
}
|
|
929
|
+
const connections = workflow.connections[current];
|
|
930
|
+
if (!connections?.main)
|
|
931
|
+
continue;
|
|
932
|
+
for (const outputs of connections.main) {
|
|
933
|
+
if (!outputs)
|
|
934
|
+
continue;
|
|
935
|
+
for (const conn of outputs) {
|
|
936
|
+
queue.push(conn.node);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
return false;
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Quick failure mode check
|
|
944
|
+
*/
|
|
945
|
+
async quickCheck(workflowId) {
|
|
946
|
+
const result = await this.testFailureModes(workflowId, { dryRun: true });
|
|
947
|
+
return {
|
|
948
|
+
resilient: result.passed,
|
|
949
|
+
errorHandlingScore: result.errorHandlingScore,
|
|
950
|
+
criticalIssues: result.findings.filter(f => f.severity === 'critical').length,
|
|
951
|
+
topIssue: result.findings[0]?.message || null,
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
exports.N8nFailureModeTesterAgent = N8nFailureModeTesterAgent;
|
|
956
|
+
//# sourceMappingURL=N8nFailureModeTesterAgent.js.map
|