agentic-qe 2.5.6 → 2.5.8
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 +127 -0
- package/README.md +7 -4
- package/dist/agents/BaseAgent.d.ts +142 -0
- package/dist/agents/BaseAgent.d.ts.map +1 -1
- package/dist/agents/BaseAgent.js +372 -2
- package/dist/agents/BaseAgent.js.map +1 -1
- package/dist/agents/TestGeneratorAgent.d.ts +5 -0
- package/dist/agents/TestGeneratorAgent.d.ts.map +1 -1
- package/dist/agents/TestGeneratorAgent.js +38 -0
- package/dist/agents/TestGeneratorAgent.js.map +1 -1
- package/dist/agents/index.d.ts +1 -1
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.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/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/RuVectorPatternStore.d.ts +90 -0
- package/dist/core/memory/RuVectorPatternStore.d.ts.map +1 -1
- package/dist/core/memory/RuVectorPatternStore.js +209 -0
- package/dist/core/memory/RuVectorPatternStore.js.map +1 -1
- package/dist/learning/FederatedManager.d.ts +232 -0
- package/dist/learning/FederatedManager.d.ts.map +1 -0
- package/dist/learning/FederatedManager.js +489 -0
- package/dist/learning/FederatedManager.js.map +1 -0
- package/dist/learning/HNSWPatternAdapter.d.ts +117 -0
- package/dist/learning/HNSWPatternAdapter.d.ts.map +1 -0
- package/dist/learning/HNSWPatternAdapter.js +262 -0
- package/dist/learning/HNSWPatternAdapter.js.map +1 -0
- package/dist/learning/LearningEngine.d.ts +27 -0
- package/dist/learning/LearningEngine.d.ts.map +1 -1
- package/dist/learning/LearningEngine.js +75 -1
- package/dist/learning/LearningEngine.js.map +1 -1
- package/dist/learning/PatternCurator.d.ts +217 -0
- package/dist/learning/PatternCurator.d.ts.map +1 -0
- package/dist/learning/PatternCurator.js +393 -0
- package/dist/learning/PatternCurator.js.map +1 -0
- package/dist/learning/index.d.ts +6 -0
- package/dist/learning/index.d.ts.map +1 -1
- package/dist/learning/index.js +16 -1
- package/dist/learning/index.js.map +1 -1
- package/dist/learning/types.d.ts +4 -0
- package/dist/learning/types.d.ts.map +1 -1
- package/dist/mcp/server-instructions.d.ts +1 -1
- package/dist/mcp/server-instructions.js +1 -1
- package/dist/memory/HNSWPatternStore.d.ts +176 -0
- package/dist/memory/HNSWPatternStore.d.ts.map +1 -0
- package/dist/memory/HNSWPatternStore.js +392 -0
- package/dist/memory/HNSWPatternStore.js.map +1 -0
- package/dist/memory/index.d.ts +8 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +13 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/providers/HybridRouter.d.ts +85 -4
- package/dist/providers/HybridRouter.d.ts.map +1 -1
- package/dist/providers/HybridRouter.js +332 -10
- package/dist/providers/HybridRouter.js.map +1 -1
- package/dist/providers/LLMBaselineTracker.d.ts +120 -0
- package/dist/providers/LLMBaselineTracker.d.ts.map +1 -0
- package/dist/providers/LLMBaselineTracker.js +305 -0
- package/dist/providers/LLMBaselineTracker.js.map +1 -0
- package/dist/providers/OpenRouterProvider.d.ts +26 -0
- package/dist/providers/OpenRouterProvider.d.ts.map +1 -1
- package/dist/providers/OpenRouterProvider.js +75 -6
- package/dist/providers/OpenRouterProvider.js.map +1 -1
- package/dist/providers/RuVectorClient.d.ts +259 -0
- package/dist/providers/RuVectorClient.d.ts.map +1 -0
- package/dist/providers/RuVectorClient.js +416 -0
- package/dist/providers/RuVectorClient.js.map +1 -0
- package/dist/providers/RuvllmPatternCurator.d.ts +116 -0
- package/dist/providers/RuvllmPatternCurator.d.ts.map +1 -0
- package/dist/providers/RuvllmPatternCurator.js +323 -0
- package/dist/providers/RuvllmPatternCurator.js.map +1 -0
- package/dist/providers/RuvllmProvider.d.ts +233 -1
- package/dist/providers/RuvllmProvider.d.ts.map +1 -1
- package/dist/providers/RuvllmProvider.js +781 -11
- package/dist/providers/RuvllmProvider.js.map +1 -1
- package/dist/providers/index.d.ts +5 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +12 -2
- package/dist/providers/index.js.map +1 -1
- package/dist/utils/ruvllm-loader.d.ts +98 -1
- package/dist/utils/ruvllm-loader.d.ts.map +1 -1
- package/dist/utils/ruvllm-loader.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,952 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* N8nReplayabilityTesterAgent
|
|
4
|
+
*
|
|
5
|
+
* Determinism and replayability testing for n8n workflows:
|
|
6
|
+
* - Fixed timestamps and stable IDs
|
|
7
|
+
* - Consistent pagination handling
|
|
8
|
+
* - Controlled randomness detection
|
|
9
|
+
* - Execution replay from fixtures
|
|
10
|
+
* - Snapshot comparison testing
|
|
11
|
+
* - Idempotent execution verification
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.N8nReplayabilityTesterAgent = void 0;
|
|
15
|
+
const N8nBaseAgent_1 = require("./N8nBaseAgent");
|
|
16
|
+
const N8nTestHarness_1 = require("./N8nTestHarness");
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Non-Determinism Patterns
|
|
19
|
+
// ============================================================================
|
|
20
|
+
const NON_DETERMINISTIC_PATTERNS = {
|
|
21
|
+
// Timestamp-related patterns
|
|
22
|
+
timestamps: [
|
|
23
|
+
/Date\.now\(\)/,
|
|
24
|
+
/new Date\(\)/,
|
|
25
|
+
/\$now/,
|
|
26
|
+
/\$today/,
|
|
27
|
+
/timestamp/i,
|
|
28
|
+
/createdAt/i,
|
|
29
|
+
/updatedAt/i,
|
|
30
|
+
/\{\{\s*\$now\s*\}\}/,
|
|
31
|
+
],
|
|
32
|
+
// Random value patterns
|
|
33
|
+
randomness: [
|
|
34
|
+
/Math\.random\(\)/,
|
|
35
|
+
/uuid/i,
|
|
36
|
+
/guid/i,
|
|
37
|
+
/nanoid/i,
|
|
38
|
+
/\$randomInt/,
|
|
39
|
+
/\$randomString/,
|
|
40
|
+
],
|
|
41
|
+
// Unstable ID patterns
|
|
42
|
+
unstableIds: [
|
|
43
|
+
/execution\.id/,
|
|
44
|
+
/\$execution\.id/,
|
|
45
|
+
/runId/i,
|
|
46
|
+
/sessionId/i,
|
|
47
|
+
/requestId/i,
|
|
48
|
+
],
|
|
49
|
+
// External state patterns
|
|
50
|
+
externalState: [
|
|
51
|
+
/process\.env/,
|
|
52
|
+
/\$env\./,
|
|
53
|
+
/\$vars\./,
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
// Node types known to be non-deterministic
|
|
57
|
+
const NON_DETERMINISTIC_NODE_TYPES = [
|
|
58
|
+
'n8n-nodes-base.httpRequest', // External API calls
|
|
59
|
+
'n8n-nodes-base.executeCommand', // Shell commands
|
|
60
|
+
'n8n-nodes-base.function', // Custom code
|
|
61
|
+
'n8n-nodes-base.code', // Custom code
|
|
62
|
+
'n8n-nodes-base.crypto', // Random generation
|
|
63
|
+
];
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// Agent Implementation
|
|
66
|
+
// ============================================================================
|
|
67
|
+
class N8nReplayabilityTesterAgent extends N8nBaseAgent_1.N8nBaseAgent {
|
|
68
|
+
constructor(config) {
|
|
69
|
+
const capabilities = [
|
|
70
|
+
{
|
|
71
|
+
name: 'determinism-testing',
|
|
72
|
+
version: '1.0.0',
|
|
73
|
+
description: 'Test workflow determinism across multiple runs',
|
|
74
|
+
parameters: {},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'execution-replay',
|
|
78
|
+
version: '1.0.0',
|
|
79
|
+
description: 'Replay workflows from recorded fixtures',
|
|
80
|
+
parameters: {},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'snapshot-comparison',
|
|
84
|
+
version: '1.0.0',
|
|
85
|
+
description: 'Compare execution snapshots for consistency',
|
|
86
|
+
parameters: {},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'fixture-recording',
|
|
90
|
+
version: '1.0.0',
|
|
91
|
+
description: 'Record execution fixtures for replay testing',
|
|
92
|
+
parameters: {},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'fixture-injection',
|
|
96
|
+
version: '1.0.0',
|
|
97
|
+
description: 'Inject fixtures into workflow for deterministic execution',
|
|
98
|
+
parameters: {},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'service-mocking',
|
|
102
|
+
version: '1.0.0',
|
|
103
|
+
description: 'Mock external service calls with recorded responses',
|
|
104
|
+
parameters: {},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'time-freezing',
|
|
108
|
+
version: '1.0.0',
|
|
109
|
+
description: 'Freeze time for deterministic timestamp operations',
|
|
110
|
+
parameters: {},
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
super({
|
|
114
|
+
...config,
|
|
115
|
+
type: 'n8n-replayability-tester',
|
|
116
|
+
capabilities: [...capabilities, ...(config.capabilities || [])],
|
|
117
|
+
});
|
|
118
|
+
this.fixtureStore = new Map();
|
|
119
|
+
}
|
|
120
|
+
async performTask(task) {
|
|
121
|
+
const replayTask = task;
|
|
122
|
+
if (replayTask.type !== 'replayability-test') {
|
|
123
|
+
throw new Error(`Unsupported task type: ${replayTask.type}`);
|
|
124
|
+
}
|
|
125
|
+
return this.testReplayability(replayTask.target, replayTask.options);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Test workflow replayability
|
|
129
|
+
*/
|
|
130
|
+
async testReplayability(workflowId, options, providedWorkflow) {
|
|
131
|
+
const workflow = providedWorkflow || await this.getWorkflow(workflowId);
|
|
132
|
+
const issues = [];
|
|
133
|
+
const nonDeterministicNodes = [];
|
|
134
|
+
const recommendations = [];
|
|
135
|
+
let comparisonResults;
|
|
136
|
+
let recordedFixtures;
|
|
137
|
+
// 1. Static analysis for non-deterministic patterns
|
|
138
|
+
const staticIssues = this.analyzeStaticNonDeterminism(workflow);
|
|
139
|
+
issues.push(...staticIssues);
|
|
140
|
+
// 2. Check node types for known non-determinism
|
|
141
|
+
const nodeTypeIssues = this.checkNodeTypes(workflow);
|
|
142
|
+
issues.push(...nodeTypeIssues);
|
|
143
|
+
// 3. Run determinism check if requested
|
|
144
|
+
if (options?.checkDeterminism) {
|
|
145
|
+
const iterations = options.iterations || 3;
|
|
146
|
+
const determinismResult = await this.checkDeterminism(workflow, iterations);
|
|
147
|
+
nonDeterministicNodes.push(...determinismResult.nonDeterministicNodes);
|
|
148
|
+
issues.push(...determinismResult.issues);
|
|
149
|
+
}
|
|
150
|
+
// 4. Replay from fixture if provided
|
|
151
|
+
if (options?.replayExecutionId || options?.fixtures) {
|
|
152
|
+
const fixtures = options.fixtures || await this.loadFixtures(workflowId, options.replayExecutionId);
|
|
153
|
+
comparisonResults = await this.replayAndCompare(workflow, fixtures);
|
|
154
|
+
const replayIssues = this.analyzeReplayResults(comparisonResults);
|
|
155
|
+
issues.push(...replayIssues);
|
|
156
|
+
}
|
|
157
|
+
// 5. Record fixture if in record mode
|
|
158
|
+
if (options?.recordMode) {
|
|
159
|
+
recordedFixtures = await this.recordFixture(workflow);
|
|
160
|
+
this.storeFixtures(workflowId, recordedFixtures);
|
|
161
|
+
}
|
|
162
|
+
// NEW 6. Execute with mocking if requested
|
|
163
|
+
let mockedExecutionResult;
|
|
164
|
+
if (options?.injectFixtures || options?.mockExternalServices || options?.mockConfigs) {
|
|
165
|
+
mockedExecutionResult = await this.executeWithMocking(workflowId, workflow, options.fixtures || [], options.mockConfigs || [], options.mockExternalServices || false, options.freezeTime);
|
|
166
|
+
// Add issues from mocked execution
|
|
167
|
+
if (!mockedExecutionResult.deterministic) {
|
|
168
|
+
issues.push({
|
|
169
|
+
type: 'non-deterministic',
|
|
170
|
+
severity: 'high',
|
|
171
|
+
node: 'workflow',
|
|
172
|
+
message: 'Workflow produced non-deterministic output even with mocking',
|
|
173
|
+
suggestion: 'Review mocking configuration - some non-determinism may be internal',
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Generate recommendations
|
|
178
|
+
recommendations.push(...this.generateRecommendations(issues, nonDeterministicNodes));
|
|
179
|
+
// Calculate scores
|
|
180
|
+
const determinismScore = this.calculateDeterminismScore(nonDeterministicNodes, workflow.nodes.length);
|
|
181
|
+
const replayabilityScore = this.calculateReplayabilityScore(issues);
|
|
182
|
+
const score = Math.round((determinismScore + replayabilityScore) / 2);
|
|
183
|
+
const result = {
|
|
184
|
+
workflowId: workflow.id || workflowId,
|
|
185
|
+
workflowName: workflow.name,
|
|
186
|
+
testDate: new Date().toISOString(),
|
|
187
|
+
passed: issues.filter(i => i.severity === 'critical' || i.severity === 'high').length === 0,
|
|
188
|
+
score,
|
|
189
|
+
determinismScore,
|
|
190
|
+
replayabilityScore,
|
|
191
|
+
issues,
|
|
192
|
+
nonDeterministicNodes,
|
|
193
|
+
recommendations,
|
|
194
|
+
fixtures: recordedFixtures,
|
|
195
|
+
comparisonResults,
|
|
196
|
+
mockedExecutionResult,
|
|
197
|
+
};
|
|
198
|
+
// Store result
|
|
199
|
+
await this.storeTestResult(`replayability-test:${workflowId}`, result);
|
|
200
|
+
// Emit event
|
|
201
|
+
this.emitEvent('replayability.test.completed', {
|
|
202
|
+
workflowId,
|
|
203
|
+
passed: result.passed,
|
|
204
|
+
determinismScore,
|
|
205
|
+
replayabilityScore,
|
|
206
|
+
issueCount: issues.length,
|
|
207
|
+
});
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Analyze workflow for static non-determinism patterns
|
|
212
|
+
*/
|
|
213
|
+
analyzeStaticNonDeterminism(workflow) {
|
|
214
|
+
const issues = [];
|
|
215
|
+
for (const node of workflow.nodes) {
|
|
216
|
+
const nodeJson = JSON.stringify(node.parameters);
|
|
217
|
+
// Check timestamp patterns
|
|
218
|
+
for (const pattern of NON_DETERMINISTIC_PATTERNS.timestamps) {
|
|
219
|
+
if (pattern.test(nodeJson)) {
|
|
220
|
+
issues.push({
|
|
221
|
+
type: 'timestamp-dependent',
|
|
222
|
+
severity: 'medium',
|
|
223
|
+
node: node.name,
|
|
224
|
+
message: `Node uses timestamp-dependent expression`,
|
|
225
|
+
suggestion: 'Use fixed timestamps for testing or pass timestamp as input parameter',
|
|
226
|
+
});
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// Check randomness patterns
|
|
231
|
+
for (const pattern of NON_DETERMINISTIC_PATTERNS.randomness) {
|
|
232
|
+
if (pattern.test(nodeJson)) {
|
|
233
|
+
issues.push({
|
|
234
|
+
type: 'random-value',
|
|
235
|
+
severity: 'high',
|
|
236
|
+
node: node.name,
|
|
237
|
+
message: `Node uses random value generation`,
|
|
238
|
+
suggestion: 'Seed random generators or use deterministic IDs for testing',
|
|
239
|
+
});
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Check unstable ID patterns
|
|
244
|
+
for (const pattern of NON_DETERMINISTIC_PATTERNS.unstableIds) {
|
|
245
|
+
if (pattern.test(nodeJson)) {
|
|
246
|
+
issues.push({
|
|
247
|
+
type: 'unstable-id',
|
|
248
|
+
severity: 'low',
|
|
249
|
+
node: node.name,
|
|
250
|
+
message: `Node uses execution-specific IDs`,
|
|
251
|
+
suggestion: 'Avoid using execution IDs in business logic',
|
|
252
|
+
});
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Check external state patterns
|
|
257
|
+
for (const pattern of NON_DETERMINISTIC_PATTERNS.externalState) {
|
|
258
|
+
if (pattern.test(nodeJson)) {
|
|
259
|
+
issues.push({
|
|
260
|
+
type: 'external-state',
|
|
261
|
+
severity: 'medium',
|
|
262
|
+
node: node.name,
|
|
263
|
+
message: `Node depends on external state (environment variables)`,
|
|
264
|
+
suggestion: 'Use consistent environment or mock external state for testing',
|
|
265
|
+
});
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return issues;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Check node types for known non-determinism
|
|
274
|
+
*/
|
|
275
|
+
checkNodeTypes(workflow) {
|
|
276
|
+
const issues = [];
|
|
277
|
+
for (const node of workflow.nodes) {
|
|
278
|
+
if (NON_DETERMINISTIC_NODE_TYPES.some(t => node.type.includes(t))) {
|
|
279
|
+
issues.push({
|
|
280
|
+
type: 'external-state',
|
|
281
|
+
severity: 'medium',
|
|
282
|
+
node: node.name,
|
|
283
|
+
message: `Node type "${node.type}" may produce non-deterministic results`,
|
|
284
|
+
suggestion: 'Mock external calls or use recorded responses for testing',
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return issues;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Check determinism by running workflow multiple times
|
|
292
|
+
*/
|
|
293
|
+
async checkDeterminism(workflow, iterations) {
|
|
294
|
+
const nonDeterministicNodes = [];
|
|
295
|
+
const issues = [];
|
|
296
|
+
// Collect execution results
|
|
297
|
+
const executionResults = new Map();
|
|
298
|
+
try {
|
|
299
|
+
for (let i = 0; i < iterations; i++) {
|
|
300
|
+
const execution = await this.executeWorkflow(workflow.id, {}, {
|
|
301
|
+
waitForCompletion: true,
|
|
302
|
+
timeout: 30000,
|
|
303
|
+
});
|
|
304
|
+
const runData = execution.data?.resultData?.runData;
|
|
305
|
+
if (runData) {
|
|
306
|
+
for (const [nodeName, nodeRuns] of Object.entries(runData)) {
|
|
307
|
+
const output = nodeRuns[0]?.data?.main?.[0]?.[0]?.json;
|
|
308
|
+
if (!executionResults.has(nodeName)) {
|
|
309
|
+
executionResults.set(nodeName, []);
|
|
310
|
+
}
|
|
311
|
+
executionResults.get(nodeName).push(output);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// Analyze variance
|
|
316
|
+
for (const [nodeName, outputs] of executionResults.entries()) {
|
|
317
|
+
const variance = this.calculateOutputVariance(outputs);
|
|
318
|
+
if (variance > 0) {
|
|
319
|
+
const node = workflow.nodes.find(n => n.name === nodeName);
|
|
320
|
+
nonDeterministicNodes.push({
|
|
321
|
+
nodeName,
|
|
322
|
+
nodeType: node?.type || 'unknown',
|
|
323
|
+
reason: 'Output varies between runs',
|
|
324
|
+
variance,
|
|
325
|
+
examples: outputs.slice(0, 3).map((value, i) => ({ run: i + 1, value })),
|
|
326
|
+
});
|
|
327
|
+
if (variance > 0.5) {
|
|
328
|
+
issues.push({
|
|
329
|
+
type: 'non-deterministic',
|
|
330
|
+
severity: 'high',
|
|
331
|
+
node: nodeName,
|
|
332
|
+
message: `Node output varies significantly between runs (${Math.round(variance * 100)}% variance)`,
|
|
333
|
+
suggestion: 'Review node configuration and mock external dependencies',
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
issues.push({
|
|
341
|
+
type: 'non-deterministic',
|
|
342
|
+
severity: 'medium',
|
|
343
|
+
node: 'workflow',
|
|
344
|
+
message: `Could not complete determinism check: ${error.message}`,
|
|
345
|
+
suggestion: 'Ensure workflow can be executed multiple times',
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
return { nonDeterministicNodes, issues };
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Calculate output variance between runs
|
|
352
|
+
*/
|
|
353
|
+
calculateOutputVariance(outputs) {
|
|
354
|
+
if (outputs.length < 2)
|
|
355
|
+
return 0;
|
|
356
|
+
let differences = 0;
|
|
357
|
+
const firstOutput = JSON.stringify(outputs[0]);
|
|
358
|
+
for (let i = 1; i < outputs.length; i++) {
|
|
359
|
+
if (JSON.stringify(outputs[i]) !== firstOutput) {
|
|
360
|
+
differences++;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return differences / (outputs.length - 1);
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Replay workflow and compare with fixtures
|
|
367
|
+
*/
|
|
368
|
+
async replayAndCompare(workflow, fixtures) {
|
|
369
|
+
const comparisons = [];
|
|
370
|
+
for (const fixture of fixtures) {
|
|
371
|
+
try {
|
|
372
|
+
const execution = await this.executeWorkflow(workflow.id, fixture.inputData, {
|
|
373
|
+
waitForCompletion: true,
|
|
374
|
+
timeout: 60000,
|
|
375
|
+
});
|
|
376
|
+
const runData = execution.data?.resultData?.runData;
|
|
377
|
+
if (runData) {
|
|
378
|
+
for (const [nodeName, expectedSnapshot] of Object.entries(fixture.nodeSnapshots)) {
|
|
379
|
+
const actualOutput = runData[nodeName]?.[0]?.data?.main?.[0]?.[0]?.json;
|
|
380
|
+
const differences = this.compareValues(expectedSnapshot.outputData, actualOutput, '');
|
|
381
|
+
comparisons.push({
|
|
382
|
+
nodeName,
|
|
383
|
+
matched: differences.length === 0,
|
|
384
|
+
expectedOutput: expectedSnapshot.outputData,
|
|
385
|
+
actualOutput,
|
|
386
|
+
differences,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
// Add comparison failure
|
|
393
|
+
for (const nodeName of Object.keys(fixture.nodeSnapshots)) {
|
|
394
|
+
comparisons.push({
|
|
395
|
+
nodeName,
|
|
396
|
+
matched: false,
|
|
397
|
+
expectedOutput: fixture.nodeSnapshots[nodeName].outputData,
|
|
398
|
+
actualOutput: null,
|
|
399
|
+
differences: [{
|
|
400
|
+
path: '',
|
|
401
|
+
expected: 'execution',
|
|
402
|
+
actual: 'error',
|
|
403
|
+
type: 'changed',
|
|
404
|
+
}],
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return comparisons;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Compare two values and find differences
|
|
413
|
+
*/
|
|
414
|
+
compareValues(expected, actual, path) {
|
|
415
|
+
const differences = [];
|
|
416
|
+
// Null/undefined handling
|
|
417
|
+
if (expected === null || expected === undefined) {
|
|
418
|
+
if (actual !== expected) {
|
|
419
|
+
differences.push({ path, expected, actual, type: 'changed' });
|
|
420
|
+
}
|
|
421
|
+
return differences;
|
|
422
|
+
}
|
|
423
|
+
if (actual === null || actual === undefined) {
|
|
424
|
+
differences.push({ path, expected, actual, type: 'missing' });
|
|
425
|
+
return differences;
|
|
426
|
+
}
|
|
427
|
+
// Type check
|
|
428
|
+
if (typeof expected !== typeof actual) {
|
|
429
|
+
differences.push({ path, expected, actual, type: 'type-mismatch' });
|
|
430
|
+
return differences;
|
|
431
|
+
}
|
|
432
|
+
// Array comparison
|
|
433
|
+
if (Array.isArray(expected)) {
|
|
434
|
+
if (!Array.isArray(actual)) {
|
|
435
|
+
differences.push({ path, expected, actual, type: 'type-mismatch' });
|
|
436
|
+
return differences;
|
|
437
|
+
}
|
|
438
|
+
const maxLen = Math.max(expected.length, actual.length);
|
|
439
|
+
for (let i = 0; i < maxLen; i++) {
|
|
440
|
+
const itemPath = `${path}[${i}]`;
|
|
441
|
+
if (i >= expected.length) {
|
|
442
|
+
differences.push({ path: itemPath, expected: undefined, actual: actual[i], type: 'extra' });
|
|
443
|
+
}
|
|
444
|
+
else if (i >= actual.length) {
|
|
445
|
+
differences.push({ path: itemPath, expected: expected[i], actual: undefined, type: 'missing' });
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
differences.push(...this.compareValues(expected[i], actual[i], itemPath));
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return differences;
|
|
452
|
+
}
|
|
453
|
+
// Object comparison
|
|
454
|
+
if (typeof expected === 'object') {
|
|
455
|
+
const expectedObj = expected;
|
|
456
|
+
const actualObj = actual;
|
|
457
|
+
const allKeys = new Set([...Object.keys(expectedObj), ...Object.keys(actualObj)]);
|
|
458
|
+
for (const key of allKeys) {
|
|
459
|
+
const keyPath = path ? `${path}.${key}` : key;
|
|
460
|
+
if (!(key in expectedObj)) {
|
|
461
|
+
differences.push({ path: keyPath, expected: undefined, actual: actualObj[key], type: 'extra' });
|
|
462
|
+
}
|
|
463
|
+
else if (!(key in actualObj)) {
|
|
464
|
+
differences.push({ path: keyPath, expected: expectedObj[key], actual: undefined, type: 'missing' });
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
differences.push(...this.compareValues(expectedObj[key], actualObj[key], keyPath));
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return differences;
|
|
471
|
+
}
|
|
472
|
+
// Primitive comparison
|
|
473
|
+
if (expected !== actual) {
|
|
474
|
+
differences.push({ path, expected, actual, type: 'changed' });
|
|
475
|
+
}
|
|
476
|
+
return differences;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Record execution as fixture
|
|
480
|
+
*/
|
|
481
|
+
async recordFixture(workflow) {
|
|
482
|
+
const fixtures = [];
|
|
483
|
+
try {
|
|
484
|
+
const execution = await this.executeWorkflow(workflow.id, {}, {
|
|
485
|
+
waitForCompletion: true,
|
|
486
|
+
timeout: 60000,
|
|
487
|
+
});
|
|
488
|
+
const nodeSnapshots = {};
|
|
489
|
+
const runData = execution.data?.resultData?.runData;
|
|
490
|
+
if (runData) {
|
|
491
|
+
for (const [nodeName, nodeRuns] of Object.entries(runData)) {
|
|
492
|
+
const run = nodeRuns[0];
|
|
493
|
+
if (run) {
|
|
494
|
+
nodeSnapshots[nodeName] = {
|
|
495
|
+
nodeName,
|
|
496
|
+
inputData: run.source?.[0] ? runData[run.source[0].previousNode]?.[0]?.data?.main?.[0]?.[0]?.json : null,
|
|
497
|
+
outputData: run.data?.main?.[0]?.[0]?.json,
|
|
498
|
+
executionTime: run.executionTime,
|
|
499
|
+
status: run.executionStatus,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
fixtures.push({
|
|
505
|
+
id: `fixture-${Date.now()}`,
|
|
506
|
+
name: `${workflow.name} - Auto-recorded`,
|
|
507
|
+
inputData: {},
|
|
508
|
+
expectedOutput: nodeSnapshots[Object.keys(nodeSnapshots).pop() || '']?.outputData || {},
|
|
509
|
+
nodeSnapshots,
|
|
510
|
+
metadata: {
|
|
511
|
+
recordedAt: new Date().toISOString(),
|
|
512
|
+
workflowVersion: workflow.versionId,
|
|
513
|
+
},
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
console.warn('Failed to record fixture:', error);
|
|
518
|
+
}
|
|
519
|
+
return fixtures;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Store fixtures for later use
|
|
523
|
+
*/
|
|
524
|
+
storeFixtures(workflowId, fixtures) {
|
|
525
|
+
const existing = this.fixtureStore.get(workflowId) || [];
|
|
526
|
+
this.fixtureStore.set(workflowId, [...existing, ...fixtures]);
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Load fixtures from store or execution history
|
|
530
|
+
*/
|
|
531
|
+
async loadFixtures(workflowId, executionId) {
|
|
532
|
+
if (this.fixtureStore.has(workflowId)) {
|
|
533
|
+
const fixtures = this.fixtureStore.get(workflowId);
|
|
534
|
+
if (executionId) {
|
|
535
|
+
return fixtures.filter(f => f.id === executionId);
|
|
536
|
+
}
|
|
537
|
+
return fixtures;
|
|
538
|
+
}
|
|
539
|
+
return [];
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Analyze replay results for issues
|
|
543
|
+
*/
|
|
544
|
+
analyzeReplayResults(comparisons) {
|
|
545
|
+
const issues = [];
|
|
546
|
+
for (const comparison of comparisons) {
|
|
547
|
+
if (!comparison.matched) {
|
|
548
|
+
const severity = comparison.differences.length > 5 ? 'high' : 'medium';
|
|
549
|
+
issues.push({
|
|
550
|
+
type: 'non-deterministic',
|
|
551
|
+
severity,
|
|
552
|
+
node: comparison.nodeName,
|
|
553
|
+
message: `Node output differs from recorded fixture (${comparison.differences.length} differences)`,
|
|
554
|
+
suggestion: 'Review what changed - may indicate drift or non-determinism',
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return issues;
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Calculate determinism score
|
|
562
|
+
*/
|
|
563
|
+
calculateDeterminismScore(nonDeterministicNodes, totalNodes) {
|
|
564
|
+
if (totalNodes === 0)
|
|
565
|
+
return 100;
|
|
566
|
+
const deterministicNodes = totalNodes - nonDeterministicNodes.length;
|
|
567
|
+
return Math.round((deterministicNodes / totalNodes) * 100);
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Calculate replayability score
|
|
571
|
+
*/
|
|
572
|
+
calculateReplayabilityScore(issues) {
|
|
573
|
+
const criticalCount = issues.filter(i => i.severity === 'critical').length;
|
|
574
|
+
const highCount = issues.filter(i => i.severity === 'high').length;
|
|
575
|
+
const mediumCount = issues.filter(i => i.severity === 'medium').length;
|
|
576
|
+
return Math.max(0, 100 - (criticalCount * 25) - (highCount * 15) - (mediumCount * 5));
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Generate recommendations
|
|
580
|
+
*/
|
|
581
|
+
generateRecommendations(issues, nonDeterministicNodes) {
|
|
582
|
+
const recommendations = [];
|
|
583
|
+
if (nonDeterministicNodes.length > 0) {
|
|
584
|
+
recommendations.push(`${nonDeterministicNodes.length} nodes produce varying outputs - consider mocking external dependencies`);
|
|
585
|
+
}
|
|
586
|
+
const timestampIssues = issues.filter(i => i.type === 'timestamp-dependent');
|
|
587
|
+
if (timestampIssues.length > 0) {
|
|
588
|
+
recommendations.push('Use fixed timestamps (e.g., $input.timestamp) instead of $now for reproducible tests');
|
|
589
|
+
}
|
|
590
|
+
const randomIssues = issues.filter(i => i.type === 'random-value');
|
|
591
|
+
if (randomIssues.length > 0) {
|
|
592
|
+
recommendations.push('Seed random generators or use deterministic ID generation for testing');
|
|
593
|
+
}
|
|
594
|
+
const externalStateIssues = issues.filter(i => i.type === 'external-state');
|
|
595
|
+
if (externalStateIssues.length > 0) {
|
|
596
|
+
recommendations.push('Create a test environment with consistent external state for replay testing');
|
|
597
|
+
}
|
|
598
|
+
return recommendations;
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Quick replayability check
|
|
602
|
+
*/
|
|
603
|
+
async quickCheck(workflowId) {
|
|
604
|
+
const result = await this.testReplayability(workflowId, {
|
|
605
|
+
checkDeterminism: false, // Skip actual execution for quick check
|
|
606
|
+
});
|
|
607
|
+
return {
|
|
608
|
+
replayable: result.passed,
|
|
609
|
+
determinismScore: result.determinismScore,
|
|
610
|
+
topIssue: result.issues[0]?.message || null,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
// ============================================================================
|
|
614
|
+
// Active Fixture Injection & Service Mocking
|
|
615
|
+
// ============================================================================
|
|
616
|
+
/**
|
|
617
|
+
* Execute workflow with mocked nodes and time freezing
|
|
618
|
+
*/
|
|
619
|
+
async executeWithMocking(workflowId, workflow, fixtures, mockConfigs, mockAllExternal, freezeTime) {
|
|
620
|
+
const startTime = Date.now();
|
|
621
|
+
const harness = new N8nTestHarness_1.N8nTestHarness(this.n8nConfig);
|
|
622
|
+
const mockedNodes = [];
|
|
623
|
+
const mockedCalls = [];
|
|
624
|
+
try {
|
|
625
|
+
// Build mock configurations
|
|
626
|
+
const mocks = this.buildMockConfigurations(workflow, fixtures, mockConfigs, mockAllExternal);
|
|
627
|
+
mockedNodes.push(...mocks.map(m => m.targetNode));
|
|
628
|
+
// Create mocked workflow
|
|
629
|
+
const { workflow: mockedWorkflow, cleanup } = await harness.createMockedWorkflow(workflowId, mocks);
|
|
630
|
+
try {
|
|
631
|
+
// Execute with optional time simulation
|
|
632
|
+
let execution;
|
|
633
|
+
if (freezeTime) {
|
|
634
|
+
const timeConfig = {
|
|
635
|
+
freezeTime,
|
|
636
|
+
mockDateNodes: true,
|
|
637
|
+
};
|
|
638
|
+
execution = await harness.executeWithTimeSimulation(mockedWorkflow.id, timeConfig, {});
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
// Activate and execute
|
|
642
|
+
await this.n8nClient.activateWorkflow(mockedWorkflow.id);
|
|
643
|
+
const exec = await this.n8nClient.executeWorkflow(mockedWorkflow.id, {});
|
|
644
|
+
execution = {
|
|
645
|
+
originalWorkflowId: workflowId,
|
|
646
|
+
testWorkflowId: mockedWorkflow.id,
|
|
647
|
+
execution: exec,
|
|
648
|
+
cleanedUp: false,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
// Analyze results
|
|
652
|
+
const deterministic = await this.checkMockedDeterminism(harness, mockedWorkflow.id, mocks, freezeTime);
|
|
653
|
+
// Check if output matches expected (from fixtures)
|
|
654
|
+
const outputMatchesExpected = this.checkOutputMatchesExpected(execution.execution, fixtures);
|
|
655
|
+
// Record mocked call info
|
|
656
|
+
for (const mock of mocks) {
|
|
657
|
+
mockedCalls.push({
|
|
658
|
+
nodeName: mock.targetNode,
|
|
659
|
+
originalType: this.getOriginalNodeType(workflow, mock.targetNode),
|
|
660
|
+
mockResponseUsed: true,
|
|
661
|
+
executionTime: 0, // Would need detailed execution timing
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
const summary = this.generateMockedExecutionSummary(mockedNodes.length, deterministic, outputMatchesExpected, freezeTime, Date.now() - startTime);
|
|
665
|
+
// Emit event
|
|
666
|
+
this.emitEvent('replayability.mocked-execution.completed', {
|
|
667
|
+
workflowId,
|
|
668
|
+
mockedNodes: mockedNodes.length,
|
|
669
|
+
deterministic,
|
|
670
|
+
outputMatchesExpected,
|
|
671
|
+
});
|
|
672
|
+
return {
|
|
673
|
+
executed: true,
|
|
674
|
+
mockedNodes,
|
|
675
|
+
frozenTime: freezeTime?.toISOString(),
|
|
676
|
+
deterministic,
|
|
677
|
+
outputMatchesExpected,
|
|
678
|
+
executionTime: Date.now() - startTime,
|
|
679
|
+
mockedCalls,
|
|
680
|
+
summary,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
finally {
|
|
684
|
+
await cleanup();
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
catch (error) {
|
|
688
|
+
return {
|
|
689
|
+
executed: false,
|
|
690
|
+
mockedNodes,
|
|
691
|
+
deterministic: false,
|
|
692
|
+
outputMatchesExpected: false,
|
|
693
|
+
executionTime: Date.now() - startTime,
|
|
694
|
+
mockedCalls,
|
|
695
|
+
summary: `Mocked execution failed: ${error.message}`,
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
finally {
|
|
699
|
+
await harness.cleanup();
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Build mock configurations from fixtures and explicit configs
|
|
704
|
+
*/
|
|
705
|
+
buildMockConfigurations(workflow, fixtures, explicitMocks, mockAllExternal) {
|
|
706
|
+
const mocks = [];
|
|
707
|
+
// Add explicit mocks
|
|
708
|
+
for (const mock of explicitMocks) {
|
|
709
|
+
mocks.push({
|
|
710
|
+
targetNode: mock.nodeName,
|
|
711
|
+
mockResponse: mock.mockResponse,
|
|
712
|
+
statusCode: mock.statusCode,
|
|
713
|
+
headers: mock.headers,
|
|
714
|
+
delay: mock.delay,
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
// Add mocks from fixtures
|
|
718
|
+
for (const fixture of fixtures) {
|
|
719
|
+
for (const [nodeName, snapshot] of Object.entries(fixture.nodeSnapshots)) {
|
|
720
|
+
// Don't double-mock
|
|
721
|
+
if (mocks.some(m => m.targetNode === nodeName))
|
|
722
|
+
continue;
|
|
723
|
+
mocks.push({
|
|
724
|
+
targetNode: nodeName,
|
|
725
|
+
mockResponse: snapshot.outputData,
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
// Auto-mock all external nodes if requested
|
|
730
|
+
if (mockAllExternal) {
|
|
731
|
+
for (const node of workflow.nodes) {
|
|
732
|
+
// Skip if already mocked
|
|
733
|
+
if (mocks.some(m => m.targetNode === node.name))
|
|
734
|
+
continue;
|
|
735
|
+
// Mock HTTP requests and other external nodes
|
|
736
|
+
if (NON_DETERMINISTIC_NODE_TYPES.some(t => node.type.includes(t))) {
|
|
737
|
+
// Look for fixture data first
|
|
738
|
+
const fixtureData = this.findFixtureDataForNode(fixtures, node.name);
|
|
739
|
+
mocks.push({
|
|
740
|
+
targetNode: node.name,
|
|
741
|
+
mockResponse: fixtureData || this.generateDefaultMock(node),
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
return mocks;
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Find fixture data for a specific node
|
|
750
|
+
*/
|
|
751
|
+
findFixtureDataForNode(fixtures, nodeName) {
|
|
752
|
+
for (const fixture of fixtures) {
|
|
753
|
+
const snapshot = fixture.nodeSnapshots[nodeName];
|
|
754
|
+
if (snapshot) {
|
|
755
|
+
return snapshot.outputData;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Generate a default mock response for a node
|
|
762
|
+
*/
|
|
763
|
+
generateDefaultMock(node) {
|
|
764
|
+
const nodeType = node.type.toLowerCase();
|
|
765
|
+
if (nodeType.includes('httprequest')) {
|
|
766
|
+
return {
|
|
767
|
+
__mocked: true,
|
|
768
|
+
status: 200,
|
|
769
|
+
data: {},
|
|
770
|
+
message: 'Mocked HTTP response',
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
if (nodeType.includes('executecommand')) {
|
|
774
|
+
return {
|
|
775
|
+
__mocked: true,
|
|
776
|
+
stdout: '',
|
|
777
|
+
stderr: '',
|
|
778
|
+
exitCode: 0,
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
return {
|
|
782
|
+
__mocked: true,
|
|
783
|
+
data: null,
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Get original node type before mocking
|
|
788
|
+
*/
|
|
789
|
+
getOriginalNodeType(workflow, nodeName) {
|
|
790
|
+
const node = workflow.nodes.find(n => n.name === nodeName);
|
|
791
|
+
return node?.type || 'unknown';
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Check if mocked execution is deterministic by running twice
|
|
795
|
+
*/
|
|
796
|
+
async checkMockedDeterminism(harness, workflowId, mocks, freezeTime) {
|
|
797
|
+
try {
|
|
798
|
+
// Execute twice with same mocks
|
|
799
|
+
const config = {
|
|
800
|
+
concurrency: 2,
|
|
801
|
+
staggerMs: 100,
|
|
802
|
+
timeout: 30000,
|
|
803
|
+
};
|
|
804
|
+
const result = await harness.executeConcurrently(workflowId, config);
|
|
805
|
+
// Check if outputs are identical
|
|
806
|
+
return result.allIdentical;
|
|
807
|
+
}
|
|
808
|
+
catch {
|
|
809
|
+
// If we can't verify, assume not deterministic
|
|
810
|
+
return false;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Check if execution output matches fixture expectations
|
|
815
|
+
*/
|
|
816
|
+
checkOutputMatchesExpected(execution, fixtures) {
|
|
817
|
+
if (!execution || fixtures.length === 0) {
|
|
818
|
+
return true; // No comparison to make
|
|
819
|
+
}
|
|
820
|
+
const runData = execution.data?.resultData?.runData;
|
|
821
|
+
if (!runData)
|
|
822
|
+
return false;
|
|
823
|
+
for (const fixture of fixtures) {
|
|
824
|
+
// Get final output
|
|
825
|
+
const nodeNames = Object.keys(runData);
|
|
826
|
+
const lastNode = nodeNames[nodeNames.length - 1];
|
|
827
|
+
const actualOutput = runData[lastNode]?.[0]?.data?.main?.[0]?.[0]?.json;
|
|
828
|
+
// Compare with expected
|
|
829
|
+
const differences = this.compareValues(fixture.expectedOutput, actualOutput, '');
|
|
830
|
+
// Allow for some differences (timestamps, IDs that may change)
|
|
831
|
+
const significantDifferences = differences.filter(d => !this.isAllowedDifference(d.path));
|
|
832
|
+
if (significantDifferences.length > 0) {
|
|
833
|
+
return false;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return true;
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Check if a difference is allowed (timestamps, IDs, etc)
|
|
840
|
+
*/
|
|
841
|
+
isAllowedDifference(path) {
|
|
842
|
+
const allowedPatterns = [
|
|
843
|
+
/timestamp/i,
|
|
844
|
+
/createdAt/i,
|
|
845
|
+
/updatedAt/i,
|
|
846
|
+
/id$/i,
|
|
847
|
+
/executionId/i,
|
|
848
|
+
/requestId/i,
|
|
849
|
+
/sessionId/i,
|
|
850
|
+
];
|
|
851
|
+
return allowedPatterns.some(pattern => pattern.test(path));
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Generate summary of mocked execution
|
|
855
|
+
*/
|
|
856
|
+
generateMockedExecutionSummary(mockedCount, deterministic, outputMatches, freezeTime, executionTime) {
|
|
857
|
+
const parts = [];
|
|
858
|
+
parts.push(`Executed with ${mockedCount} mocked node(s)`);
|
|
859
|
+
if (freezeTime) {
|
|
860
|
+
parts.push(`Time frozen to ${freezeTime.toISOString()}`);
|
|
861
|
+
}
|
|
862
|
+
if (deterministic) {
|
|
863
|
+
parts.push('✓ Execution is deterministic');
|
|
864
|
+
}
|
|
865
|
+
else {
|
|
866
|
+
parts.push('⚠️ Execution shows non-deterministic behavior');
|
|
867
|
+
}
|
|
868
|
+
if (outputMatches) {
|
|
869
|
+
parts.push('✓ Output matches expected fixtures');
|
|
870
|
+
}
|
|
871
|
+
else {
|
|
872
|
+
parts.push('⚠️ Output differs from expected fixtures');
|
|
873
|
+
}
|
|
874
|
+
if (executionTime) {
|
|
875
|
+
parts.push(`Completed in ${executionTime}ms`);
|
|
876
|
+
}
|
|
877
|
+
return parts.join('. ');
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* Record and store fixtures with mocking support
|
|
881
|
+
*/
|
|
882
|
+
async recordFixtureWithMocking(workflowId, inputData = {}) {
|
|
883
|
+
const workflow = await this.getWorkflow(workflowId);
|
|
884
|
+
const execution = await this.executeWorkflow(workflowId, inputData, {
|
|
885
|
+
waitForCompletion: true,
|
|
886
|
+
timeout: 60000,
|
|
887
|
+
});
|
|
888
|
+
const nodeSnapshots = {};
|
|
889
|
+
const runData = execution.data?.resultData?.runData;
|
|
890
|
+
if (runData) {
|
|
891
|
+
for (const [nodeName, nodeRuns] of Object.entries(runData)) {
|
|
892
|
+
const run = nodeRuns[0];
|
|
893
|
+
if (run) {
|
|
894
|
+
nodeSnapshots[nodeName] = {
|
|
895
|
+
nodeName,
|
|
896
|
+
inputData: run.source?.[0] ? runData[run.source[0].previousNode]?.[0]?.data?.main?.[0]?.[0]?.json : null,
|
|
897
|
+
outputData: run.data?.main?.[0]?.[0]?.json,
|
|
898
|
+
executionTime: run.executionTime,
|
|
899
|
+
status: run.executionStatus,
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
const fixture = {
|
|
905
|
+
id: `fixture-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
906
|
+
name: `${workflow.name} - ${new Date().toISOString()}`,
|
|
907
|
+
inputData,
|
|
908
|
+
expectedOutput: nodeSnapshots[Object.keys(nodeSnapshots).pop() || '']?.outputData || {},
|
|
909
|
+
nodeSnapshots,
|
|
910
|
+
metadata: {
|
|
911
|
+
recordedAt: new Date().toISOString(),
|
|
912
|
+
workflowVersion: workflow.versionId,
|
|
913
|
+
environment: process.env.NODE_ENV || 'development',
|
|
914
|
+
},
|
|
915
|
+
};
|
|
916
|
+
// Store fixture
|
|
917
|
+
this.storeFixtures(workflowId, [fixture]);
|
|
918
|
+
// Also persist to memory store
|
|
919
|
+
await this.storeTestResult(`fixture:${workflowId}:${fixture.id}`, fixture);
|
|
920
|
+
return fixture;
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Load fixtures from persistent storage
|
|
924
|
+
*/
|
|
925
|
+
async loadPersistedFixtures(workflowId) {
|
|
926
|
+
const fixtures = [];
|
|
927
|
+
// Load from in-memory store
|
|
928
|
+
const inMemory = this.fixtureStore.get(workflowId) || [];
|
|
929
|
+
fixtures.push(...inMemory);
|
|
930
|
+
// Would also load from persistent storage here
|
|
931
|
+
// const persisted = await this.retrieveTestResult(`fixtures:${workflowId}`);
|
|
932
|
+
return fixtures;
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Execute replay test with fixtures from storage
|
|
936
|
+
*/
|
|
937
|
+
async executeReplayTest(workflowId, fixtureId) {
|
|
938
|
+
const fixtures = await this.loadPersistedFixtures(workflowId);
|
|
939
|
+
const targetFixtures = fixtureId
|
|
940
|
+
? fixtures.filter(f => f.id === fixtureId)
|
|
941
|
+
: fixtures;
|
|
942
|
+
return this.testReplayability(workflowId, {
|
|
943
|
+
fixtures: targetFixtures,
|
|
944
|
+
injectFixtures: true,
|
|
945
|
+
mockExternalServices: true,
|
|
946
|
+
checkDeterminism: true,
|
|
947
|
+
iterations: 2,
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
exports.N8nReplayabilityTesterAgent = N8nReplayabilityTesterAgent;
|
|
952
|
+
//# sourceMappingURL=N8nReplayabilityTesterAgent.js.map
|