callman-core 1.0.6 → 1.2.0
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/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/script/errors.d.ts +3 -0
- package/dist/script/errors.d.ts.map +1 -0
- package/dist/script/errors.js +68 -0
- package/dist/script/errors.js.map +1 -0
- package/dist/script/executor.d.ts +5 -0
- package/dist/script/executor.d.ts.map +1 -0
- package/dist/script/executor.js +301 -0
- package/dist/script/executor.js.map +1 -0
- package/dist/script/features/assertions.d.ts +46 -0
- package/dist/script/features/assertions.d.ts.map +1 -0
- package/dist/script/features/assertions.js +264 -0
- package/dist/script/features/assertions.js.map +1 -0
- package/dist/script/features/counter.d.ts +11 -0
- package/dist/script/features/counter.d.ts.map +1 -0
- package/dist/script/features/counter.js +43 -0
- package/dist/script/features/counter.js.map +1 -0
- package/dist/script/features/cryptojs.d.ts +13 -0
- package/dist/script/features/cryptojs.d.ts.map +1 -0
- package/dist/script/features/cryptojs.js +63 -0
- package/dist/script/features/cryptojs.js.map +1 -0
- package/dist/script/features/db.d.ts +36 -0
- package/dist/script/features/db.d.ts.map +1 -0
- package/dist/script/features/db.js +165 -0
- package/dist/script/features/db.js.map +1 -0
- package/dist/script/features/environment.d.ts +11 -0
- package/dist/script/features/environment.d.ts.map +1 -0
- package/dist/script/features/environment.js +61 -0
- package/dist/script/features/environment.js.map +1 -0
- package/dist/script/features/feature-policy.d.ts +8 -0
- package/dist/script/features/feature-policy.d.ts.map +1 -0
- package/dist/script/features/feature-policy.js +88 -0
- package/dist/script/features/feature-policy.js.map +1 -0
- package/dist/script/features/kafka.d.ts +11 -0
- package/dist/script/features/kafka.d.ts.map +1 -0
- package/dist/script/features/kafka.js +71 -0
- package/dist/script/features/kafka.js.map +1 -0
- package/dist/script/features/log.d.ts +15 -0
- package/dist/script/features/log.d.ts.map +1 -0
- package/dist/script/features/log.js +59 -0
- package/dist/script/features/log.js.map +1 -0
- package/dist/script/features/node.d.ts +14 -0
- package/dist/script/features/node.d.ts.map +1 -0
- package/dist/script/features/node.js +82 -0
- package/dist/script/features/node.js.map +1 -0
- package/dist/script/features/random.d.ts +13 -0
- package/dist/script/features/random.d.ts.map +1 -0
- package/dist/script/features/random.js +111 -0
- package/dist/script/features/random.js.map +1 -0
- package/dist/script/features/redis.d.ts +10 -0
- package/dist/script/features/redis.d.ts.map +1 -0
- package/dist/script/features/redis.js +22 -0
- package/dist/script/features/redis.js.map +1 -0
- package/dist/script/features/request.d.ts +23 -0
- package/dist/script/features/request.d.ts.map +1 -0
- package/dist/script/features/request.js +105 -0
- package/dist/script/features/request.js.map +1 -0
- package/dist/script/features/response.d.ts +25 -0
- package/dist/script/features/response.d.ts.map +1 -0
- package/dist/script/features/response.js +43 -0
- package/dist/script/features/response.js.map +1 -0
- package/dist/script/features/tests.d.ts +15 -0
- package/dist/script/features/tests.d.ts.map +1 -0
- package/dist/script/features/tests.js +147 -0
- package/dist/script/features/tests.js.map +1 -0
- package/dist/script/features/variables.d.ts +6 -0
- package/dist/script/features/variables.d.ts.map +1 -0
- package/dist/script/features/variables.js +25 -0
- package/dist/script/features/variables.js.map +1 -0
- package/dist/script/legacy.d.ts +4 -0
- package/dist/script/legacy.d.ts.map +1 -0
- package/dist/script/legacy.js +128 -0
- package/dist/script/legacy.js.map +1 -0
- package/dist/script/sandbox.d.ts +16 -0
- package/dist/script/sandbox.d.ts.map +1 -0
- package/dist/script/sandbox.js +51 -0
- package/dist/script/sandbox.js.map +1 -0
- package/dist/script/scope.d.ts +23 -0
- package/dist/script/scope.d.ts.map +1 -0
- package/dist/script/scope.js +172 -0
- package/dist/script/scope.js.map +1 -0
- package/dist/script/scriptEngine.d.ts +2 -2
- package/dist/script/scriptEngine.d.ts.map +1 -1
- package/dist/script/scriptEngine.js +7 -105
- package/dist/script/scriptEngine.js.map +1 -1
- package/dist/script/timeout.d.ts +3 -0
- package/dist/script/timeout.d.ts.map +1 -0
- package/dist/script/timeout.js +38 -0
- package/dist/script/timeout.js.map +1 -0
- package/dist/script/types.d.ts +167 -0
- package/dist/script/types.d.ts.map +1 -0
- package/dist/script/types.js +7 -0
- package/dist/script/types.js.map +1 -0
- package/dist-cjs/contract/contractValidator.js +236 -0
- package/dist-cjs/contract/contractValidator.js.map +1 -0
- package/dist-cjs/index.js +47 -0
- package/dist-cjs/index.js.map +1 -0
- package/dist-cjs/package.json +1 -0
- package/dist-cjs/request/executeRequest.js +33 -0
- package/dist-cjs/request/executeRequest.js.map +1 -0
- package/dist-cjs/runner/requestRunner.js +102 -0
- package/dist-cjs/runner/requestRunner.js.map +1 -0
- package/dist-cjs/runner/runCollection.js +53 -0
- package/dist-cjs/runner/runCollection.js.map +1 -0
- package/dist-cjs/scenario-runner/condition.js +1735 -0
- package/dist-cjs/scenario-runner/condition.js.map +1 -0
- package/dist-cjs/scenario-runner/conditionTypes.js +3 -0
- package/dist-cjs/scenario-runner/conditionTypes.js.map +1 -0
- package/dist-cjs/scenario-runner/executionPolicyRunner.js +53 -0
- package/dist-cjs/scenario-runner/executionPolicyRunner.js.map +1 -0
- package/dist-cjs/scenario-runner/graphTraversal.js +27 -0
- package/dist-cjs/scenario-runner/graphTraversal.js.map +1 -0
- package/dist-cjs/scenario-runner/index.js +39 -0
- package/dist-cjs/scenario-runner/index.js.map +1 -0
- package/dist-cjs/scenario-runner/nodePolicy.js +127 -0
- package/dist-cjs/scenario-runner/nodePolicy.js.map +1 -0
- package/dist-cjs/scenario-runner/notification/index.js +18 -0
- package/dist-cjs/scenario-runner/notification/index.js.map +1 -0
- package/dist-cjs/scenario-runner/notification/providers/provider.interface.js +3 -0
- package/dist-cjs/scenario-runner/notification/providers/provider.interface.js.map +1 -0
- package/dist-cjs/scenario-runner/notification/providers/slack.provider.js +88 -0
- package/dist-cjs/scenario-runner/notification/providers/slack.provider.js.map +1 -0
- package/dist-cjs/scenario-runner/notification/template.js +13 -0
- package/dist-cjs/scenario-runner/notification/template.js.map +1 -0
- package/dist-cjs/scenario-runner/retryExecutor.js +57 -0
- package/dist-cjs/scenario-runner/retryExecutor.js.map +1 -0
- package/dist-cjs/scenario-runner/runScenario.js +1545 -0
- package/dist-cjs/scenario-runner/runScenario.js.map +1 -0
- package/dist-cjs/scenario-runner/templateResolver.js +468 -0
- package/dist-cjs/scenario-runner/templateResolver.js.map +1 -0
- package/dist-cjs/scenario-runner/types.js +3 -0
- package/dist-cjs/scenario-runner/types.js.map +1 -0
- package/dist-cjs/scenario-runner/waitNodeExecutor.js +71 -0
- package/dist-cjs/scenario-runner/waitNodeExecutor.js.map +1 -0
- package/dist-cjs/script/errors.js +72 -0
- package/dist-cjs/script/errors.js.map +1 -0
- package/dist-cjs/script/executor.js +306 -0
- package/dist-cjs/script/executor.js.map +1 -0
- package/dist-cjs/script/features/assertions.js +269 -0
- package/dist-cjs/script/features/assertions.js.map +1 -0
- package/dist-cjs/script/features/counter.js +47 -0
- package/dist-cjs/script/features/counter.js.map +1 -0
- package/dist-cjs/script/features/cryptojs.js +67 -0
- package/dist-cjs/script/features/cryptojs.js.map +1 -0
- package/dist-cjs/script/features/db.js +169 -0
- package/dist-cjs/script/features/db.js.map +1 -0
- package/dist-cjs/script/features/environment.js +65 -0
- package/dist-cjs/script/features/environment.js.map +1 -0
- package/dist-cjs/script/features/feature-policy.js +94 -0
- package/dist-cjs/script/features/feature-policy.js.map +1 -0
- package/dist-cjs/script/features/kafka.js +77 -0
- package/dist-cjs/script/features/kafka.js.map +1 -0
- package/dist-cjs/script/features/log.js +64 -0
- package/dist-cjs/script/features/log.js.map +1 -0
- package/dist-cjs/script/features/node.js +89 -0
- package/dist-cjs/script/features/node.js.map +1 -0
- package/dist-cjs/script/features/random.js +115 -0
- package/dist-cjs/script/features/random.js.map +1 -0
- package/dist-cjs/script/features/redis.js +26 -0
- package/dist-cjs/script/features/redis.js.map +1 -0
- package/dist-cjs/script/features/request.js +109 -0
- package/dist-cjs/script/features/request.js.map +1 -0
- package/dist-cjs/script/features/response.js +47 -0
- package/dist-cjs/script/features/response.js.map +1 -0
- package/dist-cjs/script/features/tests.js +151 -0
- package/dist-cjs/script/features/tests.js.map +1 -0
- package/dist-cjs/script/features/variables.js +29 -0
- package/dist-cjs/script/features/variables.js.map +1 -0
- package/dist-cjs/script/legacy.js +132 -0
- package/dist-cjs/script/legacy.js.map +1 -0
- package/dist-cjs/script/pmApi.js +353 -0
- package/dist-cjs/script/pmApi.js.map +1 -0
- package/dist-cjs/script/sandbox.js +91 -0
- package/dist-cjs/script/sandbox.js.map +1 -0
- package/dist-cjs/script/scope.js +177 -0
- package/dist-cjs/script/scope.js.map +1 -0
- package/dist-cjs/script/scriptEngine.js +12 -0
- package/dist-cjs/script/scriptEngine.js.map +1 -0
- package/dist-cjs/script/timeout.js +43 -0
- package/dist-cjs/script/timeout.js.map +1 -0
- package/dist-cjs/script/types.js +8 -0
- package/dist-cjs/script/types.js.map +1 -0
- package/dist-cjs/types/index.js +3 -0
- package/dist-cjs/types/index.js.map +1 -0
- package/dist-cjs/utils/get.js +52 -0
- package/dist-cjs/utils/get.js.map +1 -0
- package/dist-cjs/utils/random.js +87 -0
- package/dist-cjs/utils/random.js.map +1 -0
- package/dist-cjs/variables/envStore.js +24 -0
- package/dist-cjs/variables/envStore.js.map +1 -0
- package/dist-cjs/variables/resolver.js +95 -0
- package/dist-cjs/variables/resolver.js.map +1 -0
- package/package.json +19 -3
- package/samples/runSample.mjs +0 -128
- package/tests/waitNode.test.mjs +0 -311
- package/tsconfig.json +0 -18
|
@@ -0,0 +1,1545 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runScenario = void 0;
|
|
4
|
+
const condition_js_1 = require("./condition.js");
|
|
5
|
+
const graphTraversal_js_1 = require("./graphTraversal.js");
|
|
6
|
+
const executionPolicyRunner_js_1 = require("./executionPolicyRunner.js");
|
|
7
|
+
const nodePolicy_js_1 = require("./nodePolicy.js");
|
|
8
|
+
const templateResolver_js_1 = require("./templateResolver.js");
|
|
9
|
+
const waitNodeExecutor_js_1 = require("./waitNodeExecutor.js");
|
|
10
|
+
const index_js_1 = require("./notification/index.js");
|
|
11
|
+
class ScenarioStopError extends Error {
|
|
12
|
+
constructor() {
|
|
13
|
+
super("Scenario execution stopped");
|
|
14
|
+
this.name = "ScenarioStopError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
class ScenarioNodeFailureError extends Error {
|
|
18
|
+
constructor(message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "ScenarioNodeFailureError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const DEFAULT_MIN_NODE_VISUAL_STATE_MS = 180;
|
|
24
|
+
const createId = (prefix) => {
|
|
25
|
+
const randomUuid = globalThis.crypto?.randomUUID?.();
|
|
26
|
+
return randomUuid ?? `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
27
|
+
};
|
|
28
|
+
const createRunId = () => createId("scenario-run");
|
|
29
|
+
const buildTitle = (node) => {
|
|
30
|
+
const label = node.data.label.trim();
|
|
31
|
+
if (label) {
|
|
32
|
+
return label;
|
|
33
|
+
}
|
|
34
|
+
return `${node.type[0]?.toUpperCase() ?? ""}${node.type.slice(1)}`;
|
|
35
|
+
};
|
|
36
|
+
const isKafkaNode = (node) => node?.type === "kafka";
|
|
37
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
38
|
+
const RESERVED_WORKFLOW_ROOT_LABELS = new Set(["workflow", "__meta"]);
|
|
39
|
+
const cloneNodeExecutionMeta = (meta) => ({
|
|
40
|
+
...meta,
|
|
41
|
+
...(meta.error ? { error: { ...meta.error } } : {}),
|
|
42
|
+
});
|
|
43
|
+
const cloneWorkflowExecutionMeta = (meta) => ({
|
|
44
|
+
...meta,
|
|
45
|
+
});
|
|
46
|
+
const cloneNodeMetaByLabel = (value) => Object.fromEntries(Object.entries(value).map(([label, meta]) => [label, cloneNodeExecutionMeta(meta)]));
|
|
47
|
+
const attachNodeMetaToOutput = (output, meta) => {
|
|
48
|
+
const normalizedMeta = cloneNodeExecutionMeta(meta);
|
|
49
|
+
if (Array.isArray(output)) {
|
|
50
|
+
const nextValue = [...output];
|
|
51
|
+
nextValue.__meta = normalizedMeta;
|
|
52
|
+
return nextValue;
|
|
53
|
+
}
|
|
54
|
+
if (isRecord(output)) {
|
|
55
|
+
return {
|
|
56
|
+
...output,
|
|
57
|
+
__meta: normalizedMeta,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (output === null || typeof output === "undefined") {
|
|
61
|
+
return {
|
|
62
|
+
__meta: normalizedMeta,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return output;
|
|
66
|
+
};
|
|
67
|
+
const createInitialWorkflowMeta = ({ startedAt, totalNodes, }) => ({
|
|
68
|
+
status: "success",
|
|
69
|
+
success: true,
|
|
70
|
+
failed: false,
|
|
71
|
+
totalNodes,
|
|
72
|
+
successfulNodes: 0,
|
|
73
|
+
failedNodes: 0,
|
|
74
|
+
skippedNodes: 0,
|
|
75
|
+
totalRetries: 0,
|
|
76
|
+
startedAt,
|
|
77
|
+
endedAt: startedAt,
|
|
78
|
+
durationMs: 0,
|
|
79
|
+
currentNode: null,
|
|
80
|
+
lastNode: null,
|
|
81
|
+
failedNode: null,
|
|
82
|
+
});
|
|
83
|
+
const buildWorkflowRoot = ({ scenario, meta, }) => ({
|
|
84
|
+
id: scenario.id,
|
|
85
|
+
name: scenario.name,
|
|
86
|
+
...(typeof scenario.version === "number" ? { version: scenario.version } : {}),
|
|
87
|
+
__meta: cloneWorkflowExecutionMeta(meta),
|
|
88
|
+
});
|
|
89
|
+
const createInputSnapshot = (context) => ({
|
|
90
|
+
env: context.environment,
|
|
91
|
+
global: context.globals,
|
|
92
|
+
workflow: context.workflowContext,
|
|
93
|
+
response: context.responseRoot,
|
|
94
|
+
db: {
|
|
95
|
+
result: context.dbResult ?? null,
|
|
96
|
+
},
|
|
97
|
+
kafkaEvent: context.kafkaEvent ?? null,
|
|
98
|
+
redis: {
|
|
99
|
+
result: context.redisResult ?? null,
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
const createContextSnapshot = (context) => ({
|
|
103
|
+
environment: { ...context.environment },
|
|
104
|
+
globals: { ...context.globals },
|
|
105
|
+
workflowContext: { ...context.workflowContext },
|
|
106
|
+
nodeMetaByLabel: cloneNodeMetaByLabel(context.nodeMetaByLabel),
|
|
107
|
+
workflowMeta: cloneWorkflowExecutionMeta(context.workflowMeta),
|
|
108
|
+
responseRoot: context.responseRoot ? { ...context.responseRoot } : null,
|
|
109
|
+
lastResponse: context.lastResponse,
|
|
110
|
+
dbResult: context.dbResult,
|
|
111
|
+
kafkaEvent: context.kafkaEvent,
|
|
112
|
+
kafkaCapture: context.kafkaCapture,
|
|
113
|
+
redisResult: context.redisResult,
|
|
114
|
+
});
|
|
115
|
+
const buildKafkaEventValue = (message) => {
|
|
116
|
+
const fallbackValue = {
|
|
117
|
+
value: message.valueJson ?? message.valueText,
|
|
118
|
+
text: message.valueText,
|
|
119
|
+
key: message.key,
|
|
120
|
+
partition: message.partition,
|
|
121
|
+
offset: message.offset,
|
|
122
|
+
timestamp: message.timestamp,
|
|
123
|
+
headers: Object.fromEntries(message.headers.map((header) => [header.key, header.value])),
|
|
124
|
+
};
|
|
125
|
+
if (isRecord(message.valueJson)) {
|
|
126
|
+
return {
|
|
127
|
+
...message.valueJson,
|
|
128
|
+
meta: fallbackValue,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return fallbackValue;
|
|
132
|
+
};
|
|
133
|
+
const buildKafkaFailureMessage = (result, usedMatch) => {
|
|
134
|
+
if (result.errorMessage?.trim()) {
|
|
135
|
+
return result.errorMessage;
|
|
136
|
+
}
|
|
137
|
+
if (result.timedOut) {
|
|
138
|
+
return usedMatch
|
|
139
|
+
? "Kafka node timed out without a matching event."
|
|
140
|
+
: "Kafka node timed out without receiving any events.";
|
|
141
|
+
}
|
|
142
|
+
return usedMatch
|
|
143
|
+
? "No Kafka event matched the configured rule."
|
|
144
|
+
: "Kafka node did not receive any events.";
|
|
145
|
+
};
|
|
146
|
+
const safeStringifyKafkaMessage = (message) => {
|
|
147
|
+
try {
|
|
148
|
+
return JSON.stringify(message);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return message.valueText;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
const buildScenarioRegex = (pattern) => {
|
|
155
|
+
const trimmedPattern = pattern.trim();
|
|
156
|
+
const literalMatch = trimmedPattern.match(/^\/(.+)\/([a-z]*)$/i);
|
|
157
|
+
if (literalMatch) {
|
|
158
|
+
return new RegExp(literalMatch[1] ?? "", literalMatch[2] ?? "");
|
|
159
|
+
}
|
|
160
|
+
return new RegExp(trimmedPattern);
|
|
161
|
+
};
|
|
162
|
+
const applyVariableMutationsToContext = (context, mutations) => {
|
|
163
|
+
for (const mutation of mutations) {
|
|
164
|
+
if (mutation.type === "set") {
|
|
165
|
+
if (mutation.scope === "environment") {
|
|
166
|
+
context.environment[mutation.key] = mutation.value ?? "";
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
context.globals[mutation.key] = mutation.value ?? "";
|
|
170
|
+
}
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (mutation.scope === "environment") {
|
|
174
|
+
delete context.environment[mutation.key];
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
delete context.globals[mutation.key];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
const throwIfStopped = (signal) => {
|
|
182
|
+
if (signal?.aborted) {
|
|
183
|
+
throw new ScenarioStopError();
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
const isWorkflowFailureStatus = (status) => status === "failed" || status === "failed_continued";
|
|
187
|
+
const isWorkflowSuccessStatus = (status) => status === "success" || status === "partial_success";
|
|
188
|
+
const buildNodeExecutionMeta = ({ status, attempts, startedAt, endedAt, durationMs, continueAfterFailure = false, errorMessage, }) => ({
|
|
189
|
+
status,
|
|
190
|
+
success: status === "success",
|
|
191
|
+
failed: status === "failed",
|
|
192
|
+
skipped: status === "skipped",
|
|
193
|
+
attempts,
|
|
194
|
+
startedAt,
|
|
195
|
+
endedAt,
|
|
196
|
+
durationMs,
|
|
197
|
+
...(continueAfterFailure ? { continueAfterFailure: true } : {}),
|
|
198
|
+
...(errorMessage
|
|
199
|
+
? {
|
|
200
|
+
error: {
|
|
201
|
+
message: errorMessage,
|
|
202
|
+
type: "ScenarioNodeFailureError",
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
: {}),
|
|
206
|
+
});
|
|
207
|
+
const updateWorkflowMetaState = ({ context, records, localStatuses, startedAt, currentNode, lastNode, failedNode, endedAt = new Date().toISOString(), }) => {
|
|
208
|
+
const statuses = Array.from(localStatuses.values());
|
|
209
|
+
const successfulNodes = statuses.filter(isWorkflowSuccessStatus).length;
|
|
210
|
+
const failedNodes = statuses.filter(isWorkflowFailureStatus).length;
|
|
211
|
+
const skippedNodes = statuses.filter((status) => status === "skipped").length;
|
|
212
|
+
const totalRetries = Array.from(records.values()).reduce((sum, record) => sum + Math.max(0, record.retryCount), 0);
|
|
213
|
+
const hasPartialNodes = statuses.some((status) => status === "failed_continued" || status === "partial_success");
|
|
214
|
+
const nextStatus = failedNodes > 0 && !hasPartialNodes
|
|
215
|
+
? "failed"
|
|
216
|
+
: failedNodes > 0 || hasPartialNodes
|
|
217
|
+
? "partial"
|
|
218
|
+
: "success";
|
|
219
|
+
const nextMeta = {
|
|
220
|
+
status: nextStatus,
|
|
221
|
+
success: nextStatus === "success",
|
|
222
|
+
failed: nextStatus === "failed",
|
|
223
|
+
totalNodes: context.workflowMeta.totalNodes,
|
|
224
|
+
successfulNodes,
|
|
225
|
+
failedNodes,
|
|
226
|
+
skippedNodes,
|
|
227
|
+
totalRetries,
|
|
228
|
+
startedAt,
|
|
229
|
+
endedAt,
|
|
230
|
+
durationMs: Math.max(0, new Date(endedAt).getTime() - new Date(startedAt).getTime()),
|
|
231
|
+
currentNode: currentNode ?? context.workflowMeta.currentNode ?? null,
|
|
232
|
+
lastNode: lastNode ?? context.workflowMeta.lastNode ?? null,
|
|
233
|
+
failedNode: failedNode ?? context.workflowMeta.failedNode ?? null,
|
|
234
|
+
};
|
|
235
|
+
context.workflowMeta = nextMeta;
|
|
236
|
+
context.workflowContext.workflow = {
|
|
237
|
+
...(isRecord(context.workflowContext.workflow)
|
|
238
|
+
? context.workflowContext.workflow
|
|
239
|
+
: {}),
|
|
240
|
+
__meta: cloneWorkflowExecutionMeta(nextMeta),
|
|
241
|
+
};
|
|
242
|
+
return nextMeta;
|
|
243
|
+
};
|
|
244
|
+
const withNotificationFailurePolicyOverride = (node, policy) => {
|
|
245
|
+
if (node.type !== "notification") {
|
|
246
|
+
return policy;
|
|
247
|
+
}
|
|
248
|
+
if (typeof node.data.config.continueOnFailure !== "boolean") {
|
|
249
|
+
return policy;
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
...policy,
|
|
253
|
+
onFailure: node.data.config.continueOnFailure ? "continue" : "stop",
|
|
254
|
+
};
|
|
255
|
+
};
|
|
256
|
+
const normalizeNotificationSeverity = (value) => {
|
|
257
|
+
switch (value) {
|
|
258
|
+
case "success":
|
|
259
|
+
case "warning":
|
|
260
|
+
case "error":
|
|
261
|
+
return value;
|
|
262
|
+
case "info":
|
|
263
|
+
default:
|
|
264
|
+
return "info";
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
const persistNodeExecutionMeta = ({ context, label, output, meta, }) => {
|
|
268
|
+
const trimmedLabel = label.trim();
|
|
269
|
+
if (!trimmedLabel) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
context.nodeMetaByLabel[trimmedLabel] = cloneNodeExecutionMeta(meta);
|
|
273
|
+
if (RESERVED_WORKFLOW_ROOT_LABELS.has(trimmedLabel)) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
context.workflowContext[trimmedLabel] = attachNodeMetaToOutput(output, meta);
|
|
277
|
+
};
|
|
278
|
+
const createStepExecutionRecord = ({ stepId, stepType, title, }) => ({
|
|
279
|
+
stepId,
|
|
280
|
+
stepType,
|
|
281
|
+
title,
|
|
282
|
+
status: "idle",
|
|
283
|
+
meta: null,
|
|
284
|
+
attempts: 0,
|
|
285
|
+
currentAttempt: null,
|
|
286
|
+
maxAttempts: 1,
|
|
287
|
+
retryCount: 0,
|
|
288
|
+
retryEnabled: false,
|
|
289
|
+
retryDelayMs: 0,
|
|
290
|
+
failurePolicy: null,
|
|
291
|
+
continuedAfterFailure: false,
|
|
292
|
+
errors: [],
|
|
293
|
+
durationMs: null,
|
|
294
|
+
statusCode: null,
|
|
295
|
+
errorMessage: null,
|
|
296
|
+
response: null,
|
|
297
|
+
testResults: [],
|
|
298
|
+
contractResults: [],
|
|
299
|
+
contractSummary: null,
|
|
300
|
+
contractValidationError: null,
|
|
301
|
+
dbAssertionResults: [],
|
|
302
|
+
redisAssertionResults: [],
|
|
303
|
+
scriptSource: null,
|
|
304
|
+
scriptTimeoutMs: null,
|
|
305
|
+
scriptLogs: [],
|
|
306
|
+
conditionExpression: null,
|
|
307
|
+
conditionPreview: null,
|
|
308
|
+
conditionMode: null,
|
|
309
|
+
conditionResult: null,
|
|
310
|
+
conditionDebug: null,
|
|
311
|
+
activeBranch: null,
|
|
312
|
+
inputData: null,
|
|
313
|
+
outputData: null,
|
|
314
|
+
incomingEdgeId: null,
|
|
315
|
+
startedAt: null,
|
|
316
|
+
completedAt: null,
|
|
317
|
+
});
|
|
318
|
+
const isTerminalStatus = (status) => status !== "idle" && status !== "running" && status !== "retrying";
|
|
319
|
+
const updateRecordFromEvent = (records, event) => {
|
|
320
|
+
if (event.type === "scenario:start" ||
|
|
321
|
+
event.type === "scenario:end" ||
|
|
322
|
+
event.type === "step:reset") {
|
|
323
|
+
if (event.type !== "step:reset") {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
for (const stepId of event.stepIds) {
|
|
327
|
+
const current = records.get(stepId);
|
|
328
|
+
if (!current) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
records.set(stepId, {
|
|
332
|
+
...current,
|
|
333
|
+
status: "idle",
|
|
334
|
+
currentAttempt: null,
|
|
335
|
+
errorMessage: null,
|
|
336
|
+
startedAt: null,
|
|
337
|
+
completedAt: null,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const current = records.get(event.stepId) ??
|
|
343
|
+
createStepExecutionRecord({
|
|
344
|
+
stepId: event.stepId,
|
|
345
|
+
stepType: "stepType" in event ? event.stepType : "condition",
|
|
346
|
+
title: event.title,
|
|
347
|
+
});
|
|
348
|
+
if (event.type === "step:start") {
|
|
349
|
+
records.set(event.stepId, {
|
|
350
|
+
...current,
|
|
351
|
+
stepType: event.stepType,
|
|
352
|
+
title: event.title,
|
|
353
|
+
status: "running",
|
|
354
|
+
meta: null,
|
|
355
|
+
attempts: event.attempt ?? 1,
|
|
356
|
+
currentAttempt: event.attempt ?? 1,
|
|
357
|
+
maxAttempts: event.maxAttempts ?? 1,
|
|
358
|
+
retryCount: Math.max(0, (event.attempt ?? 1) - 1),
|
|
359
|
+
retryEnabled: event.retryEnabled ?? false,
|
|
360
|
+
retryDelayMs: event.retryDelayMs ?? 0,
|
|
361
|
+
failurePolicy: event.failurePolicy ?? null,
|
|
362
|
+
errors: (event.attempt ?? 1) > 1 ? current.errors : [],
|
|
363
|
+
inputData: event.inputData ?? null,
|
|
364
|
+
incomingEdgeId: event.incomingEdgeId ?? null,
|
|
365
|
+
errorMessage: null,
|
|
366
|
+
startedAt: current.startedAt ?? event.at,
|
|
367
|
+
completedAt: null,
|
|
368
|
+
});
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (event.type === "step:retry") {
|
|
372
|
+
records.set(event.stepId, {
|
|
373
|
+
...current,
|
|
374
|
+
stepType: event.stepType,
|
|
375
|
+
title: event.title,
|
|
376
|
+
status: "retrying",
|
|
377
|
+
meta: current.meta,
|
|
378
|
+
attempts: event.attempt,
|
|
379
|
+
currentAttempt: event.attempt,
|
|
380
|
+
maxAttempts: event.maxAttempts,
|
|
381
|
+
retryCount: Math.max(0, event.attempt - 1),
|
|
382
|
+
retryEnabled: true,
|
|
383
|
+
retryDelayMs: event.retryDelayMs,
|
|
384
|
+
failurePolicy: event.failurePolicy,
|
|
385
|
+
errors: event.errors,
|
|
386
|
+
errorMessage: event.errorMessage,
|
|
387
|
+
inputData: event.inputData ?? current.inputData,
|
|
388
|
+
incomingEdgeId: event.incomingEdgeId ?? current.incomingEdgeId,
|
|
389
|
+
startedAt: current.startedAt ?? event.at,
|
|
390
|
+
});
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (event.type === "step:condition") {
|
|
394
|
+
records.set(event.stepId, {
|
|
395
|
+
...current,
|
|
396
|
+
title: event.title,
|
|
397
|
+
conditionExpression: event.conditionExpression,
|
|
398
|
+
conditionPreview: event.conditionPreview,
|
|
399
|
+
conditionMode: event.conditionMode,
|
|
400
|
+
conditionResult: event.result,
|
|
401
|
+
conditionDebug: event.conditionDebug,
|
|
402
|
+
activeBranch: event.activeBranch,
|
|
403
|
+
});
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (event.type === "step:skip") {
|
|
407
|
+
records.set(event.stepId, {
|
|
408
|
+
...current,
|
|
409
|
+
stepType: event.stepType,
|
|
410
|
+
title: event.title,
|
|
411
|
+
status: "skipped",
|
|
412
|
+
meta: event.nodeMeta ?? current.meta,
|
|
413
|
+
inputData: event.inputData ?? current.inputData,
|
|
414
|
+
outputData: event.outputData ?? current.outputData,
|
|
415
|
+
incomingEdgeId: event.incomingEdgeId ?? current.incomingEdgeId,
|
|
416
|
+
durationMs: event.durationMs ?? current.durationMs,
|
|
417
|
+
startedAt: current.startedAt ?? event.at,
|
|
418
|
+
completedAt: event.at,
|
|
419
|
+
});
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const isSuccess = event.type === "step:success";
|
|
423
|
+
records.set(event.stepId, {
|
|
424
|
+
...current,
|
|
425
|
+
stepType: event.stepType,
|
|
426
|
+
title: event.title,
|
|
427
|
+
status: event.status ?? (isSuccess ? "success" : "failed"),
|
|
428
|
+
meta: event.nodeMeta ?? current.meta,
|
|
429
|
+
attempts: event.attempts ?? current.attempts,
|
|
430
|
+
currentAttempt: event.currentAttempt ?? current.currentAttempt,
|
|
431
|
+
maxAttempts: event.maxAttempts ?? current.maxAttempts,
|
|
432
|
+
retryCount: event.retryCount ?? current.retryCount,
|
|
433
|
+
retryEnabled: event.retryEnabled ?? current.retryEnabled,
|
|
434
|
+
retryDelayMs: event.retryDelayMs ?? current.retryDelayMs,
|
|
435
|
+
failurePolicy: event.failurePolicy ?? current.failurePolicy,
|
|
436
|
+
continuedAfterFailure: event.continuedAfterFailure ?? current.continuedAfterFailure,
|
|
437
|
+
errors: event.errors ?? current.errors,
|
|
438
|
+
durationMs: event.durationMs ?? current.durationMs,
|
|
439
|
+
statusCode: event.statusCode ?? current.statusCode,
|
|
440
|
+
errorMessage: event.type === "step:fail"
|
|
441
|
+
? event.errorMessage
|
|
442
|
+
: current.errorMessage,
|
|
443
|
+
response: event.response ?? current.response,
|
|
444
|
+
testResults: event.testResults ?? current.testResults,
|
|
445
|
+
contractResults: event.contractResults ?? current.contractResults,
|
|
446
|
+
contractSummary: event.contractSummary ?? current.contractSummary,
|
|
447
|
+
contractValidationError: event.contractValidationError ?? current.contractValidationError,
|
|
448
|
+
dbAssertionResults: event.dbAssertionResults ?? current.dbAssertionResults,
|
|
449
|
+
redisAssertionResults: event.redisAssertionResults ?? current.redisAssertionResults,
|
|
450
|
+
scriptSource: event.scriptSource ?? current.scriptSource,
|
|
451
|
+
scriptTimeoutMs: event.scriptTimeoutMs ?? current.scriptTimeoutMs,
|
|
452
|
+
scriptLogs: event.scriptLogs ?? current.scriptLogs,
|
|
453
|
+
conditionExpression: event.conditionExpression ?? current.conditionExpression,
|
|
454
|
+
conditionPreview: event.conditionPreview ?? current.conditionPreview,
|
|
455
|
+
conditionMode: event.conditionMode ?? current.conditionMode,
|
|
456
|
+
conditionResult: event.conditionResult ?? current.conditionResult,
|
|
457
|
+
conditionDebug: event.conditionDebug ?? current.conditionDebug,
|
|
458
|
+
activeBranch: event.activeBranch ?? current.activeBranch,
|
|
459
|
+
inputData: event.inputData ?? current.inputData,
|
|
460
|
+
outputData: event.outputData ?? current.outputData,
|
|
461
|
+
incomingEdgeId: event.incomingEdgeId ?? current.incomingEdgeId,
|
|
462
|
+
startedAt: current.startedAt ?? event.at,
|
|
463
|
+
completedAt: event.at,
|
|
464
|
+
});
|
|
465
|
+
};
|
|
466
|
+
const callHandler = async (handlers, event) => {
|
|
467
|
+
await handlers?.onEvent?.(event);
|
|
468
|
+
switch (event.type) {
|
|
469
|
+
case "scenario:start":
|
|
470
|
+
await handlers?.onScenarioStart?.(event);
|
|
471
|
+
return;
|
|
472
|
+
case "scenario:end":
|
|
473
|
+
return;
|
|
474
|
+
case "step:start":
|
|
475
|
+
await handlers?.onNodeStart?.(event);
|
|
476
|
+
return;
|
|
477
|
+
case "step:retry":
|
|
478
|
+
await handlers?.onNodeRetry?.(event);
|
|
479
|
+
return;
|
|
480
|
+
case "step:success":
|
|
481
|
+
await handlers?.onNodeSuccess?.(event);
|
|
482
|
+
return;
|
|
483
|
+
case "step:fail":
|
|
484
|
+
await handlers?.onNodeFail?.(event);
|
|
485
|
+
return;
|
|
486
|
+
case "step:skip":
|
|
487
|
+
await handlers?.onNodeSkip?.(event);
|
|
488
|
+
return;
|
|
489
|
+
case "step:reset":
|
|
490
|
+
await handlers?.onNodeReset?.(event);
|
|
491
|
+
return;
|
|
492
|
+
case "step:condition":
|
|
493
|
+
await handlers?.onConditionEvaluated?.(event);
|
|
494
|
+
return;
|
|
495
|
+
default:
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
const runScenario = async ({ scenario, environment = {}, globals = {}, runtimeAdapters, eventHandlers, signal, minNodeVisualStateMs = DEFAULT_MIN_NODE_VISUAL_STATE_MS, }) => {
|
|
500
|
+
const runId = createRunId();
|
|
501
|
+
const startedAtMs = Date.now();
|
|
502
|
+
const startedAt = new Date(startedAtMs).toISOString();
|
|
503
|
+
const graph = (0, templateResolver_js_1.buildScenarioExecutionGraph)(scenario.nodes, scenario.edges);
|
|
504
|
+
const orderedStartNodeIds = graph.startNodeIds;
|
|
505
|
+
const runtimeStepDescriptors = (0, graphTraversal_js_1.buildRuntimeStepDescriptors)(scenario.nodes);
|
|
506
|
+
const runtimeStepIds = runtimeStepDescriptors.map((entry) => entry.stepId);
|
|
507
|
+
const localStatuses = new Map(runtimeStepIds.map((stepId) => [stepId, "idle"]));
|
|
508
|
+
const records = new Map();
|
|
509
|
+
const events = [];
|
|
510
|
+
const initialWorkflowMeta = createInitialWorkflowMeta({
|
|
511
|
+
startedAt,
|
|
512
|
+
totalNodes: runtimeStepIds.length,
|
|
513
|
+
});
|
|
514
|
+
const runtimeContext = {
|
|
515
|
+
environment: { ...environment },
|
|
516
|
+
globals: { ...globals },
|
|
517
|
+
workflowContext: {
|
|
518
|
+
workflow: buildWorkflowRoot({
|
|
519
|
+
scenario,
|
|
520
|
+
meta: initialWorkflowMeta,
|
|
521
|
+
}),
|
|
522
|
+
},
|
|
523
|
+
nodeMetaByLabel: {},
|
|
524
|
+
workflowMeta: initialWorkflowMeta,
|
|
525
|
+
responseRoot: null,
|
|
526
|
+
lastResponse: null,
|
|
527
|
+
dbResult: null,
|
|
528
|
+
kafkaEvent: null,
|
|
529
|
+
kafkaCapture: null,
|
|
530
|
+
redisResult: null,
|
|
531
|
+
};
|
|
532
|
+
const prefetchedKafkaResults = new Map();
|
|
533
|
+
const degradedStepIds = new Set();
|
|
534
|
+
const loopIterationCounts = new Map();
|
|
535
|
+
const rootScope = {
|
|
536
|
+
graph,
|
|
537
|
+
stepIdForNode: (nodeId) => nodeId,
|
|
538
|
+
stepIdForEdge: (edgeId) => edgeId,
|
|
539
|
+
};
|
|
540
|
+
const emit = async (event) => {
|
|
541
|
+
events.push(event);
|
|
542
|
+
updateRecordFromEvent(records, event);
|
|
543
|
+
await callHandler(eventHandlers, event);
|
|
544
|
+
};
|
|
545
|
+
const waitForDuration = async (delayMs) => {
|
|
546
|
+
const normalizedDelayMs = Math.max(0, Math.round(delayMs));
|
|
547
|
+
if (normalizedDelayMs === 0) {
|
|
548
|
+
throwIfStopped(signal);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
await new Promise((resolve, reject) => {
|
|
552
|
+
const timeoutId = globalThis.setTimeout(() => {
|
|
553
|
+
signal?.removeEventListener?.("abort", onAbort);
|
|
554
|
+
resolve();
|
|
555
|
+
}, normalizedDelayMs);
|
|
556
|
+
const onAbort = () => {
|
|
557
|
+
globalThis.clearTimeout(timeoutId);
|
|
558
|
+
signal?.removeEventListener?.("abort", onAbort);
|
|
559
|
+
reject(new ScenarioStopError());
|
|
560
|
+
};
|
|
561
|
+
if (signal?.aborted) {
|
|
562
|
+
onAbort();
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
signal?.addEventListener?.("abort", onAbort, { once: true });
|
|
566
|
+
});
|
|
567
|
+
};
|
|
568
|
+
const ensureMinimumVisualDuration = async (attemptStartedAtMs) => {
|
|
569
|
+
const elapsedMs = Date.now() - attemptStartedAtMs;
|
|
570
|
+
if (elapsedMs >= minNodeVisualStateMs) {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
await waitForDuration(minNodeVisualStateMs - elapsedMs);
|
|
574
|
+
};
|
|
575
|
+
const markNodeIdsSkipped = async (scope, nodeIds, reason) => {
|
|
576
|
+
for (const skippedNodeId of nodeIds) {
|
|
577
|
+
const skippedNode = scope.graph.nodeMap.get(skippedNodeId);
|
|
578
|
+
if (!skippedNode) {
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
const stepId = scope.stepIdForNode(skippedNodeId);
|
|
582
|
+
const currentStatus = localStatuses.get(stepId) ?? "idle";
|
|
583
|
+
if (currentStatus !== "idle") {
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
localStatuses.set(stepId, "skipped");
|
|
587
|
+
const skippedAt = new Date().toISOString();
|
|
588
|
+
const nodeMeta = buildNodeExecutionMeta({
|
|
589
|
+
status: "skipped",
|
|
590
|
+
attempts: 0,
|
|
591
|
+
startedAt: skippedAt,
|
|
592
|
+
endedAt: skippedAt,
|
|
593
|
+
durationMs: 0,
|
|
594
|
+
});
|
|
595
|
+
persistNodeExecutionMeta({
|
|
596
|
+
context: runtimeContext,
|
|
597
|
+
label: skippedNode.data.label.trim(),
|
|
598
|
+
output: null,
|
|
599
|
+
meta: nodeMeta,
|
|
600
|
+
});
|
|
601
|
+
await emit({
|
|
602
|
+
type: "step:skip",
|
|
603
|
+
runId,
|
|
604
|
+
scenarioId: scenario.id,
|
|
605
|
+
stepId,
|
|
606
|
+
stepType: skippedNode.type,
|
|
607
|
+
title: buildTitle(skippedNode),
|
|
608
|
+
at: skippedAt,
|
|
609
|
+
reason,
|
|
610
|
+
nodeMeta,
|
|
611
|
+
inputData: createInputSnapshot(runtimeContext),
|
|
612
|
+
});
|
|
613
|
+
updateWorkflowMetaState({
|
|
614
|
+
context: runtimeContext,
|
|
615
|
+
records,
|
|
616
|
+
localStatuses,
|
|
617
|
+
startedAt,
|
|
618
|
+
currentNode: null,
|
|
619
|
+
lastNode: buildTitle(skippedNode),
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
const markRemainingNodesSkipped = async (reason) => {
|
|
624
|
+
await markNodeIdsSkipped(rootScope, rootScope.graph.nodeMap.keys(), reason);
|
|
625
|
+
};
|
|
626
|
+
const markScopeRemainingNodesSkipped = async (scope, reason) => {
|
|
627
|
+
await markNodeIdsSkipped(scope, scope.graph.nodeMap.keys(), reason);
|
|
628
|
+
};
|
|
629
|
+
const isNodeReady = (scope, nodeId) => {
|
|
630
|
+
const incomingEdges = scope.graph.incomingMap.get(nodeId) ?? [];
|
|
631
|
+
if (incomingEdges.length === 0) {
|
|
632
|
+
return true;
|
|
633
|
+
}
|
|
634
|
+
return incomingEdges.every((edge) => {
|
|
635
|
+
const sourceStatus = localStatuses.get(scope.stepIdForNode(edge.source)) ?? "idle";
|
|
636
|
+
return (sourceStatus === "success" ||
|
|
637
|
+
sourceStatus === "skipped" ||
|
|
638
|
+
sourceStatus === "failed_continued" ||
|
|
639
|
+
sourceStatus === "partial_success");
|
|
640
|
+
});
|
|
641
|
+
};
|
|
642
|
+
const resetReachableNodesForLoop = (scope, targetNodeId) => {
|
|
643
|
+
const stack = [targetNodeId];
|
|
644
|
+
const visited = new Set();
|
|
645
|
+
while (stack.length > 0) {
|
|
646
|
+
const currentNodeId = stack.pop();
|
|
647
|
+
if (!currentNodeId || visited.has(currentNodeId)) {
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
visited.add(currentNodeId);
|
|
651
|
+
if (scope.graph.nodeMap.has(currentNodeId)) {
|
|
652
|
+
localStatuses.set(scope.stepIdForNode(currentNodeId), "idle");
|
|
653
|
+
}
|
|
654
|
+
for (const edge of scope.graph.outgoingMap.get(currentNodeId) ?? []) {
|
|
655
|
+
stack.push(edge.target);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return Array.from(visited).map((nodeId) => scope.stepIdForNode(nodeId));
|
|
659
|
+
};
|
|
660
|
+
const executeNode = async (nodeId, incomingEdgeId, options = {}, scope = rootScope) => {
|
|
661
|
+
throwIfStopped(signal);
|
|
662
|
+
const stepId = scope.stepIdForNode(nodeId);
|
|
663
|
+
const currentStatus = localStatuses.get(stepId) ?? "idle";
|
|
664
|
+
if (currentStatus !== "idle") {
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
if (!options.force && !isNodeReady(scope, nodeId)) {
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
const node = scope.graph.nodeMap.get(nodeId);
|
|
671
|
+
if (!node) {
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
const title = buildTitle(node);
|
|
675
|
+
const nodeLabel = node.data.label.trim();
|
|
676
|
+
const executionPolicy = (0, nodePolicy_js_1.supportsScenarioNodeExecutionPolicy)(node)
|
|
677
|
+
? withNotificationFailurePolicyOverride(node, (0, nodePolicy_js_1.getScenarioNodeExecutionPolicy)(node))
|
|
678
|
+
: (0, nodePolicy_js_1.getLegacyScenarioNodeExecutionPolicy)();
|
|
679
|
+
const executionStartedAtMs = Date.now();
|
|
680
|
+
let inputData = createInputSnapshot(runtimeContext);
|
|
681
|
+
let statusCode = null;
|
|
682
|
+
let response = null;
|
|
683
|
+
let outputData = null;
|
|
684
|
+
let testResults = [];
|
|
685
|
+
let contractResults = [];
|
|
686
|
+
let contractSummary = null;
|
|
687
|
+
let contractValidationError = null;
|
|
688
|
+
let dbAssertionResults = [];
|
|
689
|
+
let redisAssertionResults = [];
|
|
690
|
+
let scriptSource = null;
|
|
691
|
+
let scriptTimeoutMs = null;
|
|
692
|
+
let scriptLogs = [];
|
|
693
|
+
let scriptHasReturnValue = false;
|
|
694
|
+
let scriptReturnValue = null;
|
|
695
|
+
let conditionResult = null;
|
|
696
|
+
let activeBranch = null;
|
|
697
|
+
let conditionExpression = null;
|
|
698
|
+
let conditionPreview = null;
|
|
699
|
+
let conditionMode = null;
|
|
700
|
+
let conditionDebug = null;
|
|
701
|
+
let shouldMarkPartialSuccess = false;
|
|
702
|
+
let workflowOutput = undefined;
|
|
703
|
+
const resetAttemptArtifacts = () => {
|
|
704
|
+
statusCode = null;
|
|
705
|
+
response = null;
|
|
706
|
+
outputData = null;
|
|
707
|
+
testResults = [];
|
|
708
|
+
contractResults = [];
|
|
709
|
+
contractSummary = null;
|
|
710
|
+
contractValidationError = null;
|
|
711
|
+
dbAssertionResults = [];
|
|
712
|
+
redisAssertionResults = [];
|
|
713
|
+
scriptSource = null;
|
|
714
|
+
scriptTimeoutMs = null;
|
|
715
|
+
scriptLogs = [];
|
|
716
|
+
scriptHasReturnValue = false;
|
|
717
|
+
scriptReturnValue = null;
|
|
718
|
+
conditionResult = null;
|
|
719
|
+
activeBranch = null;
|
|
720
|
+
conditionExpression = null;
|
|
721
|
+
conditionPreview = null;
|
|
722
|
+
conditionMode = null;
|
|
723
|
+
conditionDebug = null;
|
|
724
|
+
shouldMarkPartialSuccess = false;
|
|
725
|
+
workflowOutput = undefined;
|
|
726
|
+
};
|
|
727
|
+
const executeNodeAttempt = async (attempt, maxAttempts) => {
|
|
728
|
+
throwIfStopped(signal);
|
|
729
|
+
resetAttemptArtifacts();
|
|
730
|
+
const attemptStartedAtMs = Date.now();
|
|
731
|
+
inputData = createInputSnapshot(runtimeContext);
|
|
732
|
+
localStatuses.set(stepId, "running");
|
|
733
|
+
await emit({
|
|
734
|
+
type: "step:start",
|
|
735
|
+
runId,
|
|
736
|
+
scenarioId: scenario.id,
|
|
737
|
+
stepId,
|
|
738
|
+
stepType: node.type,
|
|
739
|
+
title,
|
|
740
|
+
at: new Date().toISOString(),
|
|
741
|
+
attempt,
|
|
742
|
+
maxAttempts,
|
|
743
|
+
retryEnabled: executionPolicy.retry.enabled,
|
|
744
|
+
retryDelayMs: executionPolicy.retry.delayMs,
|
|
745
|
+
failurePolicy: executionPolicy.onFailure,
|
|
746
|
+
inputData,
|
|
747
|
+
incomingEdgeId: incomingEdgeId ?? null,
|
|
748
|
+
});
|
|
749
|
+
updateWorkflowMetaState({
|
|
750
|
+
context: runtimeContext,
|
|
751
|
+
records,
|
|
752
|
+
localStatuses,
|
|
753
|
+
startedAt,
|
|
754
|
+
currentNode: title,
|
|
755
|
+
});
|
|
756
|
+
const templateValues = (0, templateResolver_js_1.buildScenarioTemplateValues)({
|
|
757
|
+
environment: runtimeContext.environment,
|
|
758
|
+
globals: runtimeContext.globals,
|
|
759
|
+
workflowContext: runtimeContext.workflowContext,
|
|
760
|
+
nodeMetaByLabel: runtimeContext.nodeMetaByLabel,
|
|
761
|
+
responseRoot: runtimeContext.responseRoot,
|
|
762
|
+
dbResult: runtimeContext.dbResult,
|
|
763
|
+
kafkaEvent: runtimeContext.kafkaEvent,
|
|
764
|
+
redisResult: runtimeContext.redisResult,
|
|
765
|
+
});
|
|
766
|
+
try {
|
|
767
|
+
if (node.type === "request") {
|
|
768
|
+
const kafkaCapturePlans = (scope.graph.outgoingMap.get(nodeId) ?? [])
|
|
769
|
+
.map((edge) => scope.graph.nodeMap.get(edge.target))
|
|
770
|
+
.filter(isKafkaNode)
|
|
771
|
+
.map((kafkaNode) => {
|
|
772
|
+
const refreshedTemplateValues = (0, templateResolver_js_1.buildScenarioTemplateValues)({
|
|
773
|
+
environment: runtimeContext.environment,
|
|
774
|
+
globals: runtimeContext.globals,
|
|
775
|
+
workflowContext: runtimeContext.workflowContext,
|
|
776
|
+
nodeMetaByLabel: runtimeContext.nodeMetaByLabel,
|
|
777
|
+
responseRoot: runtimeContext.responseRoot,
|
|
778
|
+
dbResult: runtimeContext.dbResult,
|
|
779
|
+
kafkaEvent: runtimeContext.kafkaEvent,
|
|
780
|
+
redisResult: runtimeContext.redisResult,
|
|
781
|
+
});
|
|
782
|
+
return {
|
|
783
|
+
stepId: scope.stepIdForNode(kafkaNode.id),
|
|
784
|
+
connectionId: kafkaNode.data.config.connectionId || null,
|
|
785
|
+
topic: (0, templateResolver_js_1.resolveScenarioTemplateString)(kafkaNode.data.config.topic, refreshedTemplateValues).trim(),
|
|
786
|
+
};
|
|
787
|
+
})
|
|
788
|
+
.filter((entry) => entry.connectionId && entry.topic);
|
|
789
|
+
const requestResult = await runtimeAdapters.requestExecutor({
|
|
790
|
+
runId,
|
|
791
|
+
stepId,
|
|
792
|
+
node,
|
|
793
|
+
context: createContextSnapshot(runtimeContext),
|
|
794
|
+
kafkaCapturePlans,
|
|
795
|
+
signal,
|
|
796
|
+
});
|
|
797
|
+
if (requestResult.cancelled) {
|
|
798
|
+
throw new ScenarioStopError();
|
|
799
|
+
}
|
|
800
|
+
runtimeContext.environment = { ...requestResult.environment };
|
|
801
|
+
runtimeContext.globals = { ...requestResult.globals };
|
|
802
|
+
runtimeContext.lastResponse = requestResult.response;
|
|
803
|
+
runtimeContext.responseRoot = requestResult.responseRoot;
|
|
804
|
+
response = requestResult.response;
|
|
805
|
+
statusCode = requestResult.response.status;
|
|
806
|
+
outputData = requestResult.outputData;
|
|
807
|
+
testResults = requestResult.testResults;
|
|
808
|
+
contractResults = requestResult.contractResults;
|
|
809
|
+
contractSummary = requestResult.contractSummary;
|
|
810
|
+
contractValidationError = requestResult.contractValidationError;
|
|
811
|
+
dbAssertionResults = requestResult.dbAssertionResults;
|
|
812
|
+
redisAssertionResults = requestResult.redisAssertionResults;
|
|
813
|
+
for (const entry of requestResult.prefetchedKafkaResults ?? []) {
|
|
814
|
+
prefetchedKafkaResults.set(entry.stepId, entry.result);
|
|
815
|
+
}
|
|
816
|
+
if (requestResult.failureMessage) {
|
|
817
|
+
throw new ScenarioNodeFailureError(requestResult.failureMessage);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
if (node.type === "db") {
|
|
821
|
+
const resolvedQuery = (0, templateResolver_js_1.resolveScenarioTemplateString)(node.data.config.query, templateValues);
|
|
822
|
+
const dbResult = await runtimeAdapters.dbExecutor({
|
|
823
|
+
runId,
|
|
824
|
+
stepId,
|
|
825
|
+
node,
|
|
826
|
+
resolvedQuery,
|
|
827
|
+
context: createContextSnapshot(runtimeContext),
|
|
828
|
+
signal,
|
|
829
|
+
});
|
|
830
|
+
runtimeContext.dbResult = dbResult.dbResult;
|
|
831
|
+
outputData = dbResult.outputData;
|
|
832
|
+
if (dbResult.failureMessage) {
|
|
833
|
+
throw new ScenarioNodeFailureError(dbResult.failureMessage);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (node.type === "wait") {
|
|
837
|
+
try {
|
|
838
|
+
outputData = await (0, waitNodeExecutor_js_1.executeWaitNode)({
|
|
839
|
+
durationMs: node.data.config.durationMs,
|
|
840
|
+
signal,
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
catch (error) {
|
|
844
|
+
if (error instanceof waitNodeExecutor_js_1.ScenarioWaitNodeCancelledError) {
|
|
845
|
+
outputData = error.summary;
|
|
846
|
+
throw new ScenarioStopError();
|
|
847
|
+
}
|
|
848
|
+
if (error instanceof waitNodeExecutor_js_1.ScenarioWaitNodeValidationError) {
|
|
849
|
+
outputData = error.summary;
|
|
850
|
+
throw new ScenarioNodeFailureError(error.message);
|
|
851
|
+
}
|
|
852
|
+
throw error;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
if (node.type === "script") {
|
|
856
|
+
const scriptResult = await runtimeAdapters.scriptExecutor({
|
|
857
|
+
runId,
|
|
858
|
+
stepId,
|
|
859
|
+
node,
|
|
860
|
+
context: createContextSnapshot(runtimeContext),
|
|
861
|
+
signal,
|
|
862
|
+
});
|
|
863
|
+
scriptSource = scriptResult.scriptSource;
|
|
864
|
+
scriptTimeoutMs = scriptResult.timeoutMs;
|
|
865
|
+
scriptLogs = scriptResult.logs;
|
|
866
|
+
testResults = scriptResult.testResults;
|
|
867
|
+
scriptHasReturnValue = scriptResult.hasReturnValue;
|
|
868
|
+
scriptReturnValue = scriptResult.returnValue;
|
|
869
|
+
outputData = scriptResult.outputData;
|
|
870
|
+
applyVariableMutationsToContext(runtimeContext, scriptResult.variableMutations);
|
|
871
|
+
if (scriptResult.failureMessage) {
|
|
872
|
+
throw new ScenarioNodeFailureError(scriptResult.failureMessage);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
if (node.type === "redis") {
|
|
876
|
+
const resolvedCommands = node.data.config.commands
|
|
877
|
+
.map((command) => (0, templateResolver_js_1.resolveScenarioTemplateString)(command, templateValues))
|
|
878
|
+
.filter((command) => command.trim().length > 0);
|
|
879
|
+
const redisResult = await runtimeAdapters.redisExecutor({
|
|
880
|
+
runId,
|
|
881
|
+
stepId,
|
|
882
|
+
node,
|
|
883
|
+
resolvedCommands,
|
|
884
|
+
context: createContextSnapshot(runtimeContext),
|
|
885
|
+
signal,
|
|
886
|
+
});
|
|
887
|
+
runtimeContext.redisResult = redisResult.redisResult;
|
|
888
|
+
outputData = redisResult.outputData;
|
|
889
|
+
if (redisResult.failureMessage) {
|
|
890
|
+
throw new ScenarioNodeFailureError(redisResult.failureMessage);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
if (node.type === "notification") {
|
|
894
|
+
const resolvedMessage = (0, index_js_1.renderNotificationTemplate)(node.data.config.messageTemplate, templateValues).trim();
|
|
895
|
+
const resolvedChannelOverride = node.data.config.channelOverride?.trim()
|
|
896
|
+
? (0, index_js_1.renderNotificationTemplate)(node.data.config.channelOverride, templateValues).trim()
|
|
897
|
+
: null;
|
|
898
|
+
const workflowRoot = isRecord(runtimeContext.workflowContext.workflow)
|
|
899
|
+
? runtimeContext.workflowContext.workflow
|
|
900
|
+
: {};
|
|
901
|
+
const deliveryRequest = (0, index_js_1.buildNotificationDeliveryRequest)({
|
|
902
|
+
provider: node.data.config.provider,
|
|
903
|
+
scenario: {
|
|
904
|
+
id: scenario.id,
|
|
905
|
+
name: scenario.name,
|
|
906
|
+
...(typeof scenario.version === "number"
|
|
907
|
+
? { version: scenario.version }
|
|
908
|
+
: {}),
|
|
909
|
+
},
|
|
910
|
+
nodeLabel: title,
|
|
911
|
+
severity: normalizeNotificationSeverity(node.data.config.severity),
|
|
912
|
+
message: resolvedMessage || "N/A",
|
|
913
|
+
channelOverride: resolvedChannelOverride,
|
|
914
|
+
triggerLabel: "Scenario Run",
|
|
915
|
+
workflowMeta: runtimeContext.workflowMeta,
|
|
916
|
+
useRichBlocks: node.data.config.advanced?.useRichBlocks !== false,
|
|
917
|
+
});
|
|
918
|
+
const notificationResult = await runtimeAdapters.notificationExecutor({
|
|
919
|
+
runId,
|
|
920
|
+
stepId,
|
|
921
|
+
node,
|
|
922
|
+
context: createContextSnapshot(runtimeContext),
|
|
923
|
+
deliveryRequest,
|
|
924
|
+
signal,
|
|
925
|
+
});
|
|
926
|
+
outputData = notificationResult.outputData;
|
|
927
|
+
if (!notificationResult.delivered || notificationResult.failureMessage) {
|
|
928
|
+
throw new ScenarioNodeFailureError(notificationResult.failureMessage ?? "Notification delivery failed.");
|
|
929
|
+
}
|
|
930
|
+
runtimeContext.workflowContext.workflow = {
|
|
931
|
+
...workflowRoot,
|
|
932
|
+
__meta: cloneWorkflowExecutionMeta(runtimeContext.workflowMeta),
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
if (node.type === "kafka") {
|
|
936
|
+
const resolvedTopic = (0, templateResolver_js_1.resolveScenarioTemplateString)(node.data.config.topic, templateValues);
|
|
937
|
+
const rules = node.data.config.rules?.filter((rule) => rule.trim()) ?? [];
|
|
938
|
+
const prefetchedCaptureResult = prefetchedKafkaResults.get(stepId);
|
|
939
|
+
const captureResult = prefetchedCaptureResult ??
|
|
940
|
+
(await runtimeAdapters.kafkaExecutor({
|
|
941
|
+
runId,
|
|
942
|
+
stepId,
|
|
943
|
+
node,
|
|
944
|
+
resolvedTopic,
|
|
945
|
+
matcher: rules.length > 0
|
|
946
|
+
? {
|
|
947
|
+
mode: node.data.config.matchMode,
|
|
948
|
+
rules,
|
|
949
|
+
templateValues,
|
|
950
|
+
}
|
|
951
|
+
: null,
|
|
952
|
+
signal,
|
|
953
|
+
})).captureResult;
|
|
954
|
+
throwIfStopped(signal);
|
|
955
|
+
let matchedMessage = captureResult.messages[0] ?? null;
|
|
956
|
+
if (node.data.config.matchMode === "regex" && rules[0]) {
|
|
957
|
+
const regex = buildScenarioRegex(rules[0]);
|
|
958
|
+
matchedMessage =
|
|
959
|
+
captureResult.messages.find((message) => {
|
|
960
|
+
regex.lastIndex = 0;
|
|
961
|
+
return regex.test(safeStringifyKafkaMessage(message));
|
|
962
|
+
}) ?? null;
|
|
963
|
+
}
|
|
964
|
+
if (node.data.config.matchMode === "rules" && rules.length > 0) {
|
|
965
|
+
matchedMessage =
|
|
966
|
+
captureResult.messages.find((message) => {
|
|
967
|
+
const kafkaEventValue = buildKafkaEventValue(message);
|
|
968
|
+
const scopedValues = (0, templateResolver_js_1.buildScenarioTemplateValues)({
|
|
969
|
+
environment: runtimeContext.environment,
|
|
970
|
+
globals: runtimeContext.globals,
|
|
971
|
+
workflowContext: runtimeContext.workflowContext,
|
|
972
|
+
nodeMetaByLabel: runtimeContext.nodeMetaByLabel,
|
|
973
|
+
responseRoot: runtimeContext.responseRoot,
|
|
974
|
+
dbResult: runtimeContext.dbResult,
|
|
975
|
+
kafkaEvent: kafkaEventValue,
|
|
976
|
+
redisResult: runtimeContext.redisResult,
|
|
977
|
+
});
|
|
978
|
+
return rules.every((rule) => (0, templateResolver_js_1.evaluateScenarioExpression)(rule, scopedValues).result);
|
|
979
|
+
}) ?? null;
|
|
980
|
+
}
|
|
981
|
+
if (!matchedMessage) {
|
|
982
|
+
outputData = captureResult;
|
|
983
|
+
throw new ScenarioNodeFailureError(buildKafkaFailureMessage(captureResult, Boolean(rules.length)));
|
|
984
|
+
}
|
|
985
|
+
runtimeContext.kafkaCapture = captureResult;
|
|
986
|
+
runtimeContext.kafkaEvent = buildKafkaEventValue(matchedMessage);
|
|
987
|
+
outputData = runtimeContext.kafkaEvent;
|
|
988
|
+
}
|
|
989
|
+
if (node.type === "condition") {
|
|
990
|
+
const conditionIssues = (0, condition_js_1.validateConditionConfig)(node.data.config);
|
|
991
|
+
const blockingConditionIssue = conditionIssues.find((issue) => issue.severity === "error");
|
|
992
|
+
if (blockingConditionIssue) {
|
|
993
|
+
throw new ScenarioNodeFailureError(blockingConditionIssue.message);
|
|
994
|
+
}
|
|
995
|
+
const evaluation = (0, condition_js_1.evaluateConditionConfig)(node.data.config, {
|
|
996
|
+
environment: runtimeContext.environment,
|
|
997
|
+
globals: runtimeContext.globals,
|
|
998
|
+
workflowContext: runtimeContext.workflowContext,
|
|
999
|
+
nodeMetaByLabel: runtimeContext.nodeMetaByLabel,
|
|
1000
|
+
responseRoot: runtimeContext.responseRoot,
|
|
1001
|
+
dbResult: runtimeContext.dbResult,
|
|
1002
|
+
kafkaEvent: runtimeContext.kafkaEvent,
|
|
1003
|
+
redisResult: runtimeContext.redisResult,
|
|
1004
|
+
});
|
|
1005
|
+
const conditionPayload = (0, condition_js_1.buildConditionOutputPayload)(node.data.config, evaluation);
|
|
1006
|
+
conditionResult = evaluation.result;
|
|
1007
|
+
activeBranch = evaluation.result ? "if" : "else";
|
|
1008
|
+
conditionExpression = conditionPayload.expression;
|
|
1009
|
+
conditionPreview = conditionPayload.preview;
|
|
1010
|
+
conditionMode = conditionPayload.mode;
|
|
1011
|
+
conditionDebug = conditionPayload.debug;
|
|
1012
|
+
outputData = conditionPayload;
|
|
1013
|
+
await emit({
|
|
1014
|
+
type: "step:condition",
|
|
1015
|
+
runId,
|
|
1016
|
+
scenarioId: scenario.id,
|
|
1017
|
+
stepId,
|
|
1018
|
+
title,
|
|
1019
|
+
at: new Date().toISOString(),
|
|
1020
|
+
conditionExpression: conditionPayload.expression,
|
|
1021
|
+
conditionPreview: conditionPayload.preview,
|
|
1022
|
+
conditionMode: conditionPayload.mode,
|
|
1023
|
+
conditionDebug: conditionPayload.debug,
|
|
1024
|
+
result: evaluation.result,
|
|
1025
|
+
activeBranch,
|
|
1026
|
+
});
|
|
1027
|
+
const inactiveBranch = evaluation.result ? "no" : "yes";
|
|
1028
|
+
const inactiveBranchNodeIds = (0, templateResolver_js_1.collectExclusiveBranchNodeIds)({
|
|
1029
|
+
graph: scope.graph,
|
|
1030
|
+
conditionNodeId: node.id,
|
|
1031
|
+
inactiveBranch,
|
|
1032
|
+
});
|
|
1033
|
+
await markNodeIdsSkipped(scope, inactiveBranchNodeIds, `Skipped because condition evaluated ${evaluation.result ? "TRUE" : "FALSE"}.`);
|
|
1034
|
+
}
|
|
1035
|
+
if (node.type === "subscenario") {
|
|
1036
|
+
const degradedCountBefore = degradedStepIds.size;
|
|
1037
|
+
const internalGraph = (0, templateResolver_js_1.buildScenarioExecutionGraph)(node.data.config.nodes, node.data.config.edges);
|
|
1038
|
+
const internalScope = {
|
|
1039
|
+
graph: internalGraph,
|
|
1040
|
+
stepIdForNode: (childNodeId) => (0, graphTraversal_js_1.createSubScenarioStepId)(stepId, childNodeId),
|
|
1041
|
+
stepIdForEdge: (edgeId) => (0, graphTraversal_js_1.createSubScenarioStepId)(stepId, edgeId),
|
|
1042
|
+
};
|
|
1043
|
+
for (const startNodeId of internalGraph.startNodeIds) {
|
|
1044
|
+
throwIfStopped(signal);
|
|
1045
|
+
await executeNode(startNodeId, null, {}, internalScope);
|
|
1046
|
+
}
|
|
1047
|
+
await markScopeRemainingNodesSkipped(internalScope, `Internal node was not reached inside ${title}.`);
|
|
1048
|
+
const groupedOutput = {};
|
|
1049
|
+
for (const childNode of (0, graphTraversal_js_1.flattenScenarioNodesForContext)(node.data.config.nodes)) {
|
|
1050
|
+
const childLabel = childNode.data.label.trim();
|
|
1051
|
+
if (childLabel &&
|
|
1052
|
+
Object.prototype.hasOwnProperty.call(runtimeContext.workflowContext, childLabel)) {
|
|
1053
|
+
groupedOutput[childLabel] = runtimeContext.workflowContext[childLabel];
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
shouldMarkPartialSuccess = degradedStepIds.size > degradedCountBefore;
|
|
1057
|
+
outputData = groupedOutput;
|
|
1058
|
+
}
|
|
1059
|
+
if (node.type === "end") {
|
|
1060
|
+
if (node.data.config.loop && node.data.config.loopTargetNodeId) {
|
|
1061
|
+
const loopTargetNode = scope.graph.nodeMap.get(node.data.config.loopTargetNodeId);
|
|
1062
|
+
if (!loopTargetNode) {
|
|
1063
|
+
throw new ScenarioNodeFailureError("Loop target node could not be found.");
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
outputData = { ...runtimeContext.workflowContext };
|
|
1067
|
+
}
|
|
1068
|
+
if (node.type === "aggregator") {
|
|
1069
|
+
const resolvedTemplate = (0, templateResolver_js_1.resolveScenarioJsonTemplateString)(node.data.config.template, templateValues);
|
|
1070
|
+
try {
|
|
1071
|
+
outputData = JSON.parse(resolvedTemplate);
|
|
1072
|
+
}
|
|
1073
|
+
catch {
|
|
1074
|
+
throw new ScenarioNodeFailureError(`Aggregator "${node.data.label}" produced invalid JSON after template resolution.`);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
throwIfStopped(signal);
|
|
1078
|
+
workflowOutput =
|
|
1079
|
+
node.type === "script"
|
|
1080
|
+
? scriptHasReturnValue
|
|
1081
|
+
? scriptReturnValue
|
|
1082
|
+
: undefined
|
|
1083
|
+
: outputData;
|
|
1084
|
+
if (nodeLabel &&
|
|
1085
|
+
!RESERVED_WORKFLOW_ROOT_LABELS.has(nodeLabel) &&
|
|
1086
|
+
typeof workflowOutput !== "undefined" &&
|
|
1087
|
+
(node.type === "script" || workflowOutput !== null)) {
|
|
1088
|
+
runtimeContext.workflowContext[nodeLabel] = workflowOutput;
|
|
1089
|
+
}
|
|
1090
|
+
await ensureMinimumVisualDuration(attemptStartedAtMs);
|
|
1091
|
+
}
|
|
1092
|
+
catch (error) {
|
|
1093
|
+
if (error instanceof ScenarioStopError) {
|
|
1094
|
+
throw error;
|
|
1095
|
+
}
|
|
1096
|
+
await ensureMinimumVisualDuration(attemptStartedAtMs);
|
|
1097
|
+
throw error instanceof ScenarioNodeFailureError
|
|
1098
|
+
? error
|
|
1099
|
+
: new ScenarioNodeFailureError(error instanceof Error ? error.message : "Scenario node failed");
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
try {
|
|
1103
|
+
const policyResult = await (0, executionPolicyRunner_js_1.executeWithScenarioNodePolicy)({
|
|
1104
|
+
policy: executionPolicy,
|
|
1105
|
+
execute: executeNodeAttempt,
|
|
1106
|
+
wait: waitForDuration,
|
|
1107
|
+
shouldRethrow: (error) => error instanceof ScenarioStopError,
|
|
1108
|
+
onRetry: async (event) => {
|
|
1109
|
+
localStatuses.set(stepId, "retrying");
|
|
1110
|
+
await emit({
|
|
1111
|
+
type: "step:retry",
|
|
1112
|
+
runId,
|
|
1113
|
+
scenarioId: scenario.id,
|
|
1114
|
+
stepId,
|
|
1115
|
+
stepType: node.type,
|
|
1116
|
+
title,
|
|
1117
|
+
at: new Date().toISOString(),
|
|
1118
|
+
attempt: event.nextAttempt,
|
|
1119
|
+
maxAttempts: event.maxAttempts,
|
|
1120
|
+
retryDelayMs: event.delayMs,
|
|
1121
|
+
retryAt: event.retryAt,
|
|
1122
|
+
failurePolicy: event.failurePolicy,
|
|
1123
|
+
errorMessage: event.errorMessage,
|
|
1124
|
+
errors: event.errors,
|
|
1125
|
+
inputData,
|
|
1126
|
+
incomingEdgeId: incomingEdgeId ?? null,
|
|
1127
|
+
});
|
|
1128
|
+
updateWorkflowMetaState({
|
|
1129
|
+
context: runtimeContext,
|
|
1130
|
+
records,
|
|
1131
|
+
localStatuses,
|
|
1132
|
+
startedAt,
|
|
1133
|
+
currentNode: title,
|
|
1134
|
+
});
|
|
1135
|
+
},
|
|
1136
|
+
});
|
|
1137
|
+
const durationMs = Math.round(Date.now() - executionStartedAtMs);
|
|
1138
|
+
if (policyResult.outcome === "failed") {
|
|
1139
|
+
localStatuses.set(stepId, "failed");
|
|
1140
|
+
const failedAt = new Date().toISOString();
|
|
1141
|
+
const nodeMeta = buildNodeExecutionMeta({
|
|
1142
|
+
status: "failed",
|
|
1143
|
+
attempts: policyResult.attempts,
|
|
1144
|
+
startedAt: new Date(executionStartedAtMs).toISOString(),
|
|
1145
|
+
endedAt: failedAt,
|
|
1146
|
+
durationMs,
|
|
1147
|
+
errorMessage: policyResult.finalErrorMessage ?? "Scenario node failed",
|
|
1148
|
+
});
|
|
1149
|
+
persistNodeExecutionMeta({
|
|
1150
|
+
context: runtimeContext,
|
|
1151
|
+
label: nodeLabel,
|
|
1152
|
+
output: typeof workflowOutput !== "undefined" ? workflowOutput : outputData,
|
|
1153
|
+
meta: nodeMeta,
|
|
1154
|
+
});
|
|
1155
|
+
updateWorkflowMetaState({
|
|
1156
|
+
context: runtimeContext,
|
|
1157
|
+
records,
|
|
1158
|
+
localStatuses,
|
|
1159
|
+
startedAt,
|
|
1160
|
+
currentNode: null,
|
|
1161
|
+
lastNode: title,
|
|
1162
|
+
failedNode: title,
|
|
1163
|
+
endedAt: failedAt,
|
|
1164
|
+
});
|
|
1165
|
+
await emit({
|
|
1166
|
+
type: "step:fail",
|
|
1167
|
+
runId,
|
|
1168
|
+
scenarioId: scenario.id,
|
|
1169
|
+
stepId,
|
|
1170
|
+
stepType: node.type,
|
|
1171
|
+
title,
|
|
1172
|
+
status: "failed",
|
|
1173
|
+
at: failedAt,
|
|
1174
|
+
attempts: policyResult.attempts,
|
|
1175
|
+
currentAttempt: policyResult.attempts,
|
|
1176
|
+
maxAttempts: policyResult.maxAttempts,
|
|
1177
|
+
retryCount: policyResult.retryCount,
|
|
1178
|
+
retryEnabled: executionPolicy.retry.enabled,
|
|
1179
|
+
retryDelayMs: executionPolicy.retry.delayMs,
|
|
1180
|
+
failurePolicy: executionPolicy.onFailure,
|
|
1181
|
+
continuedAfterFailure: false,
|
|
1182
|
+
errors: policyResult.errors,
|
|
1183
|
+
nodeMeta,
|
|
1184
|
+
errorMessage: policyResult.finalErrorMessage ?? "Scenario node failed",
|
|
1185
|
+
durationMs,
|
|
1186
|
+
statusCode,
|
|
1187
|
+
response,
|
|
1188
|
+
testResults,
|
|
1189
|
+
contractResults,
|
|
1190
|
+
contractSummary,
|
|
1191
|
+
contractValidationError,
|
|
1192
|
+
dbAssertionResults,
|
|
1193
|
+
redisAssertionResults,
|
|
1194
|
+
scriptSource,
|
|
1195
|
+
scriptTimeoutMs,
|
|
1196
|
+
scriptLogs,
|
|
1197
|
+
conditionExpression,
|
|
1198
|
+
conditionPreview,
|
|
1199
|
+
conditionMode,
|
|
1200
|
+
conditionResult,
|
|
1201
|
+
conditionDebug,
|
|
1202
|
+
activeBranch,
|
|
1203
|
+
inputData,
|
|
1204
|
+
outputData,
|
|
1205
|
+
incomingEdgeId: incomingEdgeId ?? null,
|
|
1206
|
+
});
|
|
1207
|
+
throw new ScenarioNodeFailureError(policyResult.finalErrorMessage ?? "Scenario node failed");
|
|
1208
|
+
}
|
|
1209
|
+
if (policyResult.outcome === "continued") {
|
|
1210
|
+
degradedStepIds.add(stepId);
|
|
1211
|
+
localStatuses.set(stepId, "failed_continued");
|
|
1212
|
+
const continuedAt = new Date().toISOString();
|
|
1213
|
+
const nodeMeta = buildNodeExecutionMeta({
|
|
1214
|
+
status: "failed",
|
|
1215
|
+
attempts: policyResult.attempts,
|
|
1216
|
+
startedAt: new Date(executionStartedAtMs).toISOString(),
|
|
1217
|
+
endedAt: continuedAt,
|
|
1218
|
+
durationMs,
|
|
1219
|
+
continueAfterFailure: true,
|
|
1220
|
+
errorMessage: policyResult.finalErrorMessage ??
|
|
1221
|
+
"Scenario node failed but execution continued.",
|
|
1222
|
+
});
|
|
1223
|
+
persistNodeExecutionMeta({
|
|
1224
|
+
context: runtimeContext,
|
|
1225
|
+
label: nodeLabel,
|
|
1226
|
+
output: typeof workflowOutput !== "undefined" ? workflowOutput : outputData,
|
|
1227
|
+
meta: nodeMeta,
|
|
1228
|
+
});
|
|
1229
|
+
updateWorkflowMetaState({
|
|
1230
|
+
context: runtimeContext,
|
|
1231
|
+
records,
|
|
1232
|
+
localStatuses,
|
|
1233
|
+
startedAt,
|
|
1234
|
+
currentNode: null,
|
|
1235
|
+
lastNode: title,
|
|
1236
|
+
failedNode: title,
|
|
1237
|
+
endedAt: continuedAt,
|
|
1238
|
+
});
|
|
1239
|
+
await emit({
|
|
1240
|
+
type: "step:fail",
|
|
1241
|
+
runId,
|
|
1242
|
+
scenarioId: scenario.id,
|
|
1243
|
+
stepId,
|
|
1244
|
+
stepType: node.type,
|
|
1245
|
+
title,
|
|
1246
|
+
status: "failed_continued",
|
|
1247
|
+
at: continuedAt,
|
|
1248
|
+
attempts: policyResult.attempts,
|
|
1249
|
+
currentAttempt: policyResult.attempts,
|
|
1250
|
+
maxAttempts: policyResult.maxAttempts,
|
|
1251
|
+
retryCount: policyResult.retryCount,
|
|
1252
|
+
retryEnabled: executionPolicy.retry.enabled,
|
|
1253
|
+
retryDelayMs: executionPolicy.retry.delayMs,
|
|
1254
|
+
failurePolicy: executionPolicy.onFailure,
|
|
1255
|
+
continuedAfterFailure: true,
|
|
1256
|
+
errors: policyResult.errors,
|
|
1257
|
+
nodeMeta,
|
|
1258
|
+
errorMessage: policyResult.finalErrorMessage ??
|
|
1259
|
+
"Scenario node failed but execution continued.",
|
|
1260
|
+
durationMs,
|
|
1261
|
+
statusCode,
|
|
1262
|
+
response,
|
|
1263
|
+
testResults,
|
|
1264
|
+
contractResults,
|
|
1265
|
+
contractSummary,
|
|
1266
|
+
contractValidationError,
|
|
1267
|
+
dbAssertionResults,
|
|
1268
|
+
redisAssertionResults,
|
|
1269
|
+
scriptSource,
|
|
1270
|
+
scriptTimeoutMs,
|
|
1271
|
+
scriptLogs,
|
|
1272
|
+
conditionExpression,
|
|
1273
|
+
conditionPreview,
|
|
1274
|
+
conditionMode,
|
|
1275
|
+
conditionResult,
|
|
1276
|
+
conditionDebug,
|
|
1277
|
+
activeBranch,
|
|
1278
|
+
inputData,
|
|
1279
|
+
outputData,
|
|
1280
|
+
incomingEdgeId: incomingEdgeId ?? null,
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
else {
|
|
1284
|
+
const successStatus = shouldMarkPartialSuccess ? "partial_success" : "success";
|
|
1285
|
+
if (successStatus === "partial_success") {
|
|
1286
|
+
degradedStepIds.add(stepId);
|
|
1287
|
+
}
|
|
1288
|
+
else {
|
|
1289
|
+
degradedStepIds.delete(stepId);
|
|
1290
|
+
}
|
|
1291
|
+
localStatuses.set(stepId, successStatus);
|
|
1292
|
+
const succeededAt = new Date().toISOString();
|
|
1293
|
+
const nodeMeta = buildNodeExecutionMeta({
|
|
1294
|
+
status: "success",
|
|
1295
|
+
attempts: policyResult.attempts,
|
|
1296
|
+
startedAt: new Date(executionStartedAtMs).toISOString(),
|
|
1297
|
+
endedAt: succeededAt,
|
|
1298
|
+
durationMs,
|
|
1299
|
+
});
|
|
1300
|
+
persistNodeExecutionMeta({
|
|
1301
|
+
context: runtimeContext,
|
|
1302
|
+
label: nodeLabel,
|
|
1303
|
+
output: typeof workflowOutput !== "undefined" ? workflowOutput : outputData,
|
|
1304
|
+
meta: nodeMeta,
|
|
1305
|
+
});
|
|
1306
|
+
updateWorkflowMetaState({
|
|
1307
|
+
context: runtimeContext,
|
|
1308
|
+
records,
|
|
1309
|
+
localStatuses,
|
|
1310
|
+
startedAt,
|
|
1311
|
+
currentNode: null,
|
|
1312
|
+
lastNode: title,
|
|
1313
|
+
endedAt: succeededAt,
|
|
1314
|
+
});
|
|
1315
|
+
await emit({
|
|
1316
|
+
type: "step:success",
|
|
1317
|
+
runId,
|
|
1318
|
+
scenarioId: scenario.id,
|
|
1319
|
+
stepId,
|
|
1320
|
+
stepType: node.type,
|
|
1321
|
+
title,
|
|
1322
|
+
status: successStatus,
|
|
1323
|
+
at: succeededAt,
|
|
1324
|
+
attempts: policyResult.attempts,
|
|
1325
|
+
currentAttempt: policyResult.attempts,
|
|
1326
|
+
maxAttempts: policyResult.maxAttempts,
|
|
1327
|
+
retryCount: policyResult.retryCount,
|
|
1328
|
+
retryEnabled: executionPolicy.retry.enabled,
|
|
1329
|
+
retryDelayMs: executionPolicy.retry.delayMs,
|
|
1330
|
+
failurePolicy: executionPolicy.onFailure,
|
|
1331
|
+
continuedAfterFailure: false,
|
|
1332
|
+
errors: policyResult.errors,
|
|
1333
|
+
nodeMeta,
|
|
1334
|
+
durationMs,
|
|
1335
|
+
statusCode,
|
|
1336
|
+
response,
|
|
1337
|
+
testResults,
|
|
1338
|
+
contractResults,
|
|
1339
|
+
contractSummary,
|
|
1340
|
+
contractValidationError,
|
|
1341
|
+
dbAssertionResults,
|
|
1342
|
+
redisAssertionResults,
|
|
1343
|
+
scriptSource,
|
|
1344
|
+
scriptTimeoutMs,
|
|
1345
|
+
scriptLogs,
|
|
1346
|
+
conditionExpression,
|
|
1347
|
+
conditionPreview,
|
|
1348
|
+
conditionMode,
|
|
1349
|
+
conditionResult,
|
|
1350
|
+
conditionDebug,
|
|
1351
|
+
activeBranch,
|
|
1352
|
+
inputData,
|
|
1353
|
+
outputData,
|
|
1354
|
+
incomingEdgeId: incomingEdgeId ?? null,
|
|
1355
|
+
});
|
|
1356
|
+
if (node.type === "end" && node.data.config.loop && node.data.config.loopTargetNodeId) {
|
|
1357
|
+
const loopTargetNode = scope.graph.nodeMap.get(node.data.config.loopTargetNodeId);
|
|
1358
|
+
if (!loopTargetNode) {
|
|
1359
|
+
throw new ScenarioNodeFailureError("Loop target node could not be found.");
|
|
1360
|
+
}
|
|
1361
|
+
const configuredLoopIterations = Math.min(graphTraversal_js_1.MAX_END_LOOP_ITERATIONS, Math.max(0, Math.round(node.data.config.loopIterations ?? graphTraversal_js_1.DEFAULT_END_LOOP_ITERATIONS)));
|
|
1362
|
+
const currentLoopIteration = loopIterationCounts.get(node.id) ?? 0;
|
|
1363
|
+
if (currentLoopIteration < configuredLoopIterations) {
|
|
1364
|
+
const nextLoopIteration = currentLoopIteration + 1;
|
|
1365
|
+
loopIterationCounts.set(node.id, nextLoopIteration);
|
|
1366
|
+
if (nextLoopIteration > graphTraversal_js_1.MAX_END_LOOP_ITERATIONS) {
|
|
1367
|
+
throw new ScenarioNodeFailureError(`Loop safety limit reached (${graphTraversal_js_1.MAX_END_LOOP_ITERATIONS} iterations). Stop the run or choose a narrower loop target.`);
|
|
1368
|
+
}
|
|
1369
|
+
const resetStepIds = resetReachableNodesForLoop(scope, loopTargetNode.id);
|
|
1370
|
+
for (const resetStepId of resetStepIds) {
|
|
1371
|
+
degradedStepIds.delete(resetStepId);
|
|
1372
|
+
}
|
|
1373
|
+
await emit({
|
|
1374
|
+
type: "step:reset",
|
|
1375
|
+
runId,
|
|
1376
|
+
scenarioId: scenario.id,
|
|
1377
|
+
stepIds: resetStepIds,
|
|
1378
|
+
at: new Date().toISOString(),
|
|
1379
|
+
reason: `Loop ${nextLoopIteration}/${configuredLoopIterations}: reset workflow branch`,
|
|
1380
|
+
});
|
|
1381
|
+
await waitForDuration(node.data.config.loopDelayMs ?? graphTraversal_js_1.DEFAULT_END_LOOP_DELAY_MS);
|
|
1382
|
+
await executeNode(loopTargetNode.id, null, { force: true }, scope);
|
|
1383
|
+
}
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
const outgoingEdges = scope.graph.outgoingMap.get(nodeId) ?? [];
|
|
1388
|
+
const nextEdges = node.type === "condition"
|
|
1389
|
+
? outgoingEdges.filter((edge) => (conditionResult ? "yes" : "no") === (edge.type ?? "default"))
|
|
1390
|
+
: outgoingEdges;
|
|
1391
|
+
for (const edge of nextEdges) {
|
|
1392
|
+
throwIfStopped(signal);
|
|
1393
|
+
await executeNode(edge.target, scope.stepIdForEdge(edge.id), {}, scope);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
catch (error) {
|
|
1397
|
+
if (error instanceof ScenarioStopError) {
|
|
1398
|
+
const stepState = localStatuses.get(stepId);
|
|
1399
|
+
if (stepState === "running" || stepState === "retrying") {
|
|
1400
|
+
const skipDurationMs = isRecord(outputData) && typeof outputData.actualDurationMs === "number"
|
|
1401
|
+
? Math.max(0, Math.round(outputData.actualDurationMs))
|
|
1402
|
+
: Math.max(0, Math.round(Date.now() - executionStartedAtMs));
|
|
1403
|
+
const skippedAt = new Date().toISOString();
|
|
1404
|
+
localStatuses.set(stepId, "skipped");
|
|
1405
|
+
const nodeMeta = buildNodeExecutionMeta({
|
|
1406
|
+
status: "skipped",
|
|
1407
|
+
attempts: 0,
|
|
1408
|
+
startedAt: new Date(executionStartedAtMs).toISOString(),
|
|
1409
|
+
endedAt: skippedAt,
|
|
1410
|
+
durationMs: skipDurationMs,
|
|
1411
|
+
});
|
|
1412
|
+
persistNodeExecutionMeta({
|
|
1413
|
+
context: runtimeContext,
|
|
1414
|
+
label: nodeLabel,
|
|
1415
|
+
output: typeof workflowOutput !== "undefined" ? workflowOutput : outputData,
|
|
1416
|
+
meta: nodeMeta,
|
|
1417
|
+
});
|
|
1418
|
+
updateWorkflowMetaState({
|
|
1419
|
+
context: runtimeContext,
|
|
1420
|
+
records,
|
|
1421
|
+
localStatuses,
|
|
1422
|
+
startedAt,
|
|
1423
|
+
currentNode: null,
|
|
1424
|
+
lastNode: title,
|
|
1425
|
+
endedAt: skippedAt,
|
|
1426
|
+
});
|
|
1427
|
+
await emit({
|
|
1428
|
+
type: "step:skip",
|
|
1429
|
+
runId,
|
|
1430
|
+
scenarioId: scenario.id,
|
|
1431
|
+
stepId,
|
|
1432
|
+
stepType: node.type,
|
|
1433
|
+
title,
|
|
1434
|
+
at: skippedAt,
|
|
1435
|
+
reason: "Execution stopped",
|
|
1436
|
+
nodeMeta,
|
|
1437
|
+
durationMs: skipDurationMs,
|
|
1438
|
+
inputData,
|
|
1439
|
+
outputData,
|
|
1440
|
+
incomingEdgeId: incomingEdgeId ?? null,
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
throw error;
|
|
1444
|
+
}
|
|
1445
|
+
throw error instanceof ScenarioNodeFailureError
|
|
1446
|
+
? error
|
|
1447
|
+
: new ScenarioNodeFailureError(error instanceof Error ? error.message : "Scenario node failed");
|
|
1448
|
+
}
|
|
1449
|
+
};
|
|
1450
|
+
await emit({
|
|
1451
|
+
type: "scenario:start",
|
|
1452
|
+
runId,
|
|
1453
|
+
scenarioId: scenario.id,
|
|
1454
|
+
scenarioName: scenario.name,
|
|
1455
|
+
totalSteps: runtimeStepIds.length,
|
|
1456
|
+
stepIds: runtimeStepIds,
|
|
1457
|
+
at: startedAt,
|
|
1458
|
+
});
|
|
1459
|
+
let finalStatus = "success";
|
|
1460
|
+
try {
|
|
1461
|
+
for (const startNodeId of orderedStartNodeIds) {
|
|
1462
|
+
throwIfStopped(signal);
|
|
1463
|
+
await executeNode(startNodeId, null);
|
|
1464
|
+
}
|
|
1465
|
+
await markRemainingNodesSkipped("Node was not reached during this run.");
|
|
1466
|
+
finalStatus = degradedStepIds.size > 0 ? "completed_with_failures" : "success";
|
|
1467
|
+
const endedAt = new Date().toISOString();
|
|
1468
|
+
const workflowMeta = updateWorkflowMetaState({
|
|
1469
|
+
context: runtimeContext,
|
|
1470
|
+
records,
|
|
1471
|
+
localStatuses,
|
|
1472
|
+
startedAt,
|
|
1473
|
+
currentNode: null,
|
|
1474
|
+
endedAt,
|
|
1475
|
+
});
|
|
1476
|
+
await emit({
|
|
1477
|
+
type: "scenario:end",
|
|
1478
|
+
runId,
|
|
1479
|
+
scenarioId: scenario.id,
|
|
1480
|
+
status: finalStatus,
|
|
1481
|
+
at: endedAt,
|
|
1482
|
+
durationMs: Date.now() - startedAtMs,
|
|
1483
|
+
workflowMeta,
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
catch (error) {
|
|
1487
|
+
const isStopped = error instanceof ScenarioStopError || Boolean(signal?.aborted);
|
|
1488
|
+
await markRemainingNodesSkipped(isStopped ? "Skipped because execution was stopped." : "Skipped after failure.");
|
|
1489
|
+
finalStatus = isStopped ? "stopped" : "failed";
|
|
1490
|
+
const endedAt = new Date().toISOString();
|
|
1491
|
+
const workflowMeta = updateWorkflowMetaState({
|
|
1492
|
+
context: runtimeContext,
|
|
1493
|
+
records,
|
|
1494
|
+
localStatuses,
|
|
1495
|
+
startedAt,
|
|
1496
|
+
currentNode: null,
|
|
1497
|
+
endedAt,
|
|
1498
|
+
});
|
|
1499
|
+
await emit({
|
|
1500
|
+
type: "scenario:end",
|
|
1501
|
+
runId,
|
|
1502
|
+
scenarioId: scenario.id,
|
|
1503
|
+
status: finalStatus,
|
|
1504
|
+
at: endedAt,
|
|
1505
|
+
durationMs: Date.now() - startedAtMs,
|
|
1506
|
+
workflowMeta,
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
const endedAt = new Date().toISOString();
|
|
1510
|
+
const finalWorkflowMeta = updateWorkflowMetaState({
|
|
1511
|
+
context: runtimeContext,
|
|
1512
|
+
records,
|
|
1513
|
+
localStatuses,
|
|
1514
|
+
startedAt,
|
|
1515
|
+
currentNode: null,
|
|
1516
|
+
endedAt,
|
|
1517
|
+
});
|
|
1518
|
+
const report = {
|
|
1519
|
+
runId,
|
|
1520
|
+
scenarioId: scenario.id,
|
|
1521
|
+
scenarioName: scenario.name,
|
|
1522
|
+
scenarioStatus: finalStatus,
|
|
1523
|
+
startedAt,
|
|
1524
|
+
endedAt,
|
|
1525
|
+
durationMs: Date.now() - startedAtMs,
|
|
1526
|
+
totalSteps: runtimeStepIds.length,
|
|
1527
|
+
completedSteps: Array.from(localStatuses.values()).filter(isTerminalStatus).length,
|
|
1528
|
+
context: createContextSnapshot(runtimeContext),
|
|
1529
|
+
workflowMeta: cloneWorkflowExecutionMeta(finalWorkflowMeta),
|
|
1530
|
+
nodeExecutions: runtimeStepIds.map((stepId) => {
|
|
1531
|
+
const descriptor = runtimeStepDescriptors.find((entry) => entry.stepId === stepId);
|
|
1532
|
+
return (records.get(stepId) ??
|
|
1533
|
+
createStepExecutionRecord({
|
|
1534
|
+
stepId,
|
|
1535
|
+
stepType: descriptor?.node.type ?? "script",
|
|
1536
|
+
title: descriptor ? buildTitle(descriptor.node) : stepId,
|
|
1537
|
+
}));
|
|
1538
|
+
}),
|
|
1539
|
+
events,
|
|
1540
|
+
};
|
|
1541
|
+
await eventHandlers?.onScenarioComplete?.(report);
|
|
1542
|
+
return report;
|
|
1543
|
+
};
|
|
1544
|
+
exports.runScenario = runScenario;
|
|
1545
|
+
//# sourceMappingURL=runScenario.js.map
|