callman-core 1.0.3 → 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 +176 -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/executionPolicyRunner.d.ts +2 -1
- package/dist/scenario-runner/executionPolicyRunner.d.ts.map +1 -1
- package/dist/scenario-runner/executionPolicyRunner.js +2 -1
- package/dist/scenario-runner/executionPolicyRunner.js.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 +1 -0
- package/dist/scenario-runner/nodePolicy.js.map +1 -1
- package/dist/scenario-runner/retryExecutor.d.ts +2 -1
- package/dist/scenario-runner/retryExecutor.d.ts.map +1 -1
- package/dist/scenario-runner/retryExecutor.js +4 -1
- package/dist/scenario-runner/retryExecutor.js.map +1 -1
- package/dist/scenario-runner/runScenario.d.ts.map +1 -1
- package/dist/scenario-runner/runScenario.js +346 -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 +59 -3
- package/dist/scenario-runner/types.d.ts.map +1 -1
- package/dist/scenario-runner/waitNodeExecutor.d.ts +16 -0
- package/dist/scenario-runner/waitNodeExecutor.d.ts.map +1 -0
- package/dist/scenario-runner/waitNodeExecutor.js +65 -0
- package/dist/scenario-runner/waitNodeExecutor.js.map +1 -0
- package/package.json +3 -2
- package/tests/waitNode.test.mjs +311 -0
|
@@ -3,6 +3,7 @@ import { createSubScenarioStepId, buildRuntimeStepDescriptors, DEFAULT_END_LOOP_
|
|
|
3
3
|
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
|
+
import { executeWaitNode, ScenarioWaitNodeCancelledError, ScenarioWaitNodeValidationError, } from "./waitNodeExecutor.js";
|
|
6
7
|
class ScenarioStopError extends Error {
|
|
7
8
|
constructor() {
|
|
8
9
|
super("Scenario execution stopped");
|
|
@@ -30,6 +31,51 @@ const buildTitle = (node) => {
|
|
|
30
31
|
};
|
|
31
32
|
const isKafkaNode = (node) => node?.type === "kafka";
|
|
32
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
|
+
});
|
|
33
79
|
const createInputSnapshot = (context) => ({
|
|
34
80
|
env: context.environment,
|
|
35
81
|
global: context.globals,
|
|
@@ -47,6 +93,8 @@ const createContextSnapshot = (context) => ({
|
|
|
47
93
|
environment: { ...context.environment },
|
|
48
94
|
globals: { ...context.globals },
|
|
49
95
|
workflowContext: { ...context.workflowContext },
|
|
96
|
+
nodeMetaByLabel: cloneNodeMetaByLabel(context.nodeMetaByLabel),
|
|
97
|
+
workflowMeta: cloneWorkflowExecutionMeta(context.workflowMeta),
|
|
50
98
|
responseRoot: context.responseRoot ? { ...context.responseRoot } : null,
|
|
51
99
|
lastResponse: context.lastResponse,
|
|
52
100
|
dbResult: context.dbResult,
|
|
@@ -125,11 +173,78 @@ const throwIfStopped = (signal) => {
|
|
|
125
173
|
throw new ScenarioStopError();
|
|
126
174
|
}
|
|
127
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
|
+
};
|
|
128
242
|
const createStepExecutionRecord = ({ stepId, stepType, title, }) => ({
|
|
129
243
|
stepId,
|
|
130
244
|
stepType,
|
|
131
245
|
title,
|
|
132
246
|
status: "idle",
|
|
247
|
+
meta: null,
|
|
133
248
|
attempts: 0,
|
|
134
249
|
currentAttempt: null,
|
|
135
250
|
maxAttempts: 1,
|
|
@@ -200,6 +315,7 @@ const updateRecordFromEvent = (records, event) => {
|
|
|
200
315
|
stepType: event.stepType,
|
|
201
316
|
title: event.title,
|
|
202
317
|
status: "running",
|
|
318
|
+
meta: null,
|
|
203
319
|
attempts: event.attempt ?? 1,
|
|
204
320
|
currentAttempt: event.attempt ?? 1,
|
|
205
321
|
maxAttempts: event.maxAttempts ?? 1,
|
|
@@ -222,6 +338,7 @@ const updateRecordFromEvent = (records, event) => {
|
|
|
222
338
|
stepType: event.stepType,
|
|
223
339
|
title: event.title,
|
|
224
340
|
status: "retrying",
|
|
341
|
+
meta: current.meta,
|
|
225
342
|
attempts: event.attempt,
|
|
226
343
|
currentAttempt: event.attempt,
|
|
227
344
|
maxAttempts: event.maxAttempts,
|
|
@@ -256,8 +373,11 @@ const updateRecordFromEvent = (records, event) => {
|
|
|
256
373
|
stepType: event.stepType,
|
|
257
374
|
title: event.title,
|
|
258
375
|
status: "skipped",
|
|
376
|
+
meta: event.nodeMeta ?? current.meta,
|
|
259
377
|
inputData: event.inputData ?? current.inputData,
|
|
378
|
+
outputData: event.outputData ?? current.outputData,
|
|
260
379
|
incomingEdgeId: event.incomingEdgeId ?? current.incomingEdgeId,
|
|
380
|
+
durationMs: event.durationMs ?? current.durationMs,
|
|
261
381
|
startedAt: current.startedAt ?? event.at,
|
|
262
382
|
completedAt: event.at,
|
|
263
383
|
});
|
|
@@ -269,6 +389,7 @@ const updateRecordFromEvent = (records, event) => {
|
|
|
269
389
|
stepType: event.stepType,
|
|
270
390
|
title: event.title,
|
|
271
391
|
status: event.status ?? (isSuccess ? "success" : "failed"),
|
|
392
|
+
meta: event.nodeMeta ?? current.meta,
|
|
272
393
|
attempts: event.attempts ?? current.attempts,
|
|
273
394
|
currentAttempt: event.currentAttempt ?? current.currentAttempt,
|
|
274
395
|
maxAttempts: event.maxAttempts ?? current.maxAttempts,
|
|
@@ -350,10 +471,20 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
350
471
|
const localStatuses = new Map(runtimeStepIds.map((stepId) => [stepId, "idle"]));
|
|
351
472
|
const records = new Map();
|
|
352
473
|
const events = [];
|
|
474
|
+
const initialWorkflowMeta = createInitialWorkflowMeta({
|
|
475
|
+
startedAt,
|
|
476
|
+
totalNodes: runtimeStepIds.length,
|
|
477
|
+
});
|
|
353
478
|
const runtimeContext = {
|
|
354
479
|
environment: { ...environment },
|
|
355
480
|
globals: { ...globals },
|
|
356
|
-
workflowContext: {
|
|
481
|
+
workflowContext: {
|
|
482
|
+
workflow: {
|
|
483
|
+
__meta: cloneWorkflowExecutionMeta(initialWorkflowMeta),
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
nodeMetaByLabel: {},
|
|
487
|
+
workflowMeta: initialWorkflowMeta,
|
|
357
488
|
responseRoot: null,
|
|
358
489
|
lastResponse: null,
|
|
359
490
|
dbResult: null,
|
|
@@ -416,6 +547,20 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
416
547
|
continue;
|
|
417
548
|
}
|
|
418
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
|
+
});
|
|
419
564
|
await emit({
|
|
420
565
|
type: "step:skip",
|
|
421
566
|
runId,
|
|
@@ -423,10 +568,19 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
423
568
|
stepId,
|
|
424
569
|
stepType: skippedNode.type,
|
|
425
570
|
title: buildTitle(skippedNode),
|
|
426
|
-
at:
|
|
571
|
+
at: skippedAt,
|
|
427
572
|
reason,
|
|
573
|
+
nodeMeta,
|
|
428
574
|
inputData: createInputSnapshot(runtimeContext),
|
|
429
575
|
});
|
|
576
|
+
updateWorkflowMetaState({
|
|
577
|
+
context: runtimeContext,
|
|
578
|
+
records,
|
|
579
|
+
localStatuses,
|
|
580
|
+
startedAt,
|
|
581
|
+
currentNode: null,
|
|
582
|
+
lastNode: buildTitle(skippedNode),
|
|
583
|
+
});
|
|
430
584
|
}
|
|
431
585
|
};
|
|
432
586
|
const markRemainingNodesSkipped = async (reason) => {
|
|
@@ -481,6 +635,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
481
635
|
return;
|
|
482
636
|
}
|
|
483
637
|
const title = buildTitle(node);
|
|
638
|
+
const nodeLabel = node.data.label.trim();
|
|
484
639
|
const executionPolicy = supportsScenarioNodeExecutionPolicy(node)
|
|
485
640
|
? getScenarioNodeExecutionPolicy(node)
|
|
486
641
|
: getLegacyScenarioNodeExecutionPolicy();
|
|
@@ -507,6 +662,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
507
662
|
let conditionMode = null;
|
|
508
663
|
let conditionDebug = null;
|
|
509
664
|
let shouldMarkPartialSuccess = false;
|
|
665
|
+
let workflowOutput = undefined;
|
|
510
666
|
const resetAttemptArtifacts = () => {
|
|
511
667
|
statusCode = null;
|
|
512
668
|
response = null;
|
|
@@ -529,6 +685,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
529
685
|
conditionMode = null;
|
|
530
686
|
conditionDebug = null;
|
|
531
687
|
shouldMarkPartialSuccess = false;
|
|
688
|
+
workflowOutput = undefined;
|
|
532
689
|
};
|
|
533
690
|
const executeNodeAttempt = async (attempt, maxAttempts) => {
|
|
534
691
|
throwIfStopped(signal);
|
|
@@ -552,10 +709,18 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
552
709
|
inputData,
|
|
553
710
|
incomingEdgeId: incomingEdgeId ?? null,
|
|
554
711
|
});
|
|
712
|
+
updateWorkflowMetaState({
|
|
713
|
+
context: runtimeContext,
|
|
714
|
+
records,
|
|
715
|
+
localStatuses,
|
|
716
|
+
startedAt,
|
|
717
|
+
currentNode: title,
|
|
718
|
+
});
|
|
555
719
|
const templateValues = buildScenarioTemplateValues({
|
|
556
720
|
environment: runtimeContext.environment,
|
|
557
721
|
globals: runtimeContext.globals,
|
|
558
722
|
workflowContext: runtimeContext.workflowContext,
|
|
723
|
+
nodeMetaByLabel: runtimeContext.nodeMetaByLabel,
|
|
559
724
|
responseRoot: runtimeContext.responseRoot,
|
|
560
725
|
dbResult: runtimeContext.dbResult,
|
|
561
726
|
kafkaEvent: runtimeContext.kafkaEvent,
|
|
@@ -571,6 +736,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
571
736
|
environment: runtimeContext.environment,
|
|
572
737
|
globals: runtimeContext.globals,
|
|
573
738
|
workflowContext: runtimeContext.workflowContext,
|
|
739
|
+
nodeMetaByLabel: runtimeContext.nodeMetaByLabel,
|
|
574
740
|
responseRoot: runtimeContext.responseRoot,
|
|
575
741
|
dbResult: runtimeContext.dbResult,
|
|
576
742
|
kafkaEvent: runtimeContext.kafkaEvent,
|
|
@@ -630,6 +796,25 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
630
796
|
throw new ScenarioNodeFailureError(dbResult.failureMessage);
|
|
631
797
|
}
|
|
632
798
|
}
|
|
799
|
+
if (node.type === "wait") {
|
|
800
|
+
try {
|
|
801
|
+
outputData = await executeWaitNode({
|
|
802
|
+
durationMs: node.data.config.durationMs,
|
|
803
|
+
signal,
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
catch (error) {
|
|
807
|
+
if (error instanceof ScenarioWaitNodeCancelledError) {
|
|
808
|
+
outputData = error.summary;
|
|
809
|
+
throw new ScenarioStopError();
|
|
810
|
+
}
|
|
811
|
+
if (error instanceof ScenarioWaitNodeValidationError) {
|
|
812
|
+
outputData = error.summary;
|
|
813
|
+
throw new ScenarioNodeFailureError(error.message);
|
|
814
|
+
}
|
|
815
|
+
throw error;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
633
818
|
if (node.type === "script") {
|
|
634
819
|
const scriptResult = await runtimeAdapters.scriptExecutor({
|
|
635
820
|
runId,
|
|
@@ -705,6 +890,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
705
890
|
environment: runtimeContext.environment,
|
|
706
891
|
globals: runtimeContext.globals,
|
|
707
892
|
workflowContext: runtimeContext.workflowContext,
|
|
893
|
+
nodeMetaByLabel: runtimeContext.nodeMetaByLabel,
|
|
708
894
|
responseRoot: runtimeContext.responseRoot,
|
|
709
895
|
dbResult: runtimeContext.dbResult,
|
|
710
896
|
kafkaEvent: kafkaEventValue,
|
|
@@ -731,6 +917,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
731
917
|
environment: runtimeContext.environment,
|
|
732
918
|
globals: runtimeContext.globals,
|
|
733
919
|
workflowContext: runtimeContext.workflowContext,
|
|
920
|
+
nodeMetaByLabel: runtimeContext.nodeMetaByLabel,
|
|
734
921
|
responseRoot: runtimeContext.responseRoot,
|
|
735
922
|
dbResult: runtimeContext.dbResult,
|
|
736
923
|
kafkaEvent: runtimeContext.kafkaEvent,
|
|
@@ -809,13 +996,14 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
809
996
|
}
|
|
810
997
|
}
|
|
811
998
|
throwIfStopped(signal);
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
999
|
+
workflowOutput =
|
|
1000
|
+
node.type === "script"
|
|
1001
|
+
? scriptHasReturnValue
|
|
1002
|
+
? scriptReturnValue
|
|
1003
|
+
: undefined
|
|
1004
|
+
: outputData;
|
|
818
1005
|
if (nodeLabel &&
|
|
1006
|
+
!RESERVED_WORKFLOW_ROOT_LABELS.has(nodeLabel) &&
|
|
819
1007
|
typeof workflowOutput !== "undefined" &&
|
|
820
1008
|
(node.type === "script" || workflowOutput !== null)) {
|
|
821
1009
|
runtimeContext.workflowContext[nodeLabel] = workflowOutput;
|
|
@@ -837,6 +1025,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
837
1025
|
policy: executionPolicy,
|
|
838
1026
|
execute: executeNodeAttempt,
|
|
839
1027
|
wait: waitForDuration,
|
|
1028
|
+
shouldRethrow: (error) => error instanceof ScenarioStopError,
|
|
840
1029
|
onRetry: async (event) => {
|
|
841
1030
|
localStatuses.set(stepId, "retrying");
|
|
842
1031
|
await emit({
|
|
@@ -857,11 +1046,43 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
857
1046
|
inputData,
|
|
858
1047
|
incomingEdgeId: incomingEdgeId ?? null,
|
|
859
1048
|
});
|
|
1049
|
+
updateWorkflowMetaState({
|
|
1050
|
+
context: runtimeContext,
|
|
1051
|
+
records,
|
|
1052
|
+
localStatuses,
|
|
1053
|
+
startedAt,
|
|
1054
|
+
currentNode: title,
|
|
1055
|
+
});
|
|
860
1056
|
},
|
|
861
1057
|
});
|
|
862
1058
|
const durationMs = Math.round(Date.now() - executionStartedAtMs);
|
|
863
1059
|
if (policyResult.outcome === "failed") {
|
|
864
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
|
+
});
|
|
865
1086
|
await emit({
|
|
866
1087
|
type: "step:fail",
|
|
867
1088
|
runId,
|
|
@@ -870,7 +1091,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
870
1091
|
stepType: node.type,
|
|
871
1092
|
title,
|
|
872
1093
|
status: "failed",
|
|
873
|
-
at:
|
|
1094
|
+
at: failedAt,
|
|
874
1095
|
attempts: policyResult.attempts,
|
|
875
1096
|
currentAttempt: policyResult.attempts,
|
|
876
1097
|
maxAttempts: policyResult.maxAttempts,
|
|
@@ -880,6 +1101,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
880
1101
|
failurePolicy: executionPolicy.onFailure,
|
|
881
1102
|
continuedAfterFailure: false,
|
|
882
1103
|
errors: policyResult.errors,
|
|
1104
|
+
nodeMeta,
|
|
883
1105
|
errorMessage: policyResult.finalErrorMessage ?? "Scenario node failed",
|
|
884
1106
|
durationMs,
|
|
885
1107
|
statusCode,
|
|
@@ -908,6 +1130,33 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
908
1130
|
if (policyResult.outcome === "continued") {
|
|
909
1131
|
degradedStepIds.add(stepId);
|
|
910
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
|
+
});
|
|
911
1160
|
await emit({
|
|
912
1161
|
type: "step:fail",
|
|
913
1162
|
runId,
|
|
@@ -916,7 +1165,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
916
1165
|
stepType: node.type,
|
|
917
1166
|
title,
|
|
918
1167
|
status: "failed_continued",
|
|
919
|
-
at:
|
|
1168
|
+
at: continuedAt,
|
|
920
1169
|
attempts: policyResult.attempts,
|
|
921
1170
|
currentAttempt: policyResult.attempts,
|
|
922
1171
|
maxAttempts: policyResult.maxAttempts,
|
|
@@ -926,6 +1175,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
926
1175
|
failurePolicy: executionPolicy.onFailure,
|
|
927
1176
|
continuedAfterFailure: true,
|
|
928
1177
|
errors: policyResult.errors,
|
|
1178
|
+
nodeMeta,
|
|
929
1179
|
errorMessage: policyResult.finalErrorMessage ??
|
|
930
1180
|
"Scenario node failed but execution continued.",
|
|
931
1181
|
durationMs,
|
|
@@ -960,6 +1210,29 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
960
1210
|
degradedStepIds.delete(stepId);
|
|
961
1211
|
}
|
|
962
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
|
+
});
|
|
963
1236
|
await emit({
|
|
964
1237
|
type: "step:success",
|
|
965
1238
|
runId,
|
|
@@ -968,7 +1241,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
968
1241
|
stepType: node.type,
|
|
969
1242
|
title,
|
|
970
1243
|
status: successStatus,
|
|
971
|
-
at:
|
|
1244
|
+
at: succeededAt,
|
|
972
1245
|
attempts: policyResult.attempts,
|
|
973
1246
|
currentAttempt: policyResult.attempts,
|
|
974
1247
|
maxAttempts: policyResult.maxAttempts,
|
|
@@ -978,6 +1251,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
978
1251
|
failurePolicy: executionPolicy.onFailure,
|
|
979
1252
|
continuedAfterFailure: false,
|
|
980
1253
|
errors: policyResult.errors,
|
|
1254
|
+
nodeMeta,
|
|
981
1255
|
durationMs,
|
|
982
1256
|
statusCode,
|
|
983
1257
|
response,
|
|
@@ -1044,7 +1318,33 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
1044
1318
|
if (error instanceof ScenarioStopError) {
|
|
1045
1319
|
const stepState = localStatuses.get(stepId);
|
|
1046
1320
|
if (stepState === "running" || stepState === "retrying") {
|
|
1321
|
+
const skipDurationMs = isRecord(outputData) && typeof outputData.actualDurationMs === "number"
|
|
1322
|
+
? Math.max(0, Math.round(outputData.actualDurationMs))
|
|
1323
|
+
: Math.max(0, Math.round(Date.now() - executionStartedAtMs));
|
|
1324
|
+
const skippedAt = new Date().toISOString();
|
|
1047
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
|
+
});
|
|
1048
1348
|
await emit({
|
|
1049
1349
|
type: "step:skip",
|
|
1050
1350
|
runId,
|
|
@@ -1052,9 +1352,12 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
1052
1352
|
stepId,
|
|
1053
1353
|
stepType: node.type,
|
|
1054
1354
|
title,
|
|
1055
|
-
at:
|
|
1355
|
+
at: skippedAt,
|
|
1056
1356
|
reason: "Execution stopped",
|
|
1357
|
+
nodeMeta,
|
|
1358
|
+
durationMs: skipDurationMs,
|
|
1057
1359
|
inputData,
|
|
1360
|
+
outputData,
|
|
1058
1361
|
incomingEdgeId: incomingEdgeId ?? null,
|
|
1059
1362
|
});
|
|
1060
1363
|
}
|
|
@@ -1082,29 +1385,57 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
1082
1385
|
}
|
|
1083
1386
|
await markRemainingNodesSkipped("Node was not reached during this run.");
|
|
1084
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
|
+
});
|
|
1085
1397
|
await emit({
|
|
1086
1398
|
type: "scenario:end",
|
|
1087
1399
|
runId,
|
|
1088
1400
|
scenarioId: scenario.id,
|
|
1089
1401
|
status: finalStatus,
|
|
1090
|
-
at:
|
|
1402
|
+
at: endedAt,
|
|
1091
1403
|
durationMs: Date.now() - startedAtMs,
|
|
1404
|
+
workflowMeta,
|
|
1092
1405
|
});
|
|
1093
1406
|
}
|
|
1094
1407
|
catch (error) {
|
|
1095
1408
|
const isStopped = error instanceof ScenarioStopError || Boolean(signal?.aborted);
|
|
1096
1409
|
await markRemainingNodesSkipped(isStopped ? "Skipped because execution was stopped." : "Skipped after failure.");
|
|
1097
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
|
+
});
|
|
1098
1420
|
await emit({
|
|
1099
1421
|
type: "scenario:end",
|
|
1100
1422
|
runId,
|
|
1101
1423
|
scenarioId: scenario.id,
|
|
1102
1424
|
status: finalStatus,
|
|
1103
|
-
at:
|
|
1425
|
+
at: endedAt,
|
|
1104
1426
|
durationMs: Date.now() - startedAtMs,
|
|
1427
|
+
workflowMeta,
|
|
1105
1428
|
});
|
|
1106
1429
|
}
|
|
1107
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
|
+
});
|
|
1108
1439
|
const report = {
|
|
1109
1440
|
runId,
|
|
1110
1441
|
scenarioId: scenario.id,
|
|
@@ -1116,6 +1447,7 @@ export const runScenario = async ({ scenario, environment = {}, globals = {}, ru
|
|
|
1116
1447
|
totalSteps: runtimeStepIds.length,
|
|
1117
1448
|
completedSteps: Array.from(localStatuses.values()).filter(isTerminalStatus).length,
|
|
1118
1449
|
context: createContextSnapshot(runtimeContext),
|
|
1450
|
+
workflowMeta: cloneWorkflowExecutionMeta(finalWorkflowMeta),
|
|
1119
1451
|
nodeExecutions: runtimeStepIds.map((stepId) => {
|
|
1120
1452
|
const descriptor = runtimeStepDescriptors.find((entry) => entry.stepId === stepId);
|
|
1121
1453
|
return (records.get(stepId) ??
|