callman-core 1.0.4 → 1.0.5
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 +170 -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/runScenario.d.ts.map +1 -1
- package/dist/scenario-runner/runScenario.js +318 -14
- 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 +43 -0
- package/dist/scenario-runner/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -31,6 +31,51 @@ const buildTitle = (node) => {
|
|
|
31
31
|
};
|
|
32
32
|
const isKafkaNode = (node) => node?.type === "kafka";
|
|
33
33
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
34
|
+
const RESERVED_WORKFLOW_ROOT_LABELS = new Set(["workflow", "__meta"]);
|
|
35
|
+
const cloneNodeExecutionMeta = (meta) => ({
|
|
36
|
+
...meta,
|
|
37
|
+
...(meta.error ? { error: { ...meta.error } } : {}),
|
|
38
|
+
});
|
|
39
|
+
const cloneWorkflowExecutionMeta = (meta) => ({
|
|
40
|
+
...meta,
|
|
41
|
+
});
|
|
42
|
+
const cloneNodeMetaByLabel = (value) => Object.fromEntries(Object.entries(value).map(([label, meta]) => [label, cloneNodeExecutionMeta(meta)]));
|
|
43
|
+
const attachNodeMetaToOutput = (output, meta) => {
|
|
44
|
+
const normalizedMeta = cloneNodeExecutionMeta(meta);
|
|
45
|
+
if (Array.isArray(output)) {
|
|
46
|
+
const nextValue = [...output];
|
|
47
|
+
nextValue.__meta = normalizedMeta;
|
|
48
|
+
return nextValue;
|
|
49
|
+
}
|
|
50
|
+
if (isRecord(output)) {
|
|
51
|
+
return {
|
|
52
|
+
...output,
|
|
53
|
+
__meta: normalizedMeta,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (output === null || typeof output === "undefined") {
|
|
57
|
+
return {
|
|
58
|
+
__meta: normalizedMeta,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return output;
|
|
62
|
+
};
|
|
63
|
+
const createInitialWorkflowMeta = ({ startedAt, totalNodes, }) => ({
|
|
64
|
+
status: "success",
|
|
65
|
+
success: true,
|
|
66
|
+
failed: false,
|
|
67
|
+
totalNodes,
|
|
68
|
+
successfulNodes: 0,
|
|
69
|
+
failedNodes: 0,
|
|
70
|
+
skippedNodes: 0,
|
|
71
|
+
totalRetries: 0,
|
|
72
|
+
startedAt,
|
|
73
|
+
endedAt: startedAt,
|
|
74
|
+
durationMs: 0,
|
|
75
|
+
currentNode: null,
|
|
76
|
+
lastNode: null,
|
|
77
|
+
failedNode: null,
|
|
78
|
+
});
|
|
34
79
|
const createInputSnapshot = (context) => ({
|
|
35
80
|
env: context.environment,
|
|
36
81
|
global: context.globals,
|
|
@@ -48,6 +93,8 @@ const createContextSnapshot = (context) => ({
|
|
|
48
93
|
environment: { ...context.environment },
|
|
49
94
|
globals: { ...context.globals },
|
|
50
95
|
workflowContext: { ...context.workflowContext },
|
|
96
|
+
nodeMetaByLabel: cloneNodeMetaByLabel(context.nodeMetaByLabel),
|
|
97
|
+
workflowMeta: cloneWorkflowExecutionMeta(context.workflowMeta),
|
|
51
98
|
responseRoot: context.responseRoot ? { ...context.responseRoot } : null,
|
|
52
99
|
lastResponse: context.lastResponse,
|
|
53
100
|
dbResult: context.dbResult,
|
|
@@ -126,11 +173,78 @@ const throwIfStopped = (signal) => {
|
|
|
126
173
|
throw new ScenarioStopError();
|
|
127
174
|
}
|
|
128
175
|
};
|
|
176
|
+
const isWorkflowFailureStatus = (status) => status === "failed" || status === "failed_continued";
|
|
177
|
+
const isWorkflowSuccessStatus = (status) => status === "success" || status === "partial_success";
|
|
178
|
+
const buildNodeExecutionMeta = ({ status, attempts, startedAt, endedAt, durationMs, continueAfterFailure = false, errorMessage, }) => ({
|
|
179
|
+
status,
|
|
180
|
+
success: status === "success",
|
|
181
|
+
failed: status === "failed",
|
|
182
|
+
skipped: status === "skipped",
|
|
183
|
+
attempts,
|
|
184
|
+
startedAt,
|
|
185
|
+
endedAt,
|
|
186
|
+
durationMs,
|
|
187
|
+
...(continueAfterFailure ? { continueAfterFailure: true } : {}),
|
|
188
|
+
...(errorMessage
|
|
189
|
+
? {
|
|
190
|
+
error: {
|
|
191
|
+
message: errorMessage,
|
|
192
|
+
type: "ScenarioNodeFailureError",
|
|
193
|
+
},
|
|
194
|
+
}
|
|
195
|
+
: {}),
|
|
196
|
+
});
|
|
197
|
+
const updateWorkflowMetaState = ({ context, records, localStatuses, startedAt, currentNode, lastNode, failedNode, endedAt = new Date().toISOString(), }) => {
|
|
198
|
+
const statuses = Array.from(localStatuses.values());
|
|
199
|
+
const successfulNodes = statuses.filter(isWorkflowSuccessStatus).length;
|
|
200
|
+
const failedNodes = statuses.filter(isWorkflowFailureStatus).length;
|
|
201
|
+
const skippedNodes = statuses.filter((status) => status === "skipped").length;
|
|
202
|
+
const totalRetries = Array.from(records.values()).reduce((sum, record) => sum + Math.max(0, record.retryCount), 0);
|
|
203
|
+
const hasPartialNodes = statuses.some((status) => status === "failed_continued" || status === "partial_success");
|
|
204
|
+
const nextStatus = failedNodes > 0 && !hasPartialNodes
|
|
205
|
+
? "failed"
|
|
206
|
+
: failedNodes > 0 || hasPartialNodes
|
|
207
|
+
? "partial"
|
|
208
|
+
: "success";
|
|
209
|
+
const nextMeta = {
|
|
210
|
+
status: nextStatus,
|
|
211
|
+
success: nextStatus === "success",
|
|
212
|
+
failed: nextStatus === "failed",
|
|
213
|
+
totalNodes: context.workflowMeta.totalNodes,
|
|
214
|
+
successfulNodes,
|
|
215
|
+
failedNodes,
|
|
216
|
+
skippedNodes,
|
|
217
|
+
totalRetries,
|
|
218
|
+
startedAt,
|
|
219
|
+
endedAt,
|
|
220
|
+
durationMs: Math.max(0, new Date(endedAt).getTime() - new Date(startedAt).getTime()),
|
|
221
|
+
currentNode: currentNode ?? context.workflowMeta.currentNode ?? null,
|
|
222
|
+
lastNode: lastNode ?? context.workflowMeta.lastNode ?? null,
|
|
223
|
+
failedNode: failedNode ?? context.workflowMeta.failedNode ?? null,
|
|
224
|
+
};
|
|
225
|
+
context.workflowMeta = nextMeta;
|
|
226
|
+
context.workflowContext.workflow = {
|
|
227
|
+
__meta: cloneWorkflowExecutionMeta(nextMeta),
|
|
228
|
+
};
|
|
229
|
+
return nextMeta;
|
|
230
|
+
};
|
|
231
|
+
const persistNodeExecutionMeta = ({ context, label, output, meta, }) => {
|
|
232
|
+
const trimmedLabel = label.trim();
|
|
233
|
+
if (!trimmedLabel) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
context.nodeMetaByLabel[trimmedLabel] = cloneNodeExecutionMeta(meta);
|
|
237
|
+
if (RESERVED_WORKFLOW_ROOT_LABELS.has(trimmedLabel)) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
context.workflowContext[trimmedLabel] = attachNodeMetaToOutput(output, meta);
|
|
241
|
+
};
|
|
129
242
|
const createStepExecutionRecord = ({ stepId, stepType, title, }) => ({
|
|
130
243
|
stepId,
|
|
131
244
|
stepType,
|
|
132
245
|
title,
|
|
133
246
|
status: "idle",
|
|
247
|
+
meta: null,
|
|
134
248
|
attempts: 0,
|
|
135
249
|
currentAttempt: null,
|
|
136
250
|
maxAttempts: 1,
|
|
@@ -201,6 +315,7 @@ const updateRecordFromEvent = (records, event) => {
|
|
|
201
315
|
stepType: event.stepType,
|
|
202
316
|
title: event.title,
|
|
203
317
|
status: "running",
|
|
318
|
+
meta: null,
|
|
204
319
|
attempts: event.attempt ?? 1,
|
|
205
320
|
currentAttempt: event.attempt ?? 1,
|
|
206
321
|
maxAttempts: event.maxAttempts ?? 1,
|
|
@@ -223,6 +338,7 @@ const updateRecordFromEvent = (records, event) => {
|
|
|
223
338
|
stepType: event.stepType,
|
|
224
339
|
title: event.title,
|
|
225
340
|
status: "retrying",
|
|
341
|
+
meta: current.meta,
|
|
226
342
|
attempts: event.attempt,
|
|
227
343
|
currentAttempt: event.attempt,
|
|
228
344
|
maxAttempts: event.maxAttempts,
|
|
@@ -257,6 +373,7 @@ const updateRecordFromEvent = (records, event) => {
|
|
|
257
373
|
stepType: event.stepType,
|
|
258
374
|
title: event.title,
|
|
259
375
|
status: "skipped",
|
|
376
|
+
meta: event.nodeMeta ?? current.meta,
|
|
260
377
|
inputData: event.inputData ?? current.inputData,
|
|
261
378
|
outputData: event.outputData ?? current.outputData,
|
|
262
379
|
incomingEdgeId: event.incomingEdgeId ?? current.incomingEdgeId,
|
|
@@ -272,6 +389,7 @@ const updateRecordFromEvent = (records, event) => {
|
|
|
272
389
|
stepType: event.stepType,
|
|
273
390
|
title: event.title,
|
|
274
391
|
status: event.status ?? (isSuccess ? "success" : "failed"),
|
|
392
|
+
meta: event.nodeMeta ?? current.meta,
|
|
275
393
|
attempts: event.attempts ?? current.attempts,
|
|
276
394
|
currentAttempt: event.currentAttempt ?? current.currentAttempt,
|
|
277
395
|
maxAttempts: event.maxAttempts ?? current.maxAttempts,
|
|
@@ -353,10 +471,20 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
353
471
|
const localStatuses = new Map(runtimeStepIds.map((stepId) => [stepId, "idle"]));
|
|
354
472
|
const records = new Map();
|
|
355
473
|
const events = [];
|
|
474
|
+
const initialWorkflowMeta = createInitialWorkflowMeta({
|
|
475
|
+
startedAt,
|
|
476
|
+
totalNodes: runtimeStepIds.length,
|
|
477
|
+
});
|
|
356
478
|
const runtimeContext = {
|
|
357
479
|
environment: { ...environment },
|
|
358
480
|
globals: { ...globals },
|
|
359
|
-
workflowContext: {
|
|
481
|
+
workflowContext: {
|
|
482
|
+
workflow: {
|
|
483
|
+
__meta: cloneWorkflowExecutionMeta(initialWorkflowMeta),
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
nodeMetaByLabel: {},
|
|
487
|
+
workflowMeta: initialWorkflowMeta,
|
|
360
488
|
responseRoot: null,
|
|
361
489
|
lastResponse: null,
|
|
362
490
|
dbResult: null,
|
|
@@ -419,6 +547,20 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
419
547
|
continue;
|
|
420
548
|
}
|
|
421
549
|
localStatuses.set(stepId, "skipped");
|
|
550
|
+
const skippedAt = new Date().toISOString();
|
|
551
|
+
const nodeMeta = buildNodeExecutionMeta({
|
|
552
|
+
status: "skipped",
|
|
553
|
+
attempts: 0,
|
|
554
|
+
startedAt: skippedAt,
|
|
555
|
+
endedAt: skippedAt,
|
|
556
|
+
durationMs: 0,
|
|
557
|
+
});
|
|
558
|
+
persistNodeExecutionMeta({
|
|
559
|
+
context: runtimeContext,
|
|
560
|
+
label: skippedNode.data.label.trim(),
|
|
561
|
+
output: null,
|
|
562
|
+
meta: nodeMeta,
|
|
563
|
+
});
|
|
422
564
|
await emit({
|
|
423
565
|
type: "step:skip",
|
|
424
566
|
runId,
|
|
@@ -426,10 +568,19 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
426
568
|
stepId,
|
|
427
569
|
stepType: skippedNode.type,
|
|
428
570
|
title: buildTitle(skippedNode),
|
|
429
|
-
at:
|
|
571
|
+
at: skippedAt,
|
|
430
572
|
reason,
|
|
573
|
+
nodeMeta,
|
|
431
574
|
inputData: createInputSnapshot(runtimeContext),
|
|
432
575
|
});
|
|
576
|
+
updateWorkflowMetaState({
|
|
577
|
+
context: runtimeContext,
|
|
578
|
+
records,
|
|
579
|
+
localStatuses,
|
|
580
|
+
startedAt,
|
|
581
|
+
currentNode: null,
|
|
582
|
+
lastNode: buildTitle(skippedNode),
|
|
583
|
+
});
|
|
433
584
|
}
|
|
434
585
|
};
|
|
435
586
|
const markRemainingNodesSkipped = async (reason) => {
|
|
@@ -484,6 +635,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
484
635
|
return;
|
|
485
636
|
}
|
|
486
637
|
const title = buildTitle(node);
|
|
638
|
+
const nodeLabel = node.data.label.trim();
|
|
487
639
|
const executionPolicy = supportsScenarioNodeExecutionPolicy(node)
|
|
488
640
|
? getScenarioNodeExecutionPolicy(node)
|
|
489
641
|
: getLegacyScenarioNodeExecutionPolicy();
|
|
@@ -510,6 +662,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
510
662
|
let conditionMode = null;
|
|
511
663
|
let conditionDebug = null;
|
|
512
664
|
let shouldMarkPartialSuccess = false;
|
|
665
|
+
let workflowOutput = undefined;
|
|
513
666
|
const resetAttemptArtifacts = () => {
|
|
514
667
|
statusCode = null;
|
|
515
668
|
response = null;
|
|
@@ -532,6 +685,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
532
685
|
conditionMode = null;
|
|
533
686
|
conditionDebug = null;
|
|
534
687
|
shouldMarkPartialSuccess = false;
|
|
688
|
+
workflowOutput = undefined;
|
|
535
689
|
};
|
|
536
690
|
const executeNodeAttempt = async (attempt, maxAttempts) => {
|
|
537
691
|
throwIfStopped(signal);
|
|
@@ -555,10 +709,18 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
555
709
|
inputData,
|
|
556
710
|
incomingEdgeId: incomingEdgeId ?? null,
|
|
557
711
|
});
|
|
712
|
+
updateWorkflowMetaState({
|
|
713
|
+
context: runtimeContext,
|
|
714
|
+
records,
|
|
715
|
+
localStatuses,
|
|
716
|
+
startedAt,
|
|
717
|
+
currentNode: title,
|
|
718
|
+
});
|
|
558
719
|
const templateValues = buildScenarioTemplateValues({
|
|
559
720
|
environment: runtimeContext.environment,
|
|
560
721
|
globals: runtimeContext.globals,
|
|
561
722
|
workflowContext: runtimeContext.workflowContext,
|
|
723
|
+
nodeMetaByLabel: runtimeContext.nodeMetaByLabel,
|
|
562
724
|
responseRoot: runtimeContext.responseRoot,
|
|
563
725
|
dbResult: runtimeContext.dbResult,
|
|
564
726
|
kafkaEvent: runtimeContext.kafkaEvent,
|
|
@@ -574,6 +736,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
574
736
|
environment: runtimeContext.environment,
|
|
575
737
|
globals: runtimeContext.globals,
|
|
576
738
|
workflowContext: runtimeContext.workflowContext,
|
|
739
|
+
nodeMetaByLabel: runtimeContext.nodeMetaByLabel,
|
|
577
740
|
responseRoot: runtimeContext.responseRoot,
|
|
578
741
|
dbResult: runtimeContext.dbResult,
|
|
579
742
|
kafkaEvent: runtimeContext.kafkaEvent,
|
|
@@ -727,6 +890,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
727
890
|
environment: runtimeContext.environment,
|
|
728
891
|
globals: runtimeContext.globals,
|
|
729
892
|
workflowContext: runtimeContext.workflowContext,
|
|
893
|
+
nodeMetaByLabel: runtimeContext.nodeMetaByLabel,
|
|
730
894
|
responseRoot: runtimeContext.responseRoot,
|
|
731
895
|
dbResult: runtimeContext.dbResult,
|
|
732
896
|
kafkaEvent: kafkaEventValue,
|
|
@@ -753,6 +917,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
753
917
|
environment: runtimeContext.environment,
|
|
754
918
|
globals: runtimeContext.globals,
|
|
755
919
|
workflowContext: runtimeContext.workflowContext,
|
|
920
|
+
nodeMetaByLabel: runtimeContext.nodeMetaByLabel,
|
|
756
921
|
responseRoot: runtimeContext.responseRoot,
|
|
757
922
|
dbResult: runtimeContext.dbResult,
|
|
758
923
|
kafkaEvent: runtimeContext.kafkaEvent,
|
|
@@ -831,13 +996,14 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
831
996
|
}
|
|
832
997
|
}
|
|
833
998
|
throwIfStopped(signal);
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
999
|
+
workflowOutput =
|
|
1000
|
+
node.type === "script"
|
|
1001
|
+
? scriptHasReturnValue
|
|
1002
|
+
? scriptReturnValue
|
|
1003
|
+
: undefined
|
|
1004
|
+
: outputData;
|
|
840
1005
|
if (nodeLabel &&
|
|
1006
|
+
!RESERVED_WORKFLOW_ROOT_LABELS.has(nodeLabel) &&
|
|
841
1007
|
typeof workflowOutput !== "undefined" &&
|
|
842
1008
|
(node.type === "script" || workflowOutput !== null)) {
|
|
843
1009
|
runtimeContext.workflowContext[nodeLabel] = workflowOutput;
|
|
@@ -880,11 +1046,43 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
880
1046
|
inputData,
|
|
881
1047
|
incomingEdgeId: incomingEdgeId ?? null,
|
|
882
1048
|
});
|
|
1049
|
+
updateWorkflowMetaState({
|
|
1050
|
+
context: runtimeContext,
|
|
1051
|
+
records,
|
|
1052
|
+
localStatuses,
|
|
1053
|
+
startedAt,
|
|
1054
|
+
currentNode: title,
|
|
1055
|
+
});
|
|
883
1056
|
},
|
|
884
1057
|
});
|
|
885
1058
|
const durationMs = Math.round(Date.now() - executionStartedAtMs);
|
|
886
1059
|
if (policyResult.outcome === "failed") {
|
|
887
1060
|
localStatuses.set(stepId, "failed");
|
|
1061
|
+
const failedAt = new Date().toISOString();
|
|
1062
|
+
const nodeMeta = buildNodeExecutionMeta({
|
|
1063
|
+
status: "failed",
|
|
1064
|
+
attempts: policyResult.attempts,
|
|
1065
|
+
startedAt: new Date(executionStartedAtMs).toISOString(),
|
|
1066
|
+
endedAt: failedAt,
|
|
1067
|
+
durationMs,
|
|
1068
|
+
errorMessage: policyResult.finalErrorMessage ?? "Scenario node failed",
|
|
1069
|
+
});
|
|
1070
|
+
persistNodeExecutionMeta({
|
|
1071
|
+
context: runtimeContext,
|
|
1072
|
+
label: nodeLabel,
|
|
1073
|
+
output: typeof workflowOutput !== "undefined" ? workflowOutput : outputData,
|
|
1074
|
+
meta: nodeMeta,
|
|
1075
|
+
});
|
|
1076
|
+
updateWorkflowMetaState({
|
|
1077
|
+
context: runtimeContext,
|
|
1078
|
+
records,
|
|
1079
|
+
localStatuses,
|
|
1080
|
+
startedAt,
|
|
1081
|
+
currentNode: null,
|
|
1082
|
+
lastNode: title,
|
|
1083
|
+
failedNode: title,
|
|
1084
|
+
endedAt: failedAt,
|
|
1085
|
+
});
|
|
888
1086
|
await emit({
|
|
889
1087
|
type: "step:fail",
|
|
890
1088
|
runId,
|
|
@@ -893,7 +1091,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
893
1091
|
stepType: node.type,
|
|
894
1092
|
title,
|
|
895
1093
|
status: "failed",
|
|
896
|
-
at:
|
|
1094
|
+
at: failedAt,
|
|
897
1095
|
attempts: policyResult.attempts,
|
|
898
1096
|
currentAttempt: policyResult.attempts,
|
|
899
1097
|
maxAttempts: policyResult.maxAttempts,
|
|
@@ -903,6 +1101,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
903
1101
|
failurePolicy: executionPolicy.onFailure,
|
|
904
1102
|
continuedAfterFailure: false,
|
|
905
1103
|
errors: policyResult.errors,
|
|
1104
|
+
nodeMeta,
|
|
906
1105
|
errorMessage: policyResult.finalErrorMessage ?? "Scenario node failed",
|
|
907
1106
|
durationMs,
|
|
908
1107
|
statusCode,
|
|
@@ -931,6 +1130,33 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
931
1130
|
if (policyResult.outcome === "continued") {
|
|
932
1131
|
degradedStepIds.add(stepId);
|
|
933
1132
|
localStatuses.set(stepId, "failed_continued");
|
|
1133
|
+
const continuedAt = new Date().toISOString();
|
|
1134
|
+
const nodeMeta = buildNodeExecutionMeta({
|
|
1135
|
+
status: "failed",
|
|
1136
|
+
attempts: policyResult.attempts,
|
|
1137
|
+
startedAt: new Date(executionStartedAtMs).toISOString(),
|
|
1138
|
+
endedAt: continuedAt,
|
|
1139
|
+
durationMs,
|
|
1140
|
+
continueAfterFailure: true,
|
|
1141
|
+
errorMessage: policyResult.finalErrorMessage ??
|
|
1142
|
+
"Scenario node failed but execution continued.",
|
|
1143
|
+
});
|
|
1144
|
+
persistNodeExecutionMeta({
|
|
1145
|
+
context: runtimeContext,
|
|
1146
|
+
label: nodeLabel,
|
|
1147
|
+
output: typeof workflowOutput !== "undefined" ? workflowOutput : outputData,
|
|
1148
|
+
meta: nodeMeta,
|
|
1149
|
+
});
|
|
1150
|
+
updateWorkflowMetaState({
|
|
1151
|
+
context: runtimeContext,
|
|
1152
|
+
records,
|
|
1153
|
+
localStatuses,
|
|
1154
|
+
startedAt,
|
|
1155
|
+
currentNode: null,
|
|
1156
|
+
lastNode: title,
|
|
1157
|
+
failedNode: title,
|
|
1158
|
+
endedAt: continuedAt,
|
|
1159
|
+
});
|
|
934
1160
|
await emit({
|
|
935
1161
|
type: "step:fail",
|
|
936
1162
|
runId,
|
|
@@ -939,7 +1165,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
939
1165
|
stepType: node.type,
|
|
940
1166
|
title,
|
|
941
1167
|
status: "failed_continued",
|
|
942
|
-
at:
|
|
1168
|
+
at: continuedAt,
|
|
943
1169
|
attempts: policyResult.attempts,
|
|
944
1170
|
currentAttempt: policyResult.attempts,
|
|
945
1171
|
maxAttempts: policyResult.maxAttempts,
|
|
@@ -949,6 +1175,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
949
1175
|
failurePolicy: executionPolicy.onFailure,
|
|
950
1176
|
continuedAfterFailure: true,
|
|
951
1177
|
errors: policyResult.errors,
|
|
1178
|
+
nodeMeta,
|
|
952
1179
|
errorMessage: policyResult.finalErrorMessage ??
|
|
953
1180
|
"Scenario node failed but execution continued.",
|
|
954
1181
|
durationMs,
|
|
@@ -983,6 +1210,29 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
983
1210
|
degradedStepIds.delete(stepId);
|
|
984
1211
|
}
|
|
985
1212
|
localStatuses.set(stepId, successStatus);
|
|
1213
|
+
const succeededAt = new Date().toISOString();
|
|
1214
|
+
const nodeMeta = buildNodeExecutionMeta({
|
|
1215
|
+
status: "success",
|
|
1216
|
+
attempts: policyResult.attempts,
|
|
1217
|
+
startedAt: new Date(executionStartedAtMs).toISOString(),
|
|
1218
|
+
endedAt: succeededAt,
|
|
1219
|
+
durationMs,
|
|
1220
|
+
});
|
|
1221
|
+
persistNodeExecutionMeta({
|
|
1222
|
+
context: runtimeContext,
|
|
1223
|
+
label: nodeLabel,
|
|
1224
|
+
output: typeof workflowOutput !== "undefined" ? workflowOutput : outputData,
|
|
1225
|
+
meta: nodeMeta,
|
|
1226
|
+
});
|
|
1227
|
+
updateWorkflowMetaState({
|
|
1228
|
+
context: runtimeContext,
|
|
1229
|
+
records,
|
|
1230
|
+
localStatuses,
|
|
1231
|
+
startedAt,
|
|
1232
|
+
currentNode: null,
|
|
1233
|
+
lastNode: title,
|
|
1234
|
+
endedAt: succeededAt,
|
|
1235
|
+
});
|
|
986
1236
|
await emit({
|
|
987
1237
|
type: "step:success",
|
|
988
1238
|
runId,
|
|
@@ -991,7 +1241,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
991
1241
|
stepType: node.type,
|
|
992
1242
|
title,
|
|
993
1243
|
status: successStatus,
|
|
994
|
-
at:
|
|
1244
|
+
at: succeededAt,
|
|
995
1245
|
attempts: policyResult.attempts,
|
|
996
1246
|
currentAttempt: policyResult.attempts,
|
|
997
1247
|
maxAttempts: policyResult.maxAttempts,
|
|
@@ -1001,6 +1251,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
1001
1251
|
failurePolicy: executionPolicy.onFailure,
|
|
1002
1252
|
continuedAfterFailure: false,
|
|
1003
1253
|
errors: policyResult.errors,
|
|
1254
|
+
nodeMeta,
|
|
1004
1255
|
durationMs,
|
|
1005
1256
|
statusCode,
|
|
1006
1257
|
response,
|
|
@@ -1070,7 +1321,30 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
1070
1321
|
const skipDurationMs = isRecord(outputData) && typeof outputData.actualDurationMs === "number"
|
|
1071
1322
|
? Math.max(0, Math.round(outputData.actualDurationMs))
|
|
1072
1323
|
: Math.max(0, Math.round(Date.now() - executionStartedAtMs));
|
|
1324
|
+
const skippedAt = new Date().toISOString();
|
|
1073
1325
|
localStatuses.set(stepId, "skipped");
|
|
1326
|
+
const nodeMeta = buildNodeExecutionMeta({
|
|
1327
|
+
status: "skipped",
|
|
1328
|
+
attempts: 0,
|
|
1329
|
+
startedAt: new Date(executionStartedAtMs).toISOString(),
|
|
1330
|
+
endedAt: skippedAt,
|
|
1331
|
+
durationMs: skipDurationMs,
|
|
1332
|
+
});
|
|
1333
|
+
persistNodeExecutionMeta({
|
|
1334
|
+
context: runtimeContext,
|
|
1335
|
+
label: nodeLabel,
|
|
1336
|
+
output: typeof workflowOutput !== "undefined" ? workflowOutput : outputData,
|
|
1337
|
+
meta: nodeMeta,
|
|
1338
|
+
});
|
|
1339
|
+
updateWorkflowMetaState({
|
|
1340
|
+
context: runtimeContext,
|
|
1341
|
+
records,
|
|
1342
|
+
localStatuses,
|
|
1343
|
+
startedAt,
|
|
1344
|
+
currentNode: null,
|
|
1345
|
+
lastNode: title,
|
|
1346
|
+
endedAt: skippedAt,
|
|
1347
|
+
});
|
|
1074
1348
|
await emit({
|
|
1075
1349
|
type: "step:skip",
|
|
1076
1350
|
runId,
|
|
@@ -1078,8 +1352,9 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
1078
1352
|
stepId,
|
|
1079
1353
|
stepType: node.type,
|
|
1080
1354
|
title,
|
|
1081
|
-
at:
|
|
1355
|
+
at: skippedAt,
|
|
1082
1356
|
reason: "Execution stopped",
|
|
1357
|
+
nodeMeta,
|
|
1083
1358
|
durationMs: skipDurationMs,
|
|
1084
1359
|
inputData,
|
|
1085
1360
|
outputData,
|
|
@@ -1110,29 +1385,57 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
1110
1385
|
}
|
|
1111
1386
|
await markRemainingNodesSkipped("Node was not reached during this run.");
|
|
1112
1387
|
finalStatus = degradedStepIds.size > 0 ? "completed_with_failures" : "success";
|
|
1388
|
+
const endedAt = new Date().toISOString();
|
|
1389
|
+
const workflowMeta = updateWorkflowMetaState({
|
|
1390
|
+
context: runtimeContext,
|
|
1391
|
+
records,
|
|
1392
|
+
localStatuses,
|
|
1393
|
+
startedAt,
|
|
1394
|
+
currentNode: null,
|
|
1395
|
+
endedAt,
|
|
1396
|
+
});
|
|
1113
1397
|
await emit({
|
|
1114
1398
|
type: "scenario:end",
|
|
1115
1399
|
runId,
|
|
1116
1400
|
scenarioId: scenario.id,
|
|
1117
1401
|
status: finalStatus,
|
|
1118
|
-
at:
|
|
1402
|
+
at: endedAt,
|
|
1119
1403
|
durationMs: Date.now() - startedAtMs,
|
|
1404
|
+
workflowMeta,
|
|
1120
1405
|
});
|
|
1121
1406
|
}
|
|
1122
1407
|
catch (error) {
|
|
1123
1408
|
const isStopped = error instanceof ScenarioStopError || Boolean(signal?.aborted);
|
|
1124
1409
|
await markRemainingNodesSkipped(isStopped ? "Skipped because execution was stopped." : "Skipped after failure.");
|
|
1125
1410
|
finalStatus = isStopped ? "stopped" : "failed";
|
|
1411
|
+
const endedAt = new Date().toISOString();
|
|
1412
|
+
const workflowMeta = updateWorkflowMetaState({
|
|
1413
|
+
context: runtimeContext,
|
|
1414
|
+
records,
|
|
1415
|
+
localStatuses,
|
|
1416
|
+
startedAt,
|
|
1417
|
+
currentNode: null,
|
|
1418
|
+
endedAt,
|
|
1419
|
+
});
|
|
1126
1420
|
await emit({
|
|
1127
1421
|
type: "scenario:end",
|
|
1128
1422
|
runId,
|
|
1129
1423
|
scenarioId: scenario.id,
|
|
1130
1424
|
status: finalStatus,
|
|
1131
|
-
at:
|
|
1425
|
+
at: endedAt,
|
|
1132
1426
|
durationMs: Date.now() - startedAtMs,
|
|
1427
|
+
workflowMeta,
|
|
1133
1428
|
});
|
|
1134
1429
|
}
|
|
1135
1430
|
const endedAt = new Date().toISOString();
|
|
1431
|
+
const finalWorkflowMeta = updateWorkflowMetaState({
|
|
1432
|
+
context: runtimeContext,
|
|
1433
|
+
records,
|
|
1434
|
+
localStatuses,
|
|
1435
|
+
startedAt,
|
|
1436
|
+
currentNode: null,
|
|
1437
|
+
endedAt,
|
|
1438
|
+
});
|
|
1136
1439
|
const report = {
|
|
1137
1440
|
runId,
|
|
1138
1441
|
scenarioId: scenario.id,
|
|
@@ -1144,6 +1447,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
1144
1447
|
totalSteps: runtimeStepIds.length,
|
|
1145
1448
|
completedSteps: Array.from(localStatuses.values()).filter(isTerminalStatus).length,
|
|
1146
1449
|
context: createContextSnapshot(runtimeContext),
|
|
1450
|
+
workflowMeta: cloneWorkflowExecutionMeta(finalWorkflowMeta),
|
|
1147
1451
|
nodeExecutions: runtimeStepIds.map((stepId) => {
|
|
1148
1452
|
const descriptor = runtimeStepDescriptors.find((entry) => entry.stepId === stepId);
|
|
1149
1453
|
return (records.get(stepId) ??
|