callman-core 1.0.4 → 1.0.6
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/scenario-runner/condition.d.ts.map +1 -1
- package/dist/scenario-runner/condition.js +180 -1
- package/dist/scenario-runner/condition.js.map +1 -1
- package/dist/scenario-runner/conditionTypes.d.ts +1 -0
- package/dist/scenario-runner/conditionTypes.d.ts.map +1 -1
- package/dist/scenario-runner/index.d.ts +1 -0
- package/dist/scenario-runner/index.d.ts.map +1 -1
- package/dist/scenario-runner/index.js +1 -0
- package/dist/scenario-runner/index.js.map +1 -1
- package/dist/scenario-runner/nodePolicy.d.ts +2 -2
- package/dist/scenario-runner/nodePolicy.d.ts.map +1 -1
- package/dist/scenario-runner/nodePolicy.js +8 -0
- package/dist/scenario-runner/nodePolicy.js.map +1 -1
- package/dist/scenario-runner/notification/index.d.ts +7 -0
- package/dist/scenario-runner/notification/index.d.ts.map +1 -0
- package/dist/scenario-runner/notification/index.js +14 -0
- package/dist/scenario-runner/notification/index.js.map +1 -0
- package/dist/scenario-runner/notification/providers/provider.interface.d.ts +20 -0
- package/dist/scenario-runner/notification/providers/provider.interface.d.ts.map +1 -0
- package/dist/scenario-runner/notification/providers/provider.interface.js +2 -0
- package/dist/scenario-runner/notification/providers/provider.interface.js.map +1 -0
- package/dist/scenario-runner/notification/providers/slack.provider.d.ts +3 -0
- package/dist/scenario-runner/notification/providers/slack.provider.d.ts.map +1 -0
- package/dist/scenario-runner/notification/providers/slack.provider.js +85 -0
- package/dist/scenario-runner/notification/providers/slack.provider.js.map +1 -0
- package/dist/scenario-runner/notification/template.d.ts +2 -0
- package/dist/scenario-runner/notification/template.d.ts.map +1 -0
- package/dist/scenario-runner/notification/template.js +9 -0
- package/dist/scenario-runner/notification/template.js.map +1 -0
- package/dist/scenario-runner/runScenario.d.ts.map +1 -1
- package/dist/scenario-runner/runScenario.js +395 -15
- package/dist/scenario-runner/runScenario.js.map +1 -1
- package/dist/scenario-runner/templateResolver.d.ts +3 -2
- package/dist/scenario-runner/templateResolver.d.ts.map +1 -1
- package/dist/scenario-runner/templateResolver.js +19 -1
- package/dist/scenario-runner/templateResolver.js.map +1 -1
- package/dist/scenario-runner/types.d.ts +92 -3
- package/dist/scenario-runner/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import { executeWithScenarioNodePolicy } from "./executionPolicyRunner.js";
|
|
|
4
4
|
import { getLegacyScenarioNodeExecutionPolicy, getScenarioNodeExecutionPolicy, supportsScenarioNodeExecutionPolicy, } from "./nodePolicy.js";
|
|
5
5
|
import { buildScenarioExecutionGraph, buildScenarioTemplateValues, collectExclusiveBranchNodeIds, evaluateScenarioExpression, resolveScenarioJsonTemplateString, resolveScenarioTemplateString, } from "./templateResolver.js";
|
|
6
6
|
import { executeWaitNode, ScenarioWaitNodeCancelledError, ScenarioWaitNodeValidationError, } from "./waitNodeExecutor.js";
|
|
7
|
+
import { buildNotificationDeliveryRequest, renderNotificationTemplate, } from "./notification/index.js";
|
|
7
8
|
class ScenarioStopError extends Error {
|
|
8
9
|
constructor() {
|
|
9
10
|
super("Scenario execution stopped");
|
|
@@ -31,6 +32,57 @@ const buildTitle = (node) => {
|
|
|
31
32
|
};
|
|
32
33
|
const isKafkaNode = (node) => node?.type === "kafka";
|
|
33
34
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
35
|
+
const RESERVED_WORKFLOW_ROOT_LABELS = new Set(["workflow", "__meta"]);
|
|
36
|
+
const cloneNodeExecutionMeta = (meta) => ({
|
|
37
|
+
...meta,
|
|
38
|
+
...(meta.error ? { error: { ...meta.error } } : {}),
|
|
39
|
+
});
|
|
40
|
+
const cloneWorkflowExecutionMeta = (meta) => ({
|
|
41
|
+
...meta,
|
|
42
|
+
});
|
|
43
|
+
const cloneNodeMetaByLabel = (value) => Object.fromEntries(Object.entries(value).map(([label, meta]) => [label, cloneNodeExecutionMeta(meta)]));
|
|
44
|
+
const attachNodeMetaToOutput = (output, meta) => {
|
|
45
|
+
const normalizedMeta = cloneNodeExecutionMeta(meta);
|
|
46
|
+
if (Array.isArray(output)) {
|
|
47
|
+
const nextValue = [...output];
|
|
48
|
+
nextValue.__meta = normalizedMeta;
|
|
49
|
+
return nextValue;
|
|
50
|
+
}
|
|
51
|
+
if (isRecord(output)) {
|
|
52
|
+
return {
|
|
53
|
+
...output,
|
|
54
|
+
__meta: normalizedMeta,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (output === null || typeof output === "undefined") {
|
|
58
|
+
return {
|
|
59
|
+
__meta: normalizedMeta,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return output;
|
|
63
|
+
};
|
|
64
|
+
const createInitialWorkflowMeta = ({ startedAt, totalNodes, }) => ({
|
|
65
|
+
status: "success",
|
|
66
|
+
success: true,
|
|
67
|
+
failed: false,
|
|
68
|
+
totalNodes,
|
|
69
|
+
successfulNodes: 0,
|
|
70
|
+
failedNodes: 0,
|
|
71
|
+
skippedNodes: 0,
|
|
72
|
+
totalRetries: 0,
|
|
73
|
+
startedAt,
|
|
74
|
+
endedAt: startedAt,
|
|
75
|
+
durationMs: 0,
|
|
76
|
+
currentNode: null,
|
|
77
|
+
lastNode: null,
|
|
78
|
+
failedNode: null,
|
|
79
|
+
});
|
|
80
|
+
const buildWorkflowRoot = ({ scenario, meta, }) => ({
|
|
81
|
+
id: scenario.id,
|
|
82
|
+
name: scenario.name,
|
|
83
|
+
...(typeof scenario.version === "number" ? { version: scenario.version } : {}),
|
|
84
|
+
__meta: cloneWorkflowExecutionMeta(meta),
|
|
85
|
+
});
|
|
34
86
|
const createInputSnapshot = (context) => ({
|
|
35
87
|
env: context.environment,
|
|
36
88
|
global: context.globals,
|
|
@@ -48,6 +100,8 @@ const createContextSnapshot = (context) => ({
|
|
|
48
100
|
environment: { ...context.environment },
|
|
49
101
|
globals: { ...context.globals },
|
|
50
102
|
workflowContext: { ...context.workflowContext },
|
|
103
|
+
nodeMetaByLabel: cloneNodeMetaByLabel(context.nodeMetaByLabel),
|
|
104
|
+
workflowMeta: cloneWorkflowExecutionMeta(context.workflowMeta),
|
|
51
105
|
responseRoot: context.responseRoot ? { ...context.responseRoot } : null,
|
|
52
106
|
lastResponse: context.lastResponse,
|
|
53
107
|
dbResult: context.dbResult,
|
|
@@ -126,11 +180,104 @@ const throwIfStopped = (signal) => {
|
|
|
126
180
|
throw new ScenarioStopError();
|
|
127
181
|
}
|
|
128
182
|
};
|
|
183
|
+
const isWorkflowFailureStatus = (status) => status === "failed" || status === "failed_continued";
|
|
184
|
+
const isWorkflowSuccessStatus = (status) => status === "success" || status === "partial_success";
|
|
185
|
+
const buildNodeExecutionMeta = ({ status, attempts, startedAt, endedAt, durationMs, continueAfterFailure = false, errorMessage, }) => ({
|
|
186
|
+
status,
|
|
187
|
+
success: status === "success",
|
|
188
|
+
failed: status === "failed",
|
|
189
|
+
skipped: status === "skipped",
|
|
190
|
+
attempts,
|
|
191
|
+
startedAt,
|
|
192
|
+
endedAt,
|
|
193
|
+
durationMs,
|
|
194
|
+
...(continueAfterFailure ? { continueAfterFailure: true } : {}),
|
|
195
|
+
...(errorMessage
|
|
196
|
+
? {
|
|
197
|
+
error: {
|
|
198
|
+
message: errorMessage,
|
|
199
|
+
type: "ScenarioNodeFailureError",
|
|
200
|
+
},
|
|
201
|
+
}
|
|
202
|
+
: {}),
|
|
203
|
+
});
|
|
204
|
+
const updateWorkflowMetaState = ({ context, records, localStatuses, startedAt, currentNode, lastNode, failedNode, endedAt = new Date().toISOString(), }) => {
|
|
205
|
+
const statuses = Array.from(localStatuses.values());
|
|
206
|
+
const successfulNodes = statuses.filter(isWorkflowSuccessStatus).length;
|
|
207
|
+
const failedNodes = statuses.filter(isWorkflowFailureStatus).length;
|
|
208
|
+
const skippedNodes = statuses.filter((status) => status === "skipped").length;
|
|
209
|
+
const totalRetries = Array.from(records.values()).reduce((sum, record) => sum + Math.max(0, record.retryCount), 0);
|
|
210
|
+
const hasPartialNodes = statuses.some((status) => status === "failed_continued" || status === "partial_success");
|
|
211
|
+
const nextStatus = failedNodes > 0 && !hasPartialNodes
|
|
212
|
+
? "failed"
|
|
213
|
+
: failedNodes > 0 || hasPartialNodes
|
|
214
|
+
? "partial"
|
|
215
|
+
: "success";
|
|
216
|
+
const nextMeta = {
|
|
217
|
+
status: nextStatus,
|
|
218
|
+
success: nextStatus === "success",
|
|
219
|
+
failed: nextStatus === "failed",
|
|
220
|
+
totalNodes: context.workflowMeta.totalNodes,
|
|
221
|
+
successfulNodes,
|
|
222
|
+
failedNodes,
|
|
223
|
+
skippedNodes,
|
|
224
|
+
totalRetries,
|
|
225
|
+
startedAt,
|
|
226
|
+
endedAt,
|
|
227
|
+
durationMs: Math.max(0, new Date(endedAt).getTime() - new Date(startedAt).getTime()),
|
|
228
|
+
currentNode: currentNode ?? context.workflowMeta.currentNode ?? null,
|
|
229
|
+
lastNode: lastNode ?? context.workflowMeta.lastNode ?? null,
|
|
230
|
+
failedNode: failedNode ?? context.workflowMeta.failedNode ?? null,
|
|
231
|
+
};
|
|
232
|
+
context.workflowMeta = nextMeta;
|
|
233
|
+
context.workflowContext.workflow = {
|
|
234
|
+
...(isRecord(context.workflowContext.workflow)
|
|
235
|
+
? context.workflowContext.workflow
|
|
236
|
+
: {}),
|
|
237
|
+
__meta: cloneWorkflowExecutionMeta(nextMeta),
|
|
238
|
+
};
|
|
239
|
+
return nextMeta;
|
|
240
|
+
};
|
|
241
|
+
const withNotificationFailurePolicyOverride = (node, policy) => {
|
|
242
|
+
if (node.type !== "notification") {
|
|
243
|
+
return policy;
|
|
244
|
+
}
|
|
245
|
+
if (typeof node.data.config.continueOnFailure !== "boolean") {
|
|
246
|
+
return policy;
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
...policy,
|
|
250
|
+
onFailure: node.data.config.continueOnFailure ? "continue" : "stop",
|
|
251
|
+
};
|
|
252
|
+
};
|
|
253
|
+
const normalizeNotificationSeverity = (value) => {
|
|
254
|
+
switch (value) {
|
|
255
|
+
case "success":
|
|
256
|
+
case "warning":
|
|
257
|
+
case "error":
|
|
258
|
+
return value;
|
|
259
|
+
case "info":
|
|
260
|
+
default:
|
|
261
|
+
return "info";
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
const persistNodeExecutionMeta = ({ context, label, output, meta, }) => {
|
|
265
|
+
const trimmedLabel = label.trim();
|
|
266
|
+
if (!trimmedLabel) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
context.nodeMetaByLabel[trimmedLabel] = cloneNodeExecutionMeta(meta);
|
|
270
|
+
if (RESERVED_WORKFLOW_ROOT_LABELS.has(trimmedLabel)) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
context.workflowContext[trimmedLabel] = attachNodeMetaToOutput(output, meta);
|
|
274
|
+
};
|
|
129
275
|
const createStepExecutionRecord = ({ stepId, stepType, title, }) => ({
|
|
130
276
|
stepId,
|
|
131
277
|
stepType,
|
|
132
278
|
title,
|
|
133
279
|
status: "idle",
|
|
280
|
+
meta: null,
|
|
134
281
|
attempts: 0,
|
|
135
282
|
currentAttempt: null,
|
|
136
283
|
maxAttempts: 1,
|
|
@@ -201,6 +348,7 @@ const updateRecordFromEvent = (records, event) => {
|
|
|
201
348
|
stepType: event.stepType,
|
|
202
349
|
title: event.title,
|
|
203
350
|
status: "running",
|
|
351
|
+
meta: null,
|
|
204
352
|
attempts: event.attempt ?? 1,
|
|
205
353
|
currentAttempt: event.attempt ?? 1,
|
|
206
354
|
maxAttempts: event.maxAttempts ?? 1,
|
|
@@ -223,6 +371,7 @@ const updateRecordFromEvent = (records, event) => {
|
|
|
223
371
|
stepType: event.stepType,
|
|
224
372
|
title: event.title,
|
|
225
373
|
status: "retrying",
|
|
374
|
+
meta: current.meta,
|
|
226
375
|
attempts: event.attempt,
|
|
227
376
|
currentAttempt: event.attempt,
|
|
228
377
|
maxAttempts: event.maxAttempts,
|
|
@@ -257,6 +406,7 @@ const updateRecordFromEvent = (records, event) => {
|
|
|
257
406
|
stepType: event.stepType,
|
|
258
407
|
title: event.title,
|
|
259
408
|
status: "skipped",
|
|
409
|
+
meta: event.nodeMeta ?? current.meta,
|
|
260
410
|
inputData: event.inputData ?? current.inputData,
|
|
261
411
|
outputData: event.outputData ?? current.outputData,
|
|
262
412
|
incomingEdgeId: event.incomingEdgeId ?? current.incomingEdgeId,
|
|
@@ -272,6 +422,7 @@ const updateRecordFromEvent = (records, event) => {
|
|
|
272
422
|
stepType: event.stepType,
|
|
273
423
|
title: event.title,
|
|
274
424
|
status: event.status ?? (isSuccess ? "success" : "failed"),
|
|
425
|
+
meta: event.nodeMeta ?? current.meta,
|
|
275
426
|
attempts: event.attempts ?? current.attempts,
|
|
276
427
|
currentAttempt: event.currentAttempt ?? current.currentAttempt,
|
|
277
428
|
maxAttempts: event.maxAttempts ?? current.maxAttempts,
|
|
@@ -353,10 +504,21 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
353
504
|
const localStatuses = new Map(runtimeStepIds.map((stepId) => [stepId, "idle"]));
|
|
354
505
|
const records = new Map();
|
|
355
506
|
const events = [];
|
|
507
|
+
const initialWorkflowMeta = createInitialWorkflowMeta({
|
|
508
|
+
startedAt,
|
|
509
|
+
totalNodes: runtimeStepIds.length,
|
|
510
|
+
});
|
|
356
511
|
const runtimeContext = {
|
|
357
512
|
environment: { ...environment },
|
|
358
513
|
globals: { ...globals },
|
|
359
|
-
workflowContext: {
|
|
514
|
+
workflowContext: {
|
|
515
|
+
workflow: buildWorkflowRoot({
|
|
516
|
+
scenario,
|
|
517
|
+
meta: initialWorkflowMeta,
|
|
518
|
+
}),
|
|
519
|
+
},
|
|
520
|
+
nodeMetaByLabel: {},
|
|
521
|
+
workflowMeta: initialWorkflowMeta,
|
|
360
522
|
responseRoot: null,
|
|
361
523
|
lastResponse: null,
|
|
362
524
|
dbResult: null,
|
|
@@ -419,6 +581,20 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
419
581
|
continue;
|
|
420
582
|
}
|
|
421
583
|
localStatuses.set(stepId, "skipped");
|
|
584
|
+
const skippedAt = new Date().toISOString();
|
|
585
|
+
const nodeMeta = buildNodeExecutionMeta({
|
|
586
|
+
status: "skipped",
|
|
587
|
+
attempts: 0,
|
|
588
|
+
startedAt: skippedAt,
|
|
589
|
+
endedAt: skippedAt,
|
|
590
|
+
durationMs: 0,
|
|
591
|
+
});
|
|
592
|
+
persistNodeExecutionMeta({
|
|
593
|
+
context: runtimeContext,
|
|
594
|
+
label: skippedNode.data.label.trim(),
|
|
595
|
+
output: null,
|
|
596
|
+
meta: nodeMeta,
|
|
597
|
+
});
|
|
422
598
|
await emit({
|
|
423
599
|
type: "step:skip",
|
|
424
600
|
runId,
|
|
@@ -426,10 +602,19 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
426
602
|
stepId,
|
|
427
603
|
stepType: skippedNode.type,
|
|
428
604
|
title: buildTitle(skippedNode),
|
|
429
|
-
at:
|
|
605
|
+
at: skippedAt,
|
|
430
606
|
reason,
|
|
607
|
+
nodeMeta,
|
|
431
608
|
inputData: createInputSnapshot(runtimeContext),
|
|
432
609
|
});
|
|
610
|
+
updateWorkflowMetaState({
|
|
611
|
+
context: runtimeContext,
|
|
612
|
+
records,
|
|
613
|
+
localStatuses,
|
|
614
|
+
startedAt,
|
|
615
|
+
currentNode: null,
|
|
616
|
+
lastNode: buildTitle(skippedNode),
|
|
617
|
+
});
|
|
433
618
|
}
|
|
434
619
|
};
|
|
435
620
|
const markRemainingNodesSkipped = async (reason) => {
|
|
@@ -484,8 +669,9 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
484
669
|
return;
|
|
485
670
|
}
|
|
486
671
|
const title = buildTitle(node);
|
|
672
|
+
const nodeLabel = node.data.label.trim();
|
|
487
673
|
const executionPolicy = supportsScenarioNodeExecutionPolicy(node)
|
|
488
|
-
? getScenarioNodeExecutionPolicy(node)
|
|
674
|
+
? withNotificationFailurePolicyOverride(node, getScenarioNodeExecutionPolicy(node))
|
|
489
675
|
: getLegacyScenarioNodeExecutionPolicy();
|
|
490
676
|
const executionStartedAtMs = Date.now();
|
|
491
677
|
let inputData = createInputSnapshot(runtimeContext);
|
|
@@ -510,6 +696,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
510
696
|
let conditionMode = null;
|
|
511
697
|
let conditionDebug = null;
|
|
512
698
|
let shouldMarkPartialSuccess = false;
|
|
699
|
+
let workflowOutput = undefined;
|
|
513
700
|
const resetAttemptArtifacts = () => {
|
|
514
701
|
statusCode = null;
|
|
515
702
|
response = null;
|
|
@@ -532,6 +719,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
532
719
|
conditionMode = null;
|
|
533
720
|
conditionDebug = null;
|
|
534
721
|
shouldMarkPartialSuccess = false;
|
|
722
|
+
workflowOutput = undefined;
|
|
535
723
|
};
|
|
536
724
|
const executeNodeAttempt = async (attempt, maxAttempts) => {
|
|
537
725
|
throwIfStopped(signal);
|
|
@@ -555,10 +743,18 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
555
743
|
inputData,
|
|
556
744
|
incomingEdgeId: incomingEdgeId ?? null,
|
|
557
745
|
});
|
|
746
|
+
updateWorkflowMetaState({
|
|
747
|
+
context: runtimeContext,
|
|
748
|
+
records,
|
|
749
|
+
localStatuses,
|
|
750
|
+
startedAt,
|
|
751
|
+
currentNode: title,
|
|
752
|
+
});
|
|
558
753
|
const templateValues = buildScenarioTemplateValues({
|
|
559
754
|
environment: runtimeContext.environment,
|
|
560
755
|
globals: runtimeContext.globals,
|
|
561
756
|
workflowContext: runtimeContext.workflowContext,
|
|
757
|
+
nodeMetaByLabel: runtimeContext.nodeMetaByLabel,
|
|
562
758
|
responseRoot: runtimeContext.responseRoot,
|
|
563
759
|
dbResult: runtimeContext.dbResult,
|
|
564
760
|
kafkaEvent: runtimeContext.kafkaEvent,
|
|
@@ -574,6 +770,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
574
770
|
environment: runtimeContext.environment,
|
|
575
771
|
globals: runtimeContext.globals,
|
|
576
772
|
workflowContext: runtimeContext.workflowContext,
|
|
773
|
+
nodeMetaByLabel: runtimeContext.nodeMetaByLabel,
|
|
577
774
|
responseRoot: runtimeContext.responseRoot,
|
|
578
775
|
dbResult: runtimeContext.dbResult,
|
|
579
776
|
kafkaEvent: runtimeContext.kafkaEvent,
|
|
@@ -690,6 +887,48 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
690
887
|
throw new ScenarioNodeFailureError(redisResult.failureMessage);
|
|
691
888
|
}
|
|
692
889
|
}
|
|
890
|
+
if (node.type === "notification") {
|
|
891
|
+
const resolvedMessage = renderNotificationTemplate(node.data.config.messageTemplate, templateValues).trim();
|
|
892
|
+
const resolvedChannelOverride = node.data.config.channelOverride?.trim()
|
|
893
|
+
? renderNotificationTemplate(node.data.config.channelOverride, templateValues).trim()
|
|
894
|
+
: null;
|
|
895
|
+
const workflowRoot = isRecord(runtimeContext.workflowContext.workflow)
|
|
896
|
+
? runtimeContext.workflowContext.workflow
|
|
897
|
+
: {};
|
|
898
|
+
const deliveryRequest = buildNotificationDeliveryRequest({
|
|
899
|
+
provider: node.data.config.provider,
|
|
900
|
+
scenario: {
|
|
901
|
+
id: scenario.id,
|
|
902
|
+
name: scenario.name,
|
|
903
|
+
...(typeof scenario.version === "number"
|
|
904
|
+
? { version: scenario.version }
|
|
905
|
+
: {}),
|
|
906
|
+
},
|
|
907
|
+
nodeLabel: title,
|
|
908
|
+
severity: normalizeNotificationSeverity(node.data.config.severity),
|
|
909
|
+
message: resolvedMessage || "N/A",
|
|
910
|
+
channelOverride: resolvedChannelOverride,
|
|
911
|
+
triggerLabel: "Scenario Run",
|
|
912
|
+
workflowMeta: runtimeContext.workflowMeta,
|
|
913
|
+
useRichBlocks: node.data.config.advanced?.useRichBlocks !== false,
|
|
914
|
+
});
|
|
915
|
+
const notificationResult = await runtimeAdapters.notificationExecutor({
|
|
916
|
+
runId,
|
|
917
|
+
stepId,
|
|
918
|
+
node,
|
|
919
|
+
context: createContextSnapshot(runtimeContext),
|
|
920
|
+
deliveryRequest,
|
|
921
|
+
signal,
|
|
922
|
+
});
|
|
923
|
+
outputData = notificationResult.outputData;
|
|
924
|
+
if (!notificationResult.delivered || notificationResult.failureMessage) {
|
|
925
|
+
throw new ScenarioNodeFailureError(notificationResult.failureMessage ?? "Notification delivery failed.");
|
|
926
|
+
}
|
|
927
|
+
runtimeContext.workflowContext.workflow = {
|
|
928
|
+
...workflowRoot,
|
|
929
|
+
__meta: cloneWorkflowExecutionMeta(runtimeContext.workflowMeta),
|
|
930
|
+
};
|
|
931
|
+
}
|
|
693
932
|
if (node.type === "kafka") {
|
|
694
933
|
const resolvedTopic = resolveScenarioTemplateString(node.data.config.topic, templateValues);
|
|
695
934
|
const rules = node.data.config.rules?.filter((rule) => rule.trim()) ?? [];
|
|
@@ -727,6 +966,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
727
966
|
environment: runtimeContext.environment,
|
|
728
967
|
globals: runtimeContext.globals,
|
|
729
968
|
workflowContext: runtimeContext.workflowContext,
|
|
969
|
+
nodeMetaByLabel: runtimeContext.nodeMetaByLabel,
|
|
730
970
|
responseRoot: runtimeContext.responseRoot,
|
|
731
971
|
dbResult: runtimeContext.dbResult,
|
|
732
972
|
kafkaEvent: kafkaEventValue,
|
|
@@ -753,6 +993,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
753
993
|
environment: runtimeContext.environment,
|
|
754
994
|
globals: runtimeContext.globals,
|
|
755
995
|
workflowContext: runtimeContext.workflowContext,
|
|
996
|
+
nodeMetaByLabel: runtimeContext.nodeMetaByLabel,
|
|
756
997
|
responseRoot: runtimeContext.responseRoot,
|
|
757
998
|
dbResult: runtimeContext.dbResult,
|
|
758
999
|
kafkaEvent: runtimeContext.kafkaEvent,
|
|
@@ -831,13 +1072,14 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
831
1072
|
}
|
|
832
1073
|
}
|
|
833
1074
|
throwIfStopped(signal);
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
1075
|
+
workflowOutput =
|
|
1076
|
+
node.type === "script"
|
|
1077
|
+
? scriptHasReturnValue
|
|
1078
|
+
? scriptReturnValue
|
|
1079
|
+
: undefined
|
|
1080
|
+
: outputData;
|
|
840
1081
|
if (nodeLabel &&
|
|
1082
|
+
!RESERVED_WORKFLOW_ROOT_LABELS.has(nodeLabel) &&
|
|
841
1083
|
typeof workflowOutput !== "undefined" &&
|
|
842
1084
|
(node.type === "script" || workflowOutput !== null)) {
|
|
843
1085
|
runtimeContext.workflowContext[nodeLabel] = workflowOutput;
|
|
@@ -880,11 +1122,43 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
880
1122
|
inputData,
|
|
881
1123
|
incomingEdgeId: incomingEdgeId ?? null,
|
|
882
1124
|
});
|
|
1125
|
+
updateWorkflowMetaState({
|
|
1126
|
+
context: runtimeContext,
|
|
1127
|
+
records,
|
|
1128
|
+
localStatuses,
|
|
1129
|
+
startedAt,
|
|
1130
|
+
currentNode: title,
|
|
1131
|
+
});
|
|
883
1132
|
},
|
|
884
1133
|
});
|
|
885
1134
|
const durationMs = Math.round(Date.now() - executionStartedAtMs);
|
|
886
1135
|
if (policyResult.outcome === "failed") {
|
|
887
1136
|
localStatuses.set(stepId, "failed");
|
|
1137
|
+
const failedAt = new Date().toISOString();
|
|
1138
|
+
const nodeMeta = buildNodeExecutionMeta({
|
|
1139
|
+
status: "failed",
|
|
1140
|
+
attempts: policyResult.attempts,
|
|
1141
|
+
startedAt: new Date(executionStartedAtMs).toISOString(),
|
|
1142
|
+
endedAt: failedAt,
|
|
1143
|
+
durationMs,
|
|
1144
|
+
errorMessage: policyResult.finalErrorMessage ?? "Scenario node failed",
|
|
1145
|
+
});
|
|
1146
|
+
persistNodeExecutionMeta({
|
|
1147
|
+
context: runtimeContext,
|
|
1148
|
+
label: nodeLabel,
|
|
1149
|
+
output: typeof workflowOutput !== "undefined" ? workflowOutput : outputData,
|
|
1150
|
+
meta: nodeMeta,
|
|
1151
|
+
});
|
|
1152
|
+
updateWorkflowMetaState({
|
|
1153
|
+
context: runtimeContext,
|
|
1154
|
+
records,
|
|
1155
|
+
localStatuses,
|
|
1156
|
+
startedAt,
|
|
1157
|
+
currentNode: null,
|
|
1158
|
+
lastNode: title,
|
|
1159
|
+
failedNode: title,
|
|
1160
|
+
endedAt: failedAt,
|
|
1161
|
+
});
|
|
888
1162
|
await emit({
|
|
889
1163
|
type: "step:fail",
|
|
890
1164
|
runId,
|
|
@@ -893,7 +1167,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
893
1167
|
stepType: node.type,
|
|
894
1168
|
title,
|
|
895
1169
|
status: "failed",
|
|
896
|
-
at:
|
|
1170
|
+
at: failedAt,
|
|
897
1171
|
attempts: policyResult.attempts,
|
|
898
1172
|
currentAttempt: policyResult.attempts,
|
|
899
1173
|
maxAttempts: policyResult.maxAttempts,
|
|
@@ -903,6 +1177,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
903
1177
|
failurePolicy: executionPolicy.onFailure,
|
|
904
1178
|
continuedAfterFailure: false,
|
|
905
1179
|
errors: policyResult.errors,
|
|
1180
|
+
nodeMeta,
|
|
906
1181
|
errorMessage: policyResult.finalErrorMessage ?? "Scenario node failed",
|
|
907
1182
|
durationMs,
|
|
908
1183
|
statusCode,
|
|
@@ -931,6 +1206,33 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
931
1206
|
if (policyResult.outcome === "continued") {
|
|
932
1207
|
degradedStepIds.add(stepId);
|
|
933
1208
|
localStatuses.set(stepId, "failed_continued");
|
|
1209
|
+
const continuedAt = new Date().toISOString();
|
|
1210
|
+
const nodeMeta = buildNodeExecutionMeta({
|
|
1211
|
+
status: "failed",
|
|
1212
|
+
attempts: policyResult.attempts,
|
|
1213
|
+
startedAt: new Date(executionStartedAtMs).toISOString(),
|
|
1214
|
+
endedAt: continuedAt,
|
|
1215
|
+
durationMs,
|
|
1216
|
+
continueAfterFailure: true,
|
|
1217
|
+
errorMessage: policyResult.finalErrorMessage ??
|
|
1218
|
+
"Scenario node failed but execution continued.",
|
|
1219
|
+
});
|
|
1220
|
+
persistNodeExecutionMeta({
|
|
1221
|
+
context: runtimeContext,
|
|
1222
|
+
label: nodeLabel,
|
|
1223
|
+
output: typeof workflowOutput !== "undefined" ? workflowOutput : outputData,
|
|
1224
|
+
meta: nodeMeta,
|
|
1225
|
+
});
|
|
1226
|
+
updateWorkflowMetaState({
|
|
1227
|
+
context: runtimeContext,
|
|
1228
|
+
records,
|
|
1229
|
+
localStatuses,
|
|
1230
|
+
startedAt,
|
|
1231
|
+
currentNode: null,
|
|
1232
|
+
lastNode: title,
|
|
1233
|
+
failedNode: title,
|
|
1234
|
+
endedAt: continuedAt,
|
|
1235
|
+
});
|
|
934
1236
|
await emit({
|
|
935
1237
|
type: "step:fail",
|
|
936
1238
|
runId,
|
|
@@ -939,7 +1241,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
939
1241
|
stepType: node.type,
|
|
940
1242
|
title,
|
|
941
1243
|
status: "failed_continued",
|
|
942
|
-
at:
|
|
1244
|
+
at: continuedAt,
|
|
943
1245
|
attempts: policyResult.attempts,
|
|
944
1246
|
currentAttempt: policyResult.attempts,
|
|
945
1247
|
maxAttempts: policyResult.maxAttempts,
|
|
@@ -949,6 +1251,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
949
1251
|
failurePolicy: executionPolicy.onFailure,
|
|
950
1252
|
continuedAfterFailure: true,
|
|
951
1253
|
errors: policyResult.errors,
|
|
1254
|
+
nodeMeta,
|
|
952
1255
|
errorMessage: policyResult.finalErrorMessage ??
|
|
953
1256
|
"Scenario node failed but execution continued.",
|
|
954
1257
|
durationMs,
|
|
@@ -983,6 +1286,29 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
983
1286
|
degradedStepIds.delete(stepId);
|
|
984
1287
|
}
|
|
985
1288
|
localStatuses.set(stepId, successStatus);
|
|
1289
|
+
const succeededAt = new Date().toISOString();
|
|
1290
|
+
const nodeMeta = buildNodeExecutionMeta({
|
|
1291
|
+
status: "success",
|
|
1292
|
+
attempts: policyResult.attempts,
|
|
1293
|
+
startedAt: new Date(executionStartedAtMs).toISOString(),
|
|
1294
|
+
endedAt: succeededAt,
|
|
1295
|
+
durationMs,
|
|
1296
|
+
});
|
|
1297
|
+
persistNodeExecutionMeta({
|
|
1298
|
+
context: runtimeContext,
|
|
1299
|
+
label: nodeLabel,
|
|
1300
|
+
output: typeof workflowOutput !== "undefined" ? workflowOutput : outputData,
|
|
1301
|
+
meta: nodeMeta,
|
|
1302
|
+
});
|
|
1303
|
+
updateWorkflowMetaState({
|
|
1304
|
+
context: runtimeContext,
|
|
1305
|
+
records,
|
|
1306
|
+
localStatuses,
|
|
1307
|
+
startedAt,
|
|
1308
|
+
currentNode: null,
|
|
1309
|
+
lastNode: title,
|
|
1310
|
+
endedAt: succeededAt,
|
|
1311
|
+
});
|
|
986
1312
|
await emit({
|
|
987
1313
|
type: "step:success",
|
|
988
1314
|
runId,
|
|
@@ -991,7 +1317,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
991
1317
|
stepType: node.type,
|
|
992
1318
|
title,
|
|
993
1319
|
status: successStatus,
|
|
994
|
-
at:
|
|
1320
|
+
at: succeededAt,
|
|
995
1321
|
attempts: policyResult.attempts,
|
|
996
1322
|
currentAttempt: policyResult.attempts,
|
|
997
1323
|
maxAttempts: policyResult.maxAttempts,
|
|
@@ -1001,6 +1327,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
1001
1327
|
failurePolicy: executionPolicy.onFailure,
|
|
1002
1328
|
continuedAfterFailure: false,
|
|
1003
1329
|
errors: policyResult.errors,
|
|
1330
|
+
nodeMeta,
|
|
1004
1331
|
durationMs,
|
|
1005
1332
|
statusCode,
|
|
1006
1333
|
response,
|
|
@@ -1070,7 +1397,30 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
1070
1397
|
const skipDurationMs = isRecord(outputData) && typeof outputData.actualDurationMs === "number"
|
|
1071
1398
|
? Math.max(0, Math.round(outputData.actualDurationMs))
|
|
1072
1399
|
: Math.max(0, Math.round(Date.now() - executionStartedAtMs));
|
|
1400
|
+
const skippedAt = new Date().toISOString();
|
|
1073
1401
|
localStatuses.set(stepId, "skipped");
|
|
1402
|
+
const nodeMeta = buildNodeExecutionMeta({
|
|
1403
|
+
status: "skipped",
|
|
1404
|
+
attempts: 0,
|
|
1405
|
+
startedAt: new Date(executionStartedAtMs).toISOString(),
|
|
1406
|
+
endedAt: skippedAt,
|
|
1407
|
+
durationMs: skipDurationMs,
|
|
1408
|
+
});
|
|
1409
|
+
persistNodeExecutionMeta({
|
|
1410
|
+
context: runtimeContext,
|
|
1411
|
+
label: nodeLabel,
|
|
1412
|
+
output: typeof workflowOutput !== "undefined" ? workflowOutput : outputData,
|
|
1413
|
+
meta: nodeMeta,
|
|
1414
|
+
});
|
|
1415
|
+
updateWorkflowMetaState({
|
|
1416
|
+
context: runtimeContext,
|
|
1417
|
+
records,
|
|
1418
|
+
localStatuses,
|
|
1419
|
+
startedAt,
|
|
1420
|
+
currentNode: null,
|
|
1421
|
+
lastNode: title,
|
|
1422
|
+
endedAt: skippedAt,
|
|
1423
|
+
});
|
|
1074
1424
|
await emit({
|
|
1075
1425
|
type: "step:skip",
|
|
1076
1426
|
runId,
|
|
@@ -1078,8 +1428,9 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
1078
1428
|
stepId,
|
|
1079
1429
|
stepType: node.type,
|
|
1080
1430
|
title,
|
|
1081
|
-
at:
|
|
1431
|
+
at: skippedAt,
|
|
1082
1432
|
reason: "Execution stopped",
|
|
1433
|
+
nodeMeta,
|
|
1083
1434
|
durationMs: skipDurationMs,
|
|
1084
1435
|
inputData,
|
|
1085
1436
|
outputData,
|
|
@@ -1110,29 +1461,57 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
1110
1461
|
}
|
|
1111
1462
|
await markRemainingNodesSkipped("Node was not reached during this run.");
|
|
1112
1463
|
finalStatus = degradedStepIds.size > 0 ? "completed_with_failures" : "success";
|
|
1464
|
+
const endedAt = new Date().toISOString();
|
|
1465
|
+
const workflowMeta = updateWorkflowMetaState({
|
|
1466
|
+
context: runtimeContext,
|
|
1467
|
+
records,
|
|
1468
|
+
localStatuses,
|
|
1469
|
+
startedAt,
|
|
1470
|
+
currentNode: null,
|
|
1471
|
+
endedAt,
|
|
1472
|
+
});
|
|
1113
1473
|
await emit({
|
|
1114
1474
|
type: "scenario:end",
|
|
1115
1475
|
runId,
|
|
1116
1476
|
scenarioId: scenario.id,
|
|
1117
1477
|
status: finalStatus,
|
|
1118
|
-
at:
|
|
1478
|
+
at: endedAt,
|
|
1119
1479
|
durationMs: Date.now() - startedAtMs,
|
|
1480
|
+
workflowMeta,
|
|
1120
1481
|
});
|
|
1121
1482
|
}
|
|
1122
1483
|
catch (error) {
|
|
1123
1484
|
const isStopped = error instanceof ScenarioStopError || Boolean(signal?.aborted);
|
|
1124
1485
|
await markRemainingNodesSkipped(isStopped ? "Skipped because execution was stopped." : "Skipped after failure.");
|
|
1125
1486
|
finalStatus = isStopped ? "stopped" : "failed";
|
|
1487
|
+
const endedAt = new Date().toISOString();
|
|
1488
|
+
const workflowMeta = updateWorkflowMetaState({
|
|
1489
|
+
context: runtimeContext,
|
|
1490
|
+
records,
|
|
1491
|
+
localStatuses,
|
|
1492
|
+
startedAt,
|
|
1493
|
+
currentNode: null,
|
|
1494
|
+
endedAt,
|
|
1495
|
+
});
|
|
1126
1496
|
await emit({
|
|
1127
1497
|
type: "scenario:end",
|
|
1128
1498
|
runId,
|
|
1129
1499
|
scenarioId: scenario.id,
|
|
1130
1500
|
status: finalStatus,
|
|
1131
|
-
at:
|
|
1501
|
+
at: endedAt,
|
|
1132
1502
|
durationMs: Date.now() - startedAtMs,
|
|
1503
|
+
workflowMeta,
|
|
1133
1504
|
});
|
|
1134
1505
|
}
|
|
1135
1506
|
const endedAt = new Date().toISOString();
|
|
1507
|
+
const finalWorkflowMeta = updateWorkflowMetaState({
|
|
1508
|
+
context: runtimeContext,
|
|
1509
|
+
records,
|
|
1510
|
+
localStatuses,
|
|
1511
|
+
startedAt,
|
|
1512
|
+
currentNode: null,
|
|
1513
|
+
endedAt,
|
|
1514
|
+
});
|
|
1136
1515
|
const report = {
|
|
1137
1516
|
runId,
|
|
1138
1517
|
scenarioId: scenario.id,
|
|
@@ -1144,6 +1523,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
1144
1523
|
totalSteps: runtimeStepIds.length,
|
|
1145
1524
|
completedSteps: Array.from(localStatuses.values()).filter(isTerminalStatus).length,
|
|
1146
1525
|
context: createContextSnapshot(runtimeContext),
|
|
1526
|
+
workflowMeta: cloneWorkflowExecutionMeta(finalWorkflowMeta),
|
|
1147
1527
|
nodeExecutions: runtimeStepIds.map((stepId) => {
|
|
1148
1528
|
const descriptor = runtimeStepDescriptors.find((entry) => entry.stepId === stepId);
|
|
1149
1529
|
return (records.get(stepId) ??
|