agentic-qe 2.5.5 → 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 +111 -0
- package/README.md +7 -4
- package/dist/adapters/MemoryStoreAdapter.d.ts +75 -123
- package/dist/adapters/MemoryStoreAdapter.d.ts.map +1 -1
- package/dist/adapters/MemoryStoreAdapter.js +204 -219
- package/dist/adapters/MemoryStoreAdapter.js.map +1 -1
- package/dist/agents/AccessibilityAllyAgent.d.ts.map +1 -1
- package/dist/agents/AccessibilityAllyAgent.js +17 -1
- package/dist/agents/AccessibilityAllyAgent.js.map +1 -1
- package/dist/agents/BaseAgent.d.ts +18 -250
- package/dist/agents/BaseAgent.d.ts.map +1 -1
- package/dist/agents/BaseAgent.js +122 -520
- package/dist/agents/BaseAgent.js.map +1 -1
- 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/agents/utils/generators.d.ts +30 -0
- package/dist/agents/utils/generators.d.ts.map +1 -0
- package/dist/agents/utils/generators.js +44 -0
- package/dist/agents/utils/generators.js.map +1 -0
- package/dist/agents/utils/index.d.ts +10 -0
- package/dist/agents/utils/index.d.ts.map +1 -0
- package/dist/agents/utils/index.js +19 -0
- package/dist/agents/utils/index.js.map +1 -0
- package/dist/agents/utils/validation.d.ts +72 -0
- package/dist/agents/utils/validation.d.ts.map +1 -0
- package/dist/agents/utils/validation.js +75 -0
- package/dist/agents/utils/validation.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/core/memory/SwarmMemoryManager.d.ts +114 -90
- package/dist/core/memory/SwarmMemoryManager.d.ts.map +1 -1
- package/dist/core/memory/SwarmMemoryManager.js +277 -235
- package/dist/core/memory/SwarmMemoryManager.js.map +1 -1
- package/dist/learning/baselines/StandardTaskSuite.d.ts.map +1 -1
- package/dist/learning/baselines/StandardTaskSuite.js +38 -0
- package/dist/learning/baselines/StandardTaskSuite.js.map +1 -1
- package/dist/mcp/server-instructions.d.ts +1 -1
- package/dist/mcp/server-instructions.js +1 -1
- package/dist/types/memory-interfaces.d.ts +76 -68
- package/dist/types/memory-interfaces.d.ts.map +1 -1
- package/dist/types/memory-interfaces.js +3 -0
- package/dist/types/memory-interfaces.js.map +1 -1
- package/docs/reference/agents.md +91 -2
- package/docs/reference/skills.md +97 -2
- package/package.json +2 -2
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* N8nChaosTesterAgent
|
|
4
|
+
*
|
|
5
|
+
* REAL Chaos engineering for n8n workflows:
|
|
6
|
+
* - ACTUAL fault injection via modified workflow copies
|
|
7
|
+
* - REAL workflow execution to test resilience
|
|
8
|
+
* - ACTUAL recovery testing with live execution
|
|
9
|
+
* - TRUE failure scenario validation
|
|
10
|
+
*
|
|
11
|
+
* This agent creates fault-injected workflow copies, executes them against
|
|
12
|
+
* the n8n instance, and measures real behavior - NOT just static analysis.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.N8nChaosTesterAgent = void 0;
|
|
16
|
+
const N8nBaseAgent_1 = require("./N8nBaseAgent");
|
|
17
|
+
const N8nTestHarness_1 = require("./N8nTestHarness");
|
|
18
|
+
// Pre-defined chaos experiments with REAL fault injection
|
|
19
|
+
const STANDARD_EXPERIMENTS = [
|
|
20
|
+
{
|
|
21
|
+
name: 'HTTP 500 Error Injection',
|
|
22
|
+
type: 'node-failure',
|
|
23
|
+
target: 'httpRequest',
|
|
24
|
+
parameters: { errorCode: 500, errorMessage: 'Internal Server Error' },
|
|
25
|
+
expectedBehavior: 'Workflow handles 500 error gracefully with retry or error branch',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'Network Timeout',
|
|
29
|
+
type: 'timeout',
|
|
30
|
+
target: 'httpRequest',
|
|
31
|
+
parameters: { timeoutMs: 30000 },
|
|
32
|
+
expectedBehavior: 'Workflow times out gracefully and triggers error handling',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'Empty Response',
|
|
36
|
+
type: 'invalid-response',
|
|
37
|
+
target: 'httpRequest',
|
|
38
|
+
parameters: { responseType: 'empty', response: {} },
|
|
39
|
+
expectedBehavior: 'Workflow handles empty/null responses without crashing',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'Malformed JSON Response',
|
|
43
|
+
type: 'data-corruption',
|
|
44
|
+
target: 'httpRequest',
|
|
45
|
+
parameters: { responseType: 'malformed', response: 'not-json' },
|
|
46
|
+
expectedBehavior: 'Workflow validates data and handles parse errors',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'Rate Limit (429)',
|
|
50
|
+
type: 'rate-limit',
|
|
51
|
+
target: 'httpRequest',
|
|
52
|
+
parameters: { errorCode: 429, retryAfter: 60 },
|
|
53
|
+
expectedBehavior: 'Workflow implements retry with backoff or handles rate limit',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'Authentication Failure (401)',
|
|
57
|
+
type: 'credential-failure',
|
|
58
|
+
target: 'httpRequest',
|
|
59
|
+
parameters: { errorCode: 401, errorMessage: 'Unauthorized' },
|
|
60
|
+
expectedBehavior: 'Workflow reports authentication failure clearly',
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
class N8nChaosTesterAgent extends N8nBaseAgent_1.N8nBaseAgent {
|
|
64
|
+
constructor(config) {
|
|
65
|
+
const capabilities = [
|
|
66
|
+
{
|
|
67
|
+
name: 'real-fault-injection',
|
|
68
|
+
version: '2.0.0',
|
|
69
|
+
description: 'Inject REAL faults into workflow execution via modified copies',
|
|
70
|
+
parameters: {},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'live-resilience-testing',
|
|
74
|
+
version: '2.0.0',
|
|
75
|
+
description: 'Test workflow resilience with ACTUAL execution',
|
|
76
|
+
parameters: {},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'recovery-validation',
|
|
80
|
+
version: '2.0.0',
|
|
81
|
+
description: 'Validate recovery mechanisms with live retry testing',
|
|
82
|
+
parameters: {},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'chaos-orchestration',
|
|
86
|
+
version: '2.0.0',
|
|
87
|
+
description: 'Orchestrate comprehensive chaos experiments',
|
|
88
|
+
parameters: {},
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
super({
|
|
92
|
+
...config,
|
|
93
|
+
type: 'n8n-chaos-tester',
|
|
94
|
+
capabilities: [...capabilities, ...(config.capabilities || [])],
|
|
95
|
+
});
|
|
96
|
+
this.experimentHistory = new Map();
|
|
97
|
+
this.testHarness = null;
|
|
98
|
+
}
|
|
99
|
+
async performTask(task) {
|
|
100
|
+
const chaosTask = task;
|
|
101
|
+
if (chaosTask.type !== 'chaos-test') {
|
|
102
|
+
throw new Error(`Unsupported task type: ${chaosTask.type}`);
|
|
103
|
+
}
|
|
104
|
+
return this.runChaosTests(chaosTask.target, chaosTask.options);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Run REAL chaos tests on workflow with actual fault injection and execution
|
|
108
|
+
*/
|
|
109
|
+
async runChaosTests(workflowId, options) {
|
|
110
|
+
const workflow = await this.getWorkflow(workflowId);
|
|
111
|
+
const startTime = Date.now();
|
|
112
|
+
// Initialize test harness for real fault injection
|
|
113
|
+
this.testHarness = new N8nTestHarness_1.N8nTestHarness(this.n8nConfig);
|
|
114
|
+
try {
|
|
115
|
+
// Get experiments to run
|
|
116
|
+
const experiments = options?.experiments ||
|
|
117
|
+
this.selectExperiments(workflow, options?.intensity || 'moderate');
|
|
118
|
+
// Run experiments with REAL fault injection
|
|
119
|
+
const experimentResults = [];
|
|
120
|
+
let actualExecutions = 0;
|
|
121
|
+
for (const experiment of experiments) {
|
|
122
|
+
if (options?.safeMode && this.isDestructive(experiment)) {
|
|
123
|
+
experimentResults.push(this.createSkippedResult(experiment, 'Safe mode enabled'));
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const result = await this.runRealExperiment(workflow, experiment, options?.testInput || {});
|
|
127
|
+
experimentResults.push(result);
|
|
128
|
+
if (result.execution) {
|
|
129
|
+
actualExecutions++;
|
|
130
|
+
}
|
|
131
|
+
// Store for history
|
|
132
|
+
const history = this.experimentHistory.get(workflowId) || [];
|
|
133
|
+
history.push(result);
|
|
134
|
+
this.experimentHistory.set(workflowId, history);
|
|
135
|
+
}
|
|
136
|
+
// Calculate resilience score based on REAL execution results
|
|
137
|
+
const resilience = this.calculateResilienceScore(workflow, experimentResults);
|
|
138
|
+
// Identify vulnerabilities from ACTUAL test failures
|
|
139
|
+
const vulnerabilities = this.identifyVulnerabilities(experimentResults);
|
|
140
|
+
// Generate recommendations based on REAL test data
|
|
141
|
+
const recommendations = this.generateRecommendations(workflow, experimentResults, vulnerabilities);
|
|
142
|
+
// Create summary
|
|
143
|
+
const summary = {
|
|
144
|
+
totalExperiments: experimentResults.length,
|
|
145
|
+
passed: experimentResults.filter(r => r.status === 'passed').length,
|
|
146
|
+
failed: experimentResults.filter(r => r.status === 'failed').length,
|
|
147
|
+
errors: experimentResults.filter(r => r.status === 'error').length,
|
|
148
|
+
skipped: experimentResults.filter(r => r.status === 'skipped').length,
|
|
149
|
+
duration: Date.now() - startTime,
|
|
150
|
+
vulnerabilitiesFound: vulnerabilities.length,
|
|
151
|
+
actualExecutions,
|
|
152
|
+
};
|
|
153
|
+
const result = {
|
|
154
|
+
workflowId,
|
|
155
|
+
experiments: experimentResults,
|
|
156
|
+
resilience,
|
|
157
|
+
vulnerabilities,
|
|
158
|
+
recommendations,
|
|
159
|
+
summary,
|
|
160
|
+
};
|
|
161
|
+
// Store result
|
|
162
|
+
await this.storeTestResult(`chaos-test:${workflowId}`, result);
|
|
163
|
+
// Emit event
|
|
164
|
+
this.emitEvent('chaos.test.completed', {
|
|
165
|
+
workflowId,
|
|
166
|
+
passed: summary.passed,
|
|
167
|
+
failed: summary.failed,
|
|
168
|
+
resilienceScore: resilience.overall,
|
|
169
|
+
vulnerabilities: vulnerabilities.length,
|
|
170
|
+
actualExecutions,
|
|
171
|
+
});
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
// Always cleanup test workflows
|
|
176
|
+
if (this.testHarness) {
|
|
177
|
+
await this.testHarness.cleanup();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Run a REAL experiment with actual fault injection and workflow execution
|
|
183
|
+
*/
|
|
184
|
+
async runRealExperiment(workflow, experiment, testInput) {
|
|
185
|
+
const startTime = Date.now();
|
|
186
|
+
const observations = [];
|
|
187
|
+
observations.push({
|
|
188
|
+
timestamp: new Date(),
|
|
189
|
+
type: 'info',
|
|
190
|
+
message: `Starting REAL experiment: ${experiment.name}`,
|
|
191
|
+
details: { experimentType: experiment.type, target: experiment.target },
|
|
192
|
+
});
|
|
193
|
+
try {
|
|
194
|
+
// Select target node
|
|
195
|
+
const targetNode = this.selectTargetNode(workflow, experiment.target);
|
|
196
|
+
if (!targetNode) {
|
|
197
|
+
observations.push({
|
|
198
|
+
timestamp: new Date(),
|
|
199
|
+
type: 'warning',
|
|
200
|
+
message: `No suitable target node found for experiment: ${experiment.name}`,
|
|
201
|
+
});
|
|
202
|
+
return this.createSkippedResult(experiment, 'No suitable target node');
|
|
203
|
+
}
|
|
204
|
+
// Convert experiment to fault injection config
|
|
205
|
+
const faultConfig = this.experimentToFaultConfig(experiment, targetNode);
|
|
206
|
+
observations.push({
|
|
207
|
+
timestamp: new Date(),
|
|
208
|
+
type: 'info',
|
|
209
|
+
message: `Creating fault-injected workflow copy targeting node: ${targetNode.name}`,
|
|
210
|
+
details: { faultConfig: JSON.parse(JSON.stringify(faultConfig)) },
|
|
211
|
+
});
|
|
212
|
+
// Create fault-injected workflow via test harness
|
|
213
|
+
const { workflow: testWorkflow, cleanup } = await this.testHarness.createFaultInjectedWorkflow(workflow.id, [faultConfig]);
|
|
214
|
+
observations.push({
|
|
215
|
+
timestamp: new Date(),
|
|
216
|
+
type: 'execution',
|
|
217
|
+
message: `Created test workflow: ${testWorkflow.id}`,
|
|
218
|
+
details: { testWorkflowId: testWorkflow.id, testWorkflowName: testWorkflow.name },
|
|
219
|
+
});
|
|
220
|
+
try {
|
|
221
|
+
// Execute the fault-injected workflow
|
|
222
|
+
observations.push({
|
|
223
|
+
timestamp: new Date(),
|
|
224
|
+
type: 'execution',
|
|
225
|
+
message: 'Executing fault-injected workflow...',
|
|
226
|
+
});
|
|
227
|
+
const execution = await this.executeWorkflow(testWorkflow.id, testInput);
|
|
228
|
+
const executionResult = await this.waitForExecutionWithTimeout(execution.id, 60000);
|
|
229
|
+
observations.push({
|
|
230
|
+
timestamp: new Date(),
|
|
231
|
+
type: 'execution',
|
|
232
|
+
message: `Execution completed: ${executionResult.status}`,
|
|
233
|
+
details: {
|
|
234
|
+
executionId: executionResult.id,
|
|
235
|
+
status: executionResult.status,
|
|
236
|
+
finished: executionResult.finished,
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
// Analyze the REAL execution result
|
|
240
|
+
const impact = this.assessRealImpact(workflow, executionResult, experiment);
|
|
241
|
+
const recovery = await this.assessRealRecovery(workflow, executionResult, experiment);
|
|
242
|
+
// Determine status based on ACTUAL behavior
|
|
243
|
+
const status = this.determineRealStatus(experiment, executionResult, impact, recovery);
|
|
244
|
+
// Extract executed nodes
|
|
245
|
+
const nodesExecuted = this.extractExecutedNodes(executionResult);
|
|
246
|
+
const errorHandlingTriggered = this.checkErrorHandlingTriggered(executionResult, workflow);
|
|
247
|
+
return {
|
|
248
|
+
experiment,
|
|
249
|
+
status,
|
|
250
|
+
duration: Date.now() - startTime,
|
|
251
|
+
observations,
|
|
252
|
+
impact,
|
|
253
|
+
recovery,
|
|
254
|
+
execution: {
|
|
255
|
+
executionId: executionResult.id,
|
|
256
|
+
workflowStatus: executionResult.status,
|
|
257
|
+
errorMessage: this.extractErrorMessage(executionResult),
|
|
258
|
+
nodesExecuted,
|
|
259
|
+
errorHandlingTriggered,
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
finally {
|
|
264
|
+
// Always cleanup the test workflow
|
|
265
|
+
await cleanup();
|
|
266
|
+
observations.push({
|
|
267
|
+
timestamp: new Date(),
|
|
268
|
+
type: 'info',
|
|
269
|
+
message: 'Test workflow cleaned up',
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
observations.push({
|
|
275
|
+
timestamp: new Date(),
|
|
276
|
+
type: 'error',
|
|
277
|
+
message: error instanceof Error ? error.message : String(error),
|
|
278
|
+
});
|
|
279
|
+
return {
|
|
280
|
+
experiment,
|
|
281
|
+
status: 'error',
|
|
282
|
+
duration: Date.now() - startTime,
|
|
283
|
+
observations,
|
|
284
|
+
impact: {
|
|
285
|
+
severity: 'critical',
|
|
286
|
+
affectedNodes: [],
|
|
287
|
+
dataLoss: false,
|
|
288
|
+
serviceDisruption: true,
|
|
289
|
+
cascadeFailure: false,
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Convert experiment to fault injection config for TestHarness
|
|
296
|
+
*/
|
|
297
|
+
experimentToFaultConfig(experiment, targetNode) {
|
|
298
|
+
const params = experiment.parameters;
|
|
299
|
+
switch (experiment.type) {
|
|
300
|
+
case 'node-failure':
|
|
301
|
+
return {
|
|
302
|
+
targetNode: targetNode.name,
|
|
303
|
+
faultType: 'error',
|
|
304
|
+
errorCode: params.errorCode || 500,
|
|
305
|
+
errorMessage: params.errorMessage || 'Injected fault: node failure',
|
|
306
|
+
};
|
|
307
|
+
case 'timeout':
|
|
308
|
+
return {
|
|
309
|
+
targetNode: targetNode.name,
|
|
310
|
+
faultType: 'timeout',
|
|
311
|
+
delay: params.timeoutMs || 30000,
|
|
312
|
+
};
|
|
313
|
+
case 'invalid-response':
|
|
314
|
+
case 'data-corruption':
|
|
315
|
+
return {
|
|
316
|
+
targetNode: targetNode.name,
|
|
317
|
+
faultType: 'empty-response',
|
|
318
|
+
};
|
|
319
|
+
case 'rate-limit':
|
|
320
|
+
return {
|
|
321
|
+
targetNode: targetNode.name,
|
|
322
|
+
faultType: 'error',
|
|
323
|
+
errorCode: 429,
|
|
324
|
+
errorMessage: 'Rate limit exceeded',
|
|
325
|
+
};
|
|
326
|
+
case 'credential-failure':
|
|
327
|
+
return {
|
|
328
|
+
targetNode: targetNode.name,
|
|
329
|
+
faultType: 'error',
|
|
330
|
+
errorCode: 401,
|
|
331
|
+
errorMessage: 'Authentication failed',
|
|
332
|
+
};
|
|
333
|
+
case 'network-delay':
|
|
334
|
+
return {
|
|
335
|
+
targetNode: targetNode.name,
|
|
336
|
+
faultType: 'timeout',
|
|
337
|
+
delay: params.delayMs || 5000,
|
|
338
|
+
};
|
|
339
|
+
case 'network-partition':
|
|
340
|
+
return {
|
|
341
|
+
targetNode: targetNode.name,
|
|
342
|
+
faultType: 'error',
|
|
343
|
+
errorCode: 503,
|
|
344
|
+
errorMessage: 'Service unavailable - network partition',
|
|
345
|
+
};
|
|
346
|
+
case 'resource-exhaustion':
|
|
347
|
+
return {
|
|
348
|
+
targetNode: targetNode.name,
|
|
349
|
+
faultType: 'error',
|
|
350
|
+
errorCode: 503,
|
|
351
|
+
errorMessage: 'Resource exhaustion',
|
|
352
|
+
};
|
|
353
|
+
default:
|
|
354
|
+
return {
|
|
355
|
+
targetNode: targetNode.name,
|
|
356
|
+
faultType: 'error',
|
|
357
|
+
errorCode: 500,
|
|
358
|
+
errorMessage: `Injected fault: ${experiment.type}`,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Wait for execution with timeout
|
|
364
|
+
*/
|
|
365
|
+
async waitForExecutionWithTimeout(executionId, timeout) {
|
|
366
|
+
const startTime = Date.now();
|
|
367
|
+
while (Date.now() - startTime < timeout) {
|
|
368
|
+
const execution = await this.n8nClient.getExecution(executionId);
|
|
369
|
+
if (execution.finished) {
|
|
370
|
+
return execution;
|
|
371
|
+
}
|
|
372
|
+
// Poll every 500ms
|
|
373
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
374
|
+
}
|
|
375
|
+
// Return the current state even if not finished
|
|
376
|
+
return this.n8nClient.getExecution(executionId);
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Assess REAL impact from actual execution
|
|
380
|
+
*/
|
|
381
|
+
assessRealImpact(workflow, execution, experiment) {
|
|
382
|
+
const failedNodes = this.extractFailedNodes(execution);
|
|
383
|
+
const executedNodes = this.extractExecutedNodes(execution);
|
|
384
|
+
// Determine severity based on actual execution outcome
|
|
385
|
+
let severity;
|
|
386
|
+
if (execution.status === 'success') {
|
|
387
|
+
severity = 'none';
|
|
388
|
+
}
|
|
389
|
+
else if (execution.status === 'failed' || execution.status === 'crashed') {
|
|
390
|
+
if (failedNodes.length === 0) {
|
|
391
|
+
severity = 'minor';
|
|
392
|
+
}
|
|
393
|
+
else if (failedNodes.length === 1) {
|
|
394
|
+
severity = 'moderate';
|
|
395
|
+
}
|
|
396
|
+
else if (failedNodes.length > workflow.nodes.length / 2) {
|
|
397
|
+
severity = 'critical';
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
severity = 'severe';
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
severity = 'moderate';
|
|
405
|
+
}
|
|
406
|
+
// Check for cascade failure - did error propagate beyond target?
|
|
407
|
+
const cascadeFailure = failedNodes.length > 1;
|
|
408
|
+
return {
|
|
409
|
+
severity,
|
|
410
|
+
affectedNodes: failedNodes,
|
|
411
|
+
dataLoss: false, // Would need to check output data
|
|
412
|
+
serviceDisruption: execution.status === 'failed' || execution.status === 'crashed',
|
|
413
|
+
cascadeFailure,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Assess REAL recovery from actual execution
|
|
418
|
+
*/
|
|
419
|
+
async assessRealRecovery(workflow, execution, experiment) {
|
|
420
|
+
const startTime = Date.now();
|
|
421
|
+
// Check if error handling was triggered
|
|
422
|
+
const errorHandlingTriggered = this.checkErrorHandlingTriggered(execution, workflow);
|
|
423
|
+
// Check if retry was attempted (look for multiple executions of same node)
|
|
424
|
+
const retryAttempted = this.checkRetryAttempted(execution);
|
|
425
|
+
// Check if workflow completed despite the fault
|
|
426
|
+
const recovered = execution.status === 'success' || errorHandlingTriggered;
|
|
427
|
+
return {
|
|
428
|
+
recovered,
|
|
429
|
+
timeToRecovery: Date.now() - startTime,
|
|
430
|
+
manualIntervention: !recovered,
|
|
431
|
+
dataIntegrity: execution.status === 'success',
|
|
432
|
+
retrySucceeded: retryAttempted && execution.status === 'success',
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Determine status based on REAL execution results
|
|
437
|
+
*/
|
|
438
|
+
determineRealStatus(experiment, execution, impact, recovery) {
|
|
439
|
+
// If workflow succeeded despite fault, that's a PASS
|
|
440
|
+
if (execution.status === 'success') {
|
|
441
|
+
return 'passed';
|
|
442
|
+
}
|
|
443
|
+
// If error was handled gracefully (error handling triggered), that's a PASS
|
|
444
|
+
if (recovery.recovered) {
|
|
445
|
+
return 'passed';
|
|
446
|
+
}
|
|
447
|
+
// If we expected the workflow to fail and it did, check if it failed gracefully
|
|
448
|
+
if (impact.severity === 'critical' || impact.cascadeFailure) {
|
|
449
|
+
return 'failed';
|
|
450
|
+
}
|
|
451
|
+
// Default to failed if error wasn't handled
|
|
452
|
+
return 'failed';
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Extract list of nodes that executed
|
|
456
|
+
*/
|
|
457
|
+
extractExecutedNodes(execution) {
|
|
458
|
+
if (!execution.data?.resultData?.runData) {
|
|
459
|
+
return [];
|
|
460
|
+
}
|
|
461
|
+
return Object.keys(execution.data.resultData.runData);
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Extract list of nodes that failed
|
|
465
|
+
*/
|
|
466
|
+
extractFailedNodes(execution) {
|
|
467
|
+
if (!execution.data?.resultData?.runData) {
|
|
468
|
+
return [];
|
|
469
|
+
}
|
|
470
|
+
const failedNodes = [];
|
|
471
|
+
const runData = execution.data.resultData.runData;
|
|
472
|
+
for (const [nodeName, nodeRuns] of Object.entries(runData)) {
|
|
473
|
+
if (Array.isArray(nodeRuns)) {
|
|
474
|
+
for (const run of nodeRuns) {
|
|
475
|
+
if (run.error) {
|
|
476
|
+
failedNodes.push(nodeName);
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return failedNodes;
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Extract error message from execution
|
|
486
|
+
*/
|
|
487
|
+
extractErrorMessage(execution) {
|
|
488
|
+
if (!execution.data?.resultData?.error) {
|
|
489
|
+
return undefined;
|
|
490
|
+
}
|
|
491
|
+
const error = execution.data.resultData.error;
|
|
492
|
+
return typeof error === 'string' ? error : error.message;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Check if error handling was triggered
|
|
496
|
+
*/
|
|
497
|
+
checkErrorHandlingTriggered(execution, workflow) {
|
|
498
|
+
const executedNodes = this.extractExecutedNodes(execution);
|
|
499
|
+
// Check if error workflow was executed
|
|
500
|
+
if (workflow.settings?.errorWorkflow) {
|
|
501
|
+
// Would need to check if error workflow was triggered
|
|
502
|
+
return execution.status === 'failed' || execution.status === 'crashed';
|
|
503
|
+
}
|
|
504
|
+
// Check if any error handling nodes were executed
|
|
505
|
+
const errorHandlingNodes = workflow.nodes.filter(n => n.type.includes('errorTrigger') ||
|
|
506
|
+
n.type.includes('Error') ||
|
|
507
|
+
(n.parameters.onError && n.parameters.onError !== 'stopWorkflow'));
|
|
508
|
+
return errorHandlingNodes.some(n => executedNodes.includes(n.name));
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Check if retry was attempted
|
|
512
|
+
*/
|
|
513
|
+
checkRetryAttempted(execution) {
|
|
514
|
+
if (!execution.data?.resultData?.runData) {
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
// Check if any node was executed multiple times
|
|
518
|
+
for (const nodeRuns of Object.values(execution.data.resultData.runData)) {
|
|
519
|
+
if (Array.isArray(nodeRuns) && nodeRuns.length > 1) {
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Select experiments based on workflow and intensity
|
|
527
|
+
*/
|
|
528
|
+
selectExperiments(workflow, intensity) {
|
|
529
|
+
const experiments = [];
|
|
530
|
+
// Add relevant standard experiments
|
|
531
|
+
for (const experiment of STANDARD_EXPERIMENTS) {
|
|
532
|
+
if (this.isExperimentApplicable(workflow, experiment)) {
|
|
533
|
+
experiments.push(experiment);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
// Adjust based on intensity
|
|
537
|
+
const countMap = { light: 2, moderate: 4, heavy: experiments.length };
|
|
538
|
+
return experiments.slice(0, countMap[intensity]);
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Check if experiment is applicable to workflow
|
|
542
|
+
*/
|
|
543
|
+
isExperimentApplicable(workflow, experiment) {
|
|
544
|
+
switch (experiment.target) {
|
|
545
|
+
case 'httpRequest':
|
|
546
|
+
return workflow.nodes.some(n => n.type.includes('httpRequest'));
|
|
547
|
+
case 'code':
|
|
548
|
+
return workflow.nodes.some(n => n.type.includes('code'));
|
|
549
|
+
case 'random':
|
|
550
|
+
return workflow.nodes.filter(n => !n.type.includes('trigger')).length > 0;
|
|
551
|
+
default:
|
|
552
|
+
return workflow.nodes.some(n => n.name === experiment.target);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Check if experiment is destructive
|
|
557
|
+
*/
|
|
558
|
+
isDestructive(experiment) {
|
|
559
|
+
const destructiveTypes = [
|
|
560
|
+
'data-corruption',
|
|
561
|
+
'resource-exhaustion',
|
|
562
|
+
];
|
|
563
|
+
return destructiveTypes.includes(experiment.type);
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Select target node for chaos
|
|
567
|
+
*/
|
|
568
|
+
selectTargetNode(workflow, target) {
|
|
569
|
+
if (!target || target === 'random') {
|
|
570
|
+
const nonTriggerNodes = workflow.nodes.filter(n => !n.type.includes('trigger') && !n.type.includes('Trigger'));
|
|
571
|
+
return nonTriggerNodes[Math.floor(Math.random() * nonTriggerNodes.length)];
|
|
572
|
+
}
|
|
573
|
+
if (target.includes('Request') || target.includes('http') || target === 'httpRequest') {
|
|
574
|
+
return workflow.nodes.find(n => n.type.includes('httpRequest'));
|
|
575
|
+
}
|
|
576
|
+
if (target === 'code') {
|
|
577
|
+
return workflow.nodes.find(n => n.type.includes('code'));
|
|
578
|
+
}
|
|
579
|
+
return workflow.nodes.find(n => n.name === target || n.id === target);
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Calculate resilience score based on REAL test results
|
|
583
|
+
*/
|
|
584
|
+
calculateResilienceScore(workflow, results) {
|
|
585
|
+
// Calculate based on ACTUAL execution results
|
|
586
|
+
const passedExperiments = results.filter(r => r.status === 'passed');
|
|
587
|
+
const executedExperiments = results.filter(r => r.execution);
|
|
588
|
+
// Fault tolerance: Did the workflow handle faults gracefully?
|
|
589
|
+
const faultTolerance = executedExperiments.length > 0
|
|
590
|
+
? (passedExperiments.length / executedExperiments.length) * 100
|
|
591
|
+
: 50;
|
|
592
|
+
// Recovery: Did error handling trigger when needed?
|
|
593
|
+
const recoveryResults = results.filter(r => r.recovery);
|
|
594
|
+
const recovery = recoveryResults.length > 0
|
|
595
|
+
? (recoveryResults.filter(r => r.recovery?.recovered).length / recoveryResults.length) * 100
|
|
596
|
+
: 50;
|
|
597
|
+
// Graceful degradation: Did failures cascade?
|
|
598
|
+
const cascadeFailures = results.filter(r => r.impact.cascadeFailure).length;
|
|
599
|
+
const gracefulDegradation = executedExperiments.length > 0
|
|
600
|
+
? Math.max(0, 100 - (cascadeFailures / executedExperiments.length) * 100)
|
|
601
|
+
: 50;
|
|
602
|
+
// Monitoring: Check workflow configuration
|
|
603
|
+
const monitoring = this.calculateMonitoringScore(workflow);
|
|
604
|
+
const overall = (faultTolerance + recovery + gracefulDegradation + monitoring) / 4;
|
|
605
|
+
let grade;
|
|
606
|
+
if (overall >= 90)
|
|
607
|
+
grade = 'A';
|
|
608
|
+
else if (overall >= 80)
|
|
609
|
+
grade = 'B';
|
|
610
|
+
else if (overall >= 70)
|
|
611
|
+
grade = 'C';
|
|
612
|
+
else if (overall >= 60)
|
|
613
|
+
grade = 'D';
|
|
614
|
+
else
|
|
615
|
+
grade = 'F';
|
|
616
|
+
return {
|
|
617
|
+
overall: Math.round(overall),
|
|
618
|
+
categories: {
|
|
619
|
+
faultTolerance: Math.round(faultTolerance),
|
|
620
|
+
recovery: Math.round(recovery),
|
|
621
|
+
gracefulDegradation: Math.round(gracefulDegradation),
|
|
622
|
+
monitoring: Math.round(monitoring),
|
|
623
|
+
},
|
|
624
|
+
grade,
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
calculateMonitoringScore(workflow) {
|
|
628
|
+
let score = 50;
|
|
629
|
+
if (workflow.settings?.saveExecutionProgress)
|
|
630
|
+
score += 25;
|
|
631
|
+
if (workflow.settings?.errorWorkflow)
|
|
632
|
+
score += 15;
|
|
633
|
+
if (workflow.settings?.saveDataErrorExecution !== 'none')
|
|
634
|
+
score += 10;
|
|
635
|
+
return Math.min(100, score);
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Identify vulnerabilities from ACTUAL test failures
|
|
639
|
+
*/
|
|
640
|
+
identifyVulnerabilities(results) {
|
|
641
|
+
const vulnerabilities = [];
|
|
642
|
+
for (const result of results) {
|
|
643
|
+
if (result.status === 'failed' && result.execution) {
|
|
644
|
+
vulnerabilities.push({
|
|
645
|
+
id: `vuln-${result.experiment.type}-${Date.now()}`,
|
|
646
|
+
type: result.experiment.type,
|
|
647
|
+
severity: result.impact.severity === 'critical' ? 'critical' :
|
|
648
|
+
result.impact.severity === 'severe' ? 'high' : 'medium',
|
|
649
|
+
description: `Workflow failed to handle ${result.experiment.name}: ${result.execution.errorMessage || 'No error handling triggered'}`,
|
|
650
|
+
location: result.impact.affectedNodes.join(', ') || 'workflow',
|
|
651
|
+
exploitedBy: result.experiment.type,
|
|
652
|
+
mitigation: result.experiment.expectedBehavior,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return vulnerabilities;
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Generate recommendations based on REAL test data
|
|
660
|
+
*/
|
|
661
|
+
generateRecommendations(workflow, results, vulnerabilities) {
|
|
662
|
+
const recommendations = [];
|
|
663
|
+
// Based on ACTUAL failed experiments
|
|
664
|
+
for (const result of results.filter(r => r.status === 'failed')) {
|
|
665
|
+
recommendations.push({
|
|
666
|
+
priority: result.impact.severity === 'critical' ? 'critical' : 'high',
|
|
667
|
+
category: result.experiment.type,
|
|
668
|
+
issue: `Failed ${result.experiment.name} - ${result.execution?.errorMessage || 'Unhandled error'}`,
|
|
669
|
+
recommendation: result.experiment.expectedBehavior,
|
|
670
|
+
effort: 'medium',
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
// Check for missing error handling
|
|
674
|
+
if (!workflow.settings?.errorWorkflow) {
|
|
675
|
+
const failedCount = results.filter(r => r.status === 'failed').length;
|
|
676
|
+
if (failedCount > 0) {
|
|
677
|
+
recommendations.push({
|
|
678
|
+
priority: 'critical',
|
|
679
|
+
category: 'error-handling',
|
|
680
|
+
issue: `No error workflow configured - ${failedCount} experiments failed`,
|
|
681
|
+
recommendation: 'Configure an error workflow to catch and handle failures',
|
|
682
|
+
effort: 'low',
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
// Check for cascade failures
|
|
687
|
+
const cascadeFailures = results.filter(r => r.impact.cascadeFailure);
|
|
688
|
+
if (cascadeFailures.length > 0) {
|
|
689
|
+
recommendations.push({
|
|
690
|
+
priority: 'high',
|
|
691
|
+
category: 'isolation',
|
|
692
|
+
issue: `${cascadeFailures.length} experiments caused cascade failures`,
|
|
693
|
+
recommendation: 'Add error isolation with continueOnFail or error branches to prevent cascade',
|
|
694
|
+
effort: 'medium',
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
return recommendations;
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Create skipped result
|
|
701
|
+
*/
|
|
702
|
+
createSkippedResult(experiment, reason) {
|
|
703
|
+
return {
|
|
704
|
+
experiment,
|
|
705
|
+
status: 'skipped',
|
|
706
|
+
duration: 0,
|
|
707
|
+
observations: [{
|
|
708
|
+
timestamp: new Date(),
|
|
709
|
+
type: 'info',
|
|
710
|
+
message: `Skipped: ${reason}`,
|
|
711
|
+
}],
|
|
712
|
+
impact: {
|
|
713
|
+
severity: 'none',
|
|
714
|
+
affectedNodes: [],
|
|
715
|
+
dataLoss: false,
|
|
716
|
+
serviceDisruption: false,
|
|
717
|
+
cascadeFailure: false,
|
|
718
|
+
},
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Get experiment history
|
|
723
|
+
*/
|
|
724
|
+
getExperimentHistory(workflowId) {
|
|
725
|
+
return this.experimentHistory.get(workflowId) || [];
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
exports.N8nChaosTesterAgent = N8nChaosTesterAgent;
|
|
729
|
+
//# sourceMappingURL=N8nChaosTesterAgent.js.map
|