@voyantjs/workflows 0.6.7 → 0.6.9
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/auth/index.d.ts +26 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +137 -0
- package/dist/conditions.d.ts +29 -0
- package/dist/conditions.d.ts.map +1 -0
- package/dist/conditions.js +5 -0
- package/dist/handler/index.d.ts +104 -0
- package/dist/handler/index.d.ts.map +1 -0
- package/dist/handler/index.js +238 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/protocol/index.d.ts +187 -0
- package/dist/protocol/index.d.ts.map +1 -0
- package/dist/protocol/index.js +7 -0
- package/dist/rate-limit/index.d.ts +40 -0
- package/dist/rate-limit/index.d.ts.map +1 -0
- package/dist/rate-limit/index.js +139 -0
- package/dist/runtime/ctx.d.ts +102 -0
- package/dist/runtime/ctx.d.ts.map +1 -0
- package/dist/runtime/ctx.js +607 -0
- package/dist/runtime/determinism.d.ts +19 -0
- package/dist/runtime/determinism.d.ts.map +1 -0
- package/dist/runtime/determinism.js +61 -0
- package/dist/runtime/errors.d.ts +21 -0
- package/dist/runtime/errors.d.ts.map +1 -0
- package/dist/runtime/errors.js +45 -0
- package/dist/runtime/executor.d.ts +159 -0
- package/dist/runtime/executor.d.ts.map +1 -0
- package/dist/runtime/executor.js +225 -0
- package/dist/runtime/journal.d.ts +55 -0
- package/dist/runtime/journal.d.ts.map +1 -0
- package/dist/runtime/journal.js +28 -0
- package/dist/testing/index.d.ts +117 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +595 -0
- package/dist/trigger.d.ts +122 -0
- package/dist/trigger.d.ts.map +1 -0
- package/dist/trigger.js +23 -0
- package/dist/types.d.ts +63 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/workflow.d.ts +212 -0
- package/dist/workflow.d.ts.map +1 -0
- package/dist/workflow.js +46 -0
- package/package.json +30 -30
- package/src/auth/index.ts +46 -52
- package/src/conditions.ts +13 -13
- package/src/handler/index.ts +110 -106
- package/src/index.ts +7 -7
- package/src/protocol/index.ts +137 -71
- package/src/rate-limit/index.ts +77 -78
- package/src/runtime/ctx.ts +354 -342
- package/src/runtime/determinism.ts +27 -27
- package/src/runtime/errors.ts +17 -17
- package/src/runtime/executor.ts +179 -172
- package/src/runtime/journal.ts +25 -25
- package/src/testing/index.ts +268 -202
- package/src/trigger.ts +64 -71
- package/src/types.ts +16 -18
- package/src/workflow.ts +154 -152
package/src/testing/index.ts
CHANGED
|
@@ -6,37 +6,53 @@
|
|
|
6
6
|
// Drives `executeWorkflowStep` across resumptions. Steps resolve
|
|
7
7
|
// from a user-supplied stub map; waitpoints resolve from fixtures.
|
|
8
8
|
|
|
9
|
-
import type {
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
import type { SerializedError } from "../protocol/index.js"
|
|
10
|
+
import {
|
|
11
|
+
type CompensationReport,
|
|
12
|
+
type ExecuteWorkflowStepResponse,
|
|
13
|
+
executeWorkflowStep,
|
|
14
|
+
type MetadataMutation,
|
|
15
|
+
type StreamChunk,
|
|
16
|
+
type WaitpointRegistration,
|
|
17
|
+
} from "../runtime/executor.js"
|
|
18
|
+
import type {
|
|
19
|
+
JournalSlice,
|
|
20
|
+
StepJournalEntry,
|
|
21
|
+
WaitpointResolutionEntry,
|
|
22
|
+
} from "../runtime/journal.js"
|
|
23
|
+
import { emptyJournal } from "../runtime/journal.js"
|
|
24
|
+
import type { RunStatus } from "../types.js"
|
|
25
|
+
import type {
|
|
26
|
+
EnvironmentContext,
|
|
27
|
+
MetadataValue,
|
|
28
|
+
StepContext,
|
|
29
|
+
WorkflowDefinition,
|
|
30
|
+
WorkflowHandle,
|
|
31
|
+
} from "../workflow.js"
|
|
32
|
+
import { getWorkflow } from "../workflow.js"
|
|
17
33
|
|
|
18
34
|
export interface TestOptions<_TIn> {
|
|
19
35
|
/** Map of stepId → function run in place of the real step body. */
|
|
20
|
-
steps?: Record<string, (stepCtx: StepContext) => unknown | Promise<unknown
|
|
36
|
+
steps?: Record<string, (stepCtx: StepContext) => unknown | Promise<unknown>>
|
|
21
37
|
/**
|
|
22
38
|
* Map of eventType → payload (or payload array). First
|
|
23
39
|
* match-per-eventType wins.
|
|
24
40
|
*/
|
|
25
|
-
waitForEvent?: Record<string, unknown | unknown[]
|
|
41
|
+
waitForEvent?: Record<string, unknown | unknown[]>
|
|
26
42
|
/** Map of signalName → payload. */
|
|
27
|
-
waitForSignal?: Record<string, unknown
|
|
43
|
+
waitForSignal?: Record<string, unknown>
|
|
28
44
|
/** Map of tokenId → payload. */
|
|
29
|
-
waitForToken?: Record<string, unknown
|
|
45
|
+
waitForToken?: Record<string, unknown>
|
|
30
46
|
/** Workflow invoke stubs keyed by child workflow id. */
|
|
31
|
-
invoke?: Record<string, unknown
|
|
47
|
+
invoke?: Record<string, unknown>
|
|
32
48
|
/** Fake env bindings, passed through to `ctx.environment` for tests. */
|
|
33
|
-
env?: Record<string, unknown
|
|
34
|
-
environment?: Partial<EnvironmentContext
|
|
49
|
+
env?: Record<string, unknown>
|
|
50
|
+
environment?: Partial<EnvironmentContext>
|
|
35
51
|
/** Fixed wall-clock basis for the run. Defaults to Date.now(). */
|
|
36
|
-
now?: () => number
|
|
37
|
-
random?: () => number
|
|
52
|
+
now?: () => number
|
|
53
|
+
random?: () => number
|
|
38
54
|
/** Max resumption cycles. Defaults to 16 — guards runaway loops. */
|
|
39
|
-
maxInvocations?: number
|
|
55
|
+
maxInvocations?: number
|
|
40
56
|
/**
|
|
41
57
|
* When true, the harness stops and returns a "waiting" TestResult as
|
|
42
58
|
* soon as any registered waitpoint has no fixture resolution (instead
|
|
@@ -48,35 +64,35 @@ export interface TestOptions<_TIn> {
|
|
|
48
64
|
* this flag, since a local dev loop is not the right place to
|
|
49
65
|
* synthesize wall-clock delays.
|
|
50
66
|
*/
|
|
51
|
-
pauseOnWait?: boolean
|
|
67
|
+
pauseOnWait?: boolean
|
|
52
68
|
}
|
|
53
69
|
|
|
54
70
|
export interface TestResult<TOut> {
|
|
55
71
|
status: Extract<
|
|
56
72
|
RunStatus,
|
|
57
73
|
"completed" | "failed" | "cancelled" | "compensated" | "compensation_failed" | "waiting"
|
|
58
|
-
|
|
59
|
-
output?: TOut
|
|
60
|
-
error?: { category: SerializedError["category"]; code: string; message: string }
|
|
61
|
-
steps: { id: string; status: "ok" | "err" | "skipped"; duration: number; output?: unknown }[]
|
|
62
|
-
events: { type: string; at: number; data: unknown }[]
|
|
63
|
-
metadata: Record<string, MetadataValue
|
|
64
|
-
compensations: CompensationReport[]
|
|
74
|
+
>
|
|
75
|
+
output?: TOut
|
|
76
|
+
error?: { category: SerializedError["category"]; code: string; message: string }
|
|
77
|
+
steps: { id: string; status: "ok" | "err" | "skipped"; duration: number; output?: unknown }[]
|
|
78
|
+
events: { type: string; at: number; data: unknown }[]
|
|
79
|
+
metadata: Record<string, MetadataValue>
|
|
80
|
+
compensations: CompensationReport[]
|
|
65
81
|
/** Chunks emitted via `ctx.stream()` / `ctx.stream.{text,json,bytes}`, grouped by streamId in emission order. */
|
|
66
|
-
streams: Record<string, StreamChunk[]
|
|
67
|
-
invocations: number
|
|
82
|
+
streams: Record<string, StreamChunk[]>
|
|
83
|
+
invocations: number
|
|
68
84
|
/**
|
|
69
85
|
* Populated when `status === "waiting"`. Holds the persisted executor
|
|
70
86
|
* state needed to resume the run via `resumeWorkflowForTest`.
|
|
71
87
|
*/
|
|
72
88
|
pause?: {
|
|
73
|
-
journal: JournalSlice
|
|
74
|
-
pendingWaitpoints: WaitpointRegistration[]
|
|
75
|
-
startedAt: number
|
|
76
|
-
invocationCount: number
|
|
77
|
-
metadataAppliedCount: number
|
|
78
|
-
fixtureCursors: { event: Record<string, number>; signal: Record<string, number> }
|
|
79
|
-
}
|
|
89
|
+
journal: JournalSlice
|
|
90
|
+
pendingWaitpoints: WaitpointRegistration[]
|
|
91
|
+
startedAt: number
|
|
92
|
+
invocationCount: number
|
|
93
|
+
metadataAppliedCount: number
|
|
94
|
+
fixtureCursors: { event: Record<string, number>; signal: Record<string, number> }
|
|
95
|
+
}
|
|
80
96
|
}
|
|
81
97
|
|
|
82
98
|
export async function runWorkflowForTest<TIn, TOut>(
|
|
@@ -84,33 +100,33 @@ export async function runWorkflowForTest<TIn, TOut>(
|
|
|
84
100
|
input: TIn,
|
|
85
101
|
opts: TestOptions<TIn> = {},
|
|
86
102
|
): Promise<TestResult<TOut>> {
|
|
87
|
-
const def = workflow as WorkflowDefinition<TIn, TOut
|
|
88
|
-
const now = opts.now ?? (() => Date.now())
|
|
89
|
-
const startedAt = now()
|
|
90
|
-
const journal: JournalSlice = emptyJournal()
|
|
91
|
-
const metadata: Record<string, MetadataValue> = {}
|
|
92
|
-
const events: TestResult<TOut>["events"] = []
|
|
93
|
-
const streams: Record<string, StreamChunk[]> = {}
|
|
94
|
-
const maxInvocations = opts.maxInvocations ?? 16
|
|
103
|
+
const def = workflow as WorkflowDefinition<TIn, TOut>
|
|
104
|
+
const now = opts.now ?? (() => Date.now())
|
|
105
|
+
const startedAt = now()
|
|
106
|
+
const journal: JournalSlice = emptyJournal()
|
|
107
|
+
const metadata: Record<string, MetadataValue> = {}
|
|
108
|
+
const events: TestResult<TOut>["events"] = []
|
|
109
|
+
const streams: Record<string, StreamChunk[]> = {}
|
|
110
|
+
const maxInvocations = opts.maxInvocations ?? 16
|
|
95
111
|
|
|
96
112
|
const environment: EnvironmentContext = {
|
|
97
113
|
name: opts.environment?.name ?? "development",
|
|
98
114
|
git: opts.environment?.git,
|
|
99
|
-
}
|
|
115
|
+
}
|
|
100
116
|
|
|
101
|
-
let invocationCount = 0
|
|
102
|
-
let last: ExecuteWorkflowStepResponse | undefined
|
|
117
|
+
let invocationCount = 0
|
|
118
|
+
let last: ExecuteWorkflowStepResponse | undefined
|
|
103
119
|
// Track how many metadata mutations have already been applied so we
|
|
104
120
|
// only apply the delta on each invocation. Otherwise replays
|
|
105
121
|
// double-count `increment` / duplicate `append` values. (Positional
|
|
106
122
|
// dedup; mirrors what the real orchestrator does with journaled ids.)
|
|
107
|
-
let metadataAppliedCount = 0
|
|
123
|
+
let metadataAppliedCount = 0
|
|
108
124
|
// Per-fixture cursors into iterable event/signal arrays, persisted
|
|
109
125
|
// across invocations so replay doesn't restart consumption.
|
|
110
|
-
const cursors: FixtureCursors = { event: new Map(), signal: new Map() }
|
|
126
|
+
const cursors: FixtureCursors = { event: new Map(), signal: new Map() }
|
|
111
127
|
|
|
112
128
|
while (invocationCount < maxInvocations) {
|
|
113
|
-
invocationCount += 1
|
|
129
|
+
invocationCount += 1
|
|
114
130
|
|
|
115
131
|
const response = await executeWorkflowStep(def as unknown as WorkflowDefinition, {
|
|
116
132
|
runId: `run_test_${def.id}`,
|
|
@@ -137,17 +153,18 @@ export async function runWorkflowForTest<TIn, TOut>(
|
|
|
137
153
|
runStartedAt: startedAt,
|
|
138
154
|
tags: [],
|
|
139
155
|
stepRunner: createStepRunner(opts, events, now),
|
|
140
|
-
})
|
|
156
|
+
})
|
|
141
157
|
|
|
142
|
-
last = response
|
|
158
|
+
last = response
|
|
143
159
|
|
|
144
|
-
const newMutations = response.metadataUpdates.slice(metadataAppliedCount)
|
|
145
|
-
applyMetadata(metadata, newMutations)
|
|
146
|
-
metadataAppliedCount = response.metadataUpdates.length
|
|
160
|
+
const newMutations = response.metadataUpdates.slice(metadataAppliedCount)
|
|
161
|
+
applyMetadata(metadata, newMutations)
|
|
162
|
+
metadataAppliedCount = response.metadataUpdates.length
|
|
147
163
|
|
|
148
164
|
for (const chunk of response.streamChunks) {
|
|
149
|
-
const bucket = streams[chunk.streamId]
|
|
150
|
-
bucket
|
|
165
|
+
const bucket = streams[chunk.streamId] ?? []
|
|
166
|
+
streams[chunk.streamId] = bucket
|
|
167
|
+
bucket.push(chunk)
|
|
151
168
|
}
|
|
152
169
|
|
|
153
170
|
if (response.status === "completed") {
|
|
@@ -160,20 +177,24 @@ export async function runWorkflowForTest<TIn, TOut>(
|
|
|
160
177
|
compensations: [],
|
|
161
178
|
streams,
|
|
162
179
|
invocations: invocationCount,
|
|
163
|
-
}
|
|
180
|
+
}
|
|
164
181
|
}
|
|
165
182
|
|
|
166
183
|
if (response.status === "failed") {
|
|
167
184
|
return {
|
|
168
185
|
status: "failed",
|
|
169
|
-
error: {
|
|
186
|
+
error: {
|
|
187
|
+
category: response.error.category,
|
|
188
|
+
code: response.error.code,
|
|
189
|
+
message: response.error.message,
|
|
190
|
+
},
|
|
170
191
|
steps: stepsFromJournal(journal),
|
|
171
192
|
events,
|
|
172
193
|
metadata,
|
|
173
194
|
compensations: [],
|
|
174
195
|
streams,
|
|
175
196
|
invocations: invocationCount,
|
|
176
|
-
}
|
|
197
|
+
}
|
|
177
198
|
}
|
|
178
199
|
|
|
179
200
|
if (response.status === "cancelled") {
|
|
@@ -185,7 +206,7 @@ export async function runWorkflowForTest<TIn, TOut>(
|
|
|
185
206
|
compensations: response.compensations,
|
|
186
207
|
streams,
|
|
187
208
|
invocations: invocationCount,
|
|
188
|
-
}
|
|
209
|
+
}
|
|
189
210
|
}
|
|
190
211
|
|
|
191
212
|
if (response.status === "compensated" || response.status === "compensation_failed") {
|
|
@@ -204,28 +225,32 @@ export async function runWorkflowForTest<TIn, TOut>(
|
|
|
204
225
|
compensations: response.compensations,
|
|
205
226
|
streams,
|
|
206
227
|
invocations: invocationCount,
|
|
207
|
-
}
|
|
228
|
+
}
|
|
208
229
|
}
|
|
209
230
|
|
|
210
231
|
// Waiting: resolve waitpoints from fixtures and loop. Waitpoints
|
|
211
232
|
// that have no fixture match either throw (default) or, when
|
|
212
233
|
// `pauseOnWait` is set, park the run and return a "waiting" result.
|
|
213
|
-
const stillPending: WaitpointRegistration[] = []
|
|
234
|
+
const stillPending: WaitpointRegistration[] = []
|
|
214
235
|
for (const wp of response.waitpoints) {
|
|
215
|
-
const resolved = await resolveWaitpoint(wp, opts, now, events, cursors)
|
|
236
|
+
const resolved = await resolveWaitpoint(wp, opts, now, events, cursors)
|
|
216
237
|
if (!resolved) {
|
|
217
238
|
if (opts.pauseOnWait) {
|
|
218
|
-
stillPending.push(wp)
|
|
219
|
-
continue
|
|
239
|
+
stillPending.push(wp)
|
|
240
|
+
continue
|
|
220
241
|
}
|
|
221
242
|
throw new Error(
|
|
222
243
|
`test harness: waitpoint ${wp.clientWaitpointId} (${wp.kind}) has no fixture resolution. ` +
|
|
223
244
|
`Provide one via TestOptions.waitForEvent / waitForSignal / (sleeps auto-resolve), ` +
|
|
224
245
|
`or set TestOptions.pauseOnWait to park the run.`,
|
|
225
|
-
)
|
|
246
|
+
)
|
|
226
247
|
}
|
|
227
|
-
journal.waitpointsResolved[wp.clientWaitpointId] = resolved
|
|
228
|
-
events.push({
|
|
248
|
+
journal.waitpointsResolved[wp.clientWaitpointId] = resolved
|
|
249
|
+
events.push({
|
|
250
|
+
type: `waitpoint.resolved:${wp.kind}`,
|
|
251
|
+
at: resolved.resolvedAt,
|
|
252
|
+
data: resolved.payload ?? null,
|
|
253
|
+
})
|
|
229
254
|
}
|
|
230
255
|
if (stillPending.length > 0) {
|
|
231
256
|
return {
|
|
@@ -247,14 +272,14 @@ export async function runWorkflowForTest<TIn, TOut>(
|
|
|
247
272
|
signal: Object.fromEntries(cursors.signal.entries()),
|
|
248
273
|
},
|
|
249
274
|
},
|
|
250
|
-
}
|
|
275
|
+
}
|
|
251
276
|
}
|
|
252
277
|
}
|
|
253
278
|
|
|
254
279
|
throw new Error(
|
|
255
280
|
`test harness exceeded maxInvocations (${maxInvocations}). ` +
|
|
256
281
|
`Last status: ${last?.status ?? "<none>"}. Possible infinite waitpoint loop.`,
|
|
257
|
-
)
|
|
282
|
+
)
|
|
258
283
|
}
|
|
259
284
|
|
|
260
285
|
/**
|
|
@@ -266,13 +291,13 @@ export async function runWorkflowForTest<TIn, TOut>(
|
|
|
266
291
|
export type WaitpointInjection =
|
|
267
292
|
| { kind: "EVENT"; eventType: string; payload?: unknown }
|
|
268
293
|
| { kind: "SIGNAL"; name: string; payload?: unknown }
|
|
269
|
-
| { kind: "MANUAL"; tokenId: string; payload?: unknown }
|
|
294
|
+
| { kind: "MANUAL"; tokenId: string; payload?: unknown }
|
|
270
295
|
|
|
271
296
|
export interface ResumeOptions<TIn> extends TestOptions<TIn> {
|
|
272
297
|
/** Persisted pause state returned from a previous `runWorkflowForTest` or `resumeWorkflowForTest`. */
|
|
273
|
-
pause: NonNullable<TestResult<unknown>["pause"]
|
|
298
|
+
pause: NonNullable<TestResult<unknown>["pause"]>
|
|
274
299
|
/** The single waitpoint resolution to inject on resume. */
|
|
275
|
-
injection: WaitpointInjection
|
|
300
|
+
injection: WaitpointInjection
|
|
276
301
|
}
|
|
277
302
|
|
|
278
303
|
/**
|
|
@@ -285,26 +310,27 @@ export async function resumeWorkflowForTest<TIn, TOut>(
|
|
|
285
310
|
input: TIn,
|
|
286
311
|
opts: ResumeOptions<TIn>,
|
|
287
312
|
): Promise<TestResult<TOut>> {
|
|
288
|
-
const def = workflow as WorkflowDefinition<TIn, TOut
|
|
289
|
-
const now = opts.now ?? (() => Date.now())
|
|
290
|
-
const maxInvocations = opts.maxInvocations ?? 16
|
|
313
|
+
const def = workflow as WorkflowDefinition<TIn, TOut>
|
|
314
|
+
const now = opts.now ?? (() => Date.now())
|
|
315
|
+
const maxInvocations = opts.maxInvocations ?? 16
|
|
291
316
|
|
|
292
|
-
const matched = matchWaitpoint(opts.pause.pendingWaitpoints, opts.injection)
|
|
317
|
+
const matched = matchWaitpoint(opts.pause.pendingWaitpoints, opts.injection)
|
|
293
318
|
if (!matched) {
|
|
294
319
|
throw new Error(
|
|
295
320
|
`resume: no pending waitpoint matches injection kind=${opts.injection.kind}, ` +
|
|
296
321
|
`key=${injectionKey(opts.injection)}`,
|
|
297
|
-
)
|
|
322
|
+
)
|
|
298
323
|
}
|
|
299
324
|
|
|
300
|
-
const journal = cloneJournal(opts.pause.journal)
|
|
325
|
+
const journal = cloneJournal(opts.pause.journal)
|
|
301
326
|
journal.waitpointsResolved[matched.clientWaitpointId] = {
|
|
302
327
|
kind: matched.kind,
|
|
303
328
|
resolvedAt: now(),
|
|
304
329
|
payload: opts.injection.payload,
|
|
305
330
|
source: "live",
|
|
306
|
-
matchedEventId:
|
|
307
|
-
|
|
331
|
+
matchedEventId:
|
|
332
|
+
opts.injection.kind === "EVENT" ? `evt_live_${opts.injection.eventType}` : undefined,
|
|
333
|
+
}
|
|
308
334
|
|
|
309
335
|
const events: TestResult<TOut>["events"] = [
|
|
310
336
|
{
|
|
@@ -312,28 +338,30 @@ export async function resumeWorkflowForTest<TIn, TOut>(
|
|
|
312
338
|
at: now(),
|
|
313
339
|
data: opts.injection.payload ?? null,
|
|
314
340
|
},
|
|
315
|
-
]
|
|
316
|
-
const metadata: Record<string, MetadataValue> = {}
|
|
317
|
-
const streams: Record<string, StreamChunk[]> = {}
|
|
341
|
+
]
|
|
342
|
+
const metadata: Record<string, MetadataValue> = {}
|
|
343
|
+
const streams: Record<string, StreamChunk[]> = {}
|
|
318
344
|
const cursors: FixtureCursors = {
|
|
319
345
|
event: new Map(Object.entries(opts.pause.fixtureCursors.event)),
|
|
320
346
|
signal: new Map(Object.entries(opts.pause.fixtureCursors.signal)),
|
|
321
|
-
}
|
|
347
|
+
}
|
|
322
348
|
|
|
323
349
|
const environment: EnvironmentContext = {
|
|
324
350
|
name: opts.environment?.name ?? "development",
|
|
325
351
|
git: opts.environment?.git,
|
|
326
|
-
}
|
|
352
|
+
}
|
|
327
353
|
|
|
328
|
-
let invocationCount = opts.pause.invocationCount
|
|
329
|
-
let metadataAppliedCount = opts.pause.metadataAppliedCount
|
|
330
|
-
let last: ExecuteWorkflowStepResponse | undefined
|
|
354
|
+
let invocationCount = opts.pause.invocationCount
|
|
355
|
+
let metadataAppliedCount = opts.pause.metadataAppliedCount
|
|
356
|
+
let last: ExecuteWorkflowStepResponse | undefined
|
|
331
357
|
|
|
332
358
|
// Remaining pending waitpoints from the previous pause (still parked).
|
|
333
|
-
let carryover = opts.pause.pendingWaitpoints.filter(
|
|
359
|
+
let carryover = opts.pause.pendingWaitpoints.filter(
|
|
360
|
+
(w) => w.clientWaitpointId !== matched.clientWaitpointId,
|
|
361
|
+
)
|
|
334
362
|
|
|
335
363
|
while (invocationCount < maxInvocations) {
|
|
336
|
-
invocationCount += 1
|
|
364
|
+
invocationCount += 1
|
|
337
365
|
|
|
338
366
|
const response = await executeWorkflowStep(def as unknown as WorkflowDefinition, {
|
|
339
367
|
runId: `run_test_${def.id}`,
|
|
@@ -360,17 +388,18 @@ export async function resumeWorkflowForTest<TIn, TOut>(
|
|
|
360
388
|
runStartedAt: opts.pause.startedAt,
|
|
361
389
|
tags: [],
|
|
362
390
|
stepRunner: createStepRunner(opts, events, now),
|
|
363
|
-
})
|
|
391
|
+
})
|
|
364
392
|
|
|
365
|
-
last = response
|
|
393
|
+
last = response
|
|
366
394
|
|
|
367
|
-
const newMutations = response.metadataUpdates.slice(metadataAppliedCount)
|
|
368
|
-
applyMetadata(metadata, newMutations)
|
|
369
|
-
metadataAppliedCount = response.metadataUpdates.length
|
|
395
|
+
const newMutations = response.metadataUpdates.slice(metadataAppliedCount)
|
|
396
|
+
applyMetadata(metadata, newMutations)
|
|
397
|
+
metadataAppliedCount = response.metadataUpdates.length
|
|
370
398
|
|
|
371
399
|
for (const chunk of response.streamChunks) {
|
|
372
|
-
const bucket = streams[chunk.streamId]
|
|
373
|
-
bucket
|
|
400
|
+
const bucket = streams[chunk.streamId] ?? []
|
|
401
|
+
streams[chunk.streamId] = bucket
|
|
402
|
+
bucket.push(chunk)
|
|
374
403
|
}
|
|
375
404
|
|
|
376
405
|
if (response.status === "completed") {
|
|
@@ -383,19 +412,23 @@ export async function resumeWorkflowForTest<TIn, TOut>(
|
|
|
383
412
|
compensations: [],
|
|
384
413
|
streams,
|
|
385
414
|
invocations: invocationCount,
|
|
386
|
-
}
|
|
415
|
+
}
|
|
387
416
|
}
|
|
388
417
|
if (response.status === "failed") {
|
|
389
418
|
return {
|
|
390
419
|
status: "failed",
|
|
391
|
-
error: {
|
|
420
|
+
error: {
|
|
421
|
+
category: response.error.category,
|
|
422
|
+
code: response.error.code,
|
|
423
|
+
message: response.error.message,
|
|
424
|
+
},
|
|
392
425
|
steps: stepsFromJournal(journal),
|
|
393
426
|
events,
|
|
394
427
|
metadata,
|
|
395
428
|
compensations: [],
|
|
396
429
|
streams,
|
|
397
430
|
invocations: invocationCount,
|
|
398
|
-
}
|
|
431
|
+
}
|
|
399
432
|
}
|
|
400
433
|
if (response.status === "cancelled") {
|
|
401
434
|
return {
|
|
@@ -406,13 +439,17 @@ export async function resumeWorkflowForTest<TIn, TOut>(
|
|
|
406
439
|
compensations: response.compensations,
|
|
407
440
|
streams,
|
|
408
441
|
invocations: invocationCount,
|
|
409
|
-
}
|
|
442
|
+
}
|
|
410
443
|
}
|
|
411
444
|
if (response.status === "compensated" || response.status === "compensation_failed") {
|
|
412
445
|
return {
|
|
413
446
|
status: response.status,
|
|
414
447
|
error: response.error
|
|
415
|
-
? {
|
|
448
|
+
? {
|
|
449
|
+
category: response.error.category,
|
|
450
|
+
code: response.error.code,
|
|
451
|
+
message: response.error.message,
|
|
452
|
+
}
|
|
416
453
|
: undefined,
|
|
417
454
|
steps: stepsFromJournal(journal),
|
|
418
455
|
events,
|
|
@@ -420,26 +457,30 @@ export async function resumeWorkflowForTest<TIn, TOut>(
|
|
|
420
457
|
compensations: response.compensations,
|
|
421
458
|
streams,
|
|
422
459
|
invocations: invocationCount,
|
|
423
|
-
}
|
|
460
|
+
}
|
|
424
461
|
}
|
|
425
462
|
|
|
426
|
-
const stillPending: WaitpointRegistration[] = [...carryover]
|
|
463
|
+
const stillPending: WaitpointRegistration[] = [...carryover]
|
|
427
464
|
for (const wp of response.waitpoints) {
|
|
428
|
-
const resolved = await resolveWaitpoint(wp, opts, now, events, cursors)
|
|
465
|
+
const resolved = await resolveWaitpoint(wp, opts, now, events, cursors)
|
|
429
466
|
if (!resolved) {
|
|
430
467
|
if (opts.pauseOnWait) {
|
|
431
|
-
stillPending.push(wp)
|
|
432
|
-
continue
|
|
468
|
+
stillPending.push(wp)
|
|
469
|
+
continue
|
|
433
470
|
}
|
|
434
471
|
throw new Error(
|
|
435
472
|
`resume: waitpoint ${wp.clientWaitpointId} (${wp.kind}) has no fixture resolution. ` +
|
|
436
473
|
`Provide one via TestOptions.waitForEvent / waitForSignal, or set pauseOnWait.`,
|
|
437
|
-
)
|
|
474
|
+
)
|
|
438
475
|
}
|
|
439
|
-
journal.waitpointsResolved[wp.clientWaitpointId] = resolved
|
|
440
|
-
events.push({
|
|
476
|
+
journal.waitpointsResolved[wp.clientWaitpointId] = resolved
|
|
477
|
+
events.push({
|
|
478
|
+
type: `waitpoint.resolved:${wp.kind}`,
|
|
479
|
+
at: resolved.resolvedAt,
|
|
480
|
+
data: resolved.payload ?? null,
|
|
481
|
+
})
|
|
441
482
|
}
|
|
442
|
-
carryover = []
|
|
483
|
+
carryover = [] // consumed into stillPending
|
|
443
484
|
|
|
444
485
|
if (stillPending.length > 0) {
|
|
445
486
|
return {
|
|
@@ -461,13 +502,13 @@ export async function resumeWorkflowForTest<TIn, TOut>(
|
|
|
461
502
|
signal: Object.fromEntries(cursors.signal.entries()),
|
|
462
503
|
},
|
|
463
504
|
},
|
|
464
|
-
}
|
|
505
|
+
}
|
|
465
506
|
}
|
|
466
507
|
}
|
|
467
508
|
|
|
468
509
|
throw new Error(
|
|
469
510
|
`resume: exceeded maxInvocations (${maxInvocations}). Last status: ${last?.status ?? "<none>"}.`,
|
|
470
|
-
)
|
|
511
|
+
)
|
|
471
512
|
}
|
|
472
513
|
|
|
473
514
|
function matchWaitpoint(
|
|
@@ -475,18 +516,18 @@ function matchWaitpoint(
|
|
|
475
516
|
inj: WaitpointInjection,
|
|
476
517
|
): WaitpointRegistration | undefined {
|
|
477
518
|
for (const wp of pending) {
|
|
478
|
-
if (wp.kind !== inj.kind) continue
|
|
479
|
-
if (inj.kind === "EVENT" && wp.meta.eventType === inj.eventType) return wp
|
|
480
|
-
if (inj.kind === "SIGNAL" && wp.meta.signalName === inj.name) return wp
|
|
481
|
-
if (inj.kind === "MANUAL" && wp.meta.tokenId === inj.tokenId) return wp
|
|
519
|
+
if (wp.kind !== inj.kind) continue
|
|
520
|
+
if (inj.kind === "EVENT" && wp.meta.eventType === inj.eventType) return wp
|
|
521
|
+
if (inj.kind === "SIGNAL" && wp.meta.signalName === inj.name) return wp
|
|
522
|
+
if (inj.kind === "MANUAL" && wp.meta.tokenId === inj.tokenId) return wp
|
|
482
523
|
}
|
|
483
|
-
return undefined
|
|
524
|
+
return undefined
|
|
484
525
|
}
|
|
485
526
|
|
|
486
527
|
function injectionKey(inj: WaitpointInjection): string {
|
|
487
|
-
if (inj.kind === "EVENT") return inj.eventType
|
|
488
|
-
if (inj.kind === "SIGNAL") return inj.name
|
|
489
|
-
return inj.tokenId
|
|
528
|
+
if (inj.kind === "EVENT") return inj.eventType
|
|
529
|
+
if (inj.kind === "SIGNAL") return inj.name
|
|
530
|
+
return inj.tokenId
|
|
490
531
|
}
|
|
491
532
|
|
|
492
533
|
function cloneJournal(j: JournalSlice): JournalSlice {
|
|
@@ -496,7 +537,7 @@ function cloneJournal(j: JournalSlice): JournalSlice {
|
|
|
496
537
|
compensationsRun: { ...j.compensationsRun },
|
|
497
538
|
metadataState: { ...j.metadataState },
|
|
498
539
|
streamsCompleted: { ...j.streamsCompleted },
|
|
499
|
-
}
|
|
540
|
+
}
|
|
500
541
|
}
|
|
501
542
|
|
|
502
543
|
function createStepRunner(
|
|
@@ -505,31 +546,35 @@ function createStepRunner(
|
|
|
505
546
|
now: () => number,
|
|
506
547
|
) {
|
|
507
548
|
return async (args: {
|
|
508
|
-
stepId: string
|
|
509
|
-
attempt: number
|
|
510
|
-
input: unknown
|
|
511
|
-
fn: (stepCtx: StepContext) => Promise<unknown
|
|
512
|
-
stepCtx: StepContext
|
|
549
|
+
stepId: string
|
|
550
|
+
attempt: number
|
|
551
|
+
input: unknown
|
|
552
|
+
fn: (stepCtx: StepContext) => Promise<unknown>
|
|
553
|
+
stepCtx: StepContext
|
|
513
554
|
}): Promise<StepJournalEntry> => {
|
|
514
|
-
const startedAt = now()
|
|
515
|
-
const mock = opts.steps?.[args.stepId]
|
|
555
|
+
const startedAt = now()
|
|
556
|
+
const mock = opts.steps?.[args.stepId]
|
|
516
557
|
try {
|
|
517
|
-
const output = mock ? await mock(args.stepCtx) : await args.fn(args.stepCtx)
|
|
518
|
-
const finishedAt = now()
|
|
519
|
-
events.push({ type: "step.ok", at: finishedAt, data: { stepId: args.stepId, output } })
|
|
558
|
+
const output = mock ? await mock(args.stepCtx) : await args.fn(args.stepCtx)
|
|
559
|
+
const finishedAt = now()
|
|
560
|
+
events.push({ type: "step.ok", at: finishedAt, data: { stepId: args.stepId, output } })
|
|
520
561
|
return {
|
|
521
562
|
attempt: args.attempt,
|
|
522
563
|
status: "ok",
|
|
523
564
|
output,
|
|
524
565
|
startedAt,
|
|
525
566
|
finishedAt,
|
|
526
|
-
}
|
|
567
|
+
}
|
|
527
568
|
} catch (err) {
|
|
528
|
-
const finishedAt = now()
|
|
529
|
-
const e = err as Error
|
|
530
|
-
const code = (err as { code?: string }).code ?? "UNKNOWN"
|
|
531
|
-
events.push({
|
|
532
|
-
|
|
569
|
+
const finishedAt = now()
|
|
570
|
+
const e = err as Error
|
|
571
|
+
const code = (err as { code?: string }).code ?? "UNKNOWN"
|
|
572
|
+
events.push({
|
|
573
|
+
type: "step.err",
|
|
574
|
+
at: finishedAt,
|
|
575
|
+
data: { stepId: args.stepId, message: e.message, code },
|
|
576
|
+
})
|
|
577
|
+
const retryAfter = (err as { retryAfter?: unknown }).retryAfter
|
|
533
578
|
return {
|
|
534
579
|
attempt: args.attempt,
|
|
535
580
|
status: "err",
|
|
@@ -543,9 +588,9 @@ function createStepRunner(
|
|
|
543
588
|
},
|
|
544
589
|
startedAt,
|
|
545
590
|
finishedAt,
|
|
546
|
-
}
|
|
591
|
+
}
|
|
547
592
|
}
|
|
548
|
-
}
|
|
593
|
+
}
|
|
549
594
|
}
|
|
550
595
|
|
|
551
596
|
async function resolveWaitpoint(
|
|
@@ -555,67 +600,87 @@ async function resolveWaitpoint(
|
|
|
555
600
|
parentEvents: TestResult<unknown>["events"],
|
|
556
601
|
cursors: FixtureCursors,
|
|
557
602
|
): Promise<WaitpointResolutionEntry | null> {
|
|
558
|
-
const at = now()
|
|
603
|
+
const at = now()
|
|
559
604
|
if (wp.kind === "DATETIME") {
|
|
560
|
-
return { kind: "DATETIME", resolvedAt: at, source: "replay" }
|
|
605
|
+
return { kind: "DATETIME", resolvedAt: at, source: "replay" }
|
|
561
606
|
}
|
|
562
607
|
if (wp.kind === "EVENT") {
|
|
563
|
-
const eventType = wp.meta.eventType as string
|
|
564
|
-
const isIter = wp.meta.iter === true
|
|
565
|
-
const fixture = opts.waitForEvent?.[eventType]
|
|
566
|
-
if (fixture === undefined) return null
|
|
608
|
+
const eventType = wp.meta.eventType as string
|
|
609
|
+
const isIter = wp.meta.iter === true
|
|
610
|
+
const fixture = opts.waitForEvent?.[eventType]
|
|
611
|
+
if (fixture === undefined) return null
|
|
567
612
|
if (isIter) {
|
|
568
|
-
return resolveIterableFixture(
|
|
613
|
+
return resolveIterableFixture(
|
|
614
|
+
fixture,
|
|
615
|
+
cursors.event,
|
|
616
|
+
eventType,
|
|
617
|
+
at,
|
|
618
|
+
"EVENT",
|
|
619
|
+
`evt_test_${eventType}`,
|
|
620
|
+
)
|
|
621
|
+
}
|
|
622
|
+
const payload = Array.isArray(fixture) ? fixture[0] : fixture
|
|
623
|
+
return {
|
|
624
|
+
kind: "EVENT",
|
|
625
|
+
resolvedAt: at,
|
|
626
|
+
matchedEventId: `evt_test_${eventType}`,
|
|
627
|
+
payload,
|
|
628
|
+
source: "live",
|
|
569
629
|
}
|
|
570
|
-
const payload = Array.isArray(fixture) ? fixture[0] : fixture;
|
|
571
|
-
return { kind: "EVENT", resolvedAt: at, matchedEventId: `evt_test_${eventType}`, payload, source: "live" };
|
|
572
630
|
}
|
|
573
631
|
if (wp.kind === "SIGNAL") {
|
|
574
|
-
const name = wp.meta.signalName as string
|
|
575
|
-
const isIter = wp.meta.iter === true
|
|
576
|
-
const fixture = opts.waitForSignal?.[name]
|
|
577
|
-
if (fixture === undefined) return null
|
|
632
|
+
const name = wp.meta.signalName as string
|
|
633
|
+
const isIter = wp.meta.iter === true
|
|
634
|
+
const fixture = opts.waitForSignal?.[name]
|
|
635
|
+
if (fixture === undefined) return null
|
|
578
636
|
if (isIter) {
|
|
579
|
-
return resolveIterableFixture(fixture, cursors.signal, name, at, "SIGNAL")
|
|
637
|
+
return resolveIterableFixture(fixture, cursors.signal, name, at, "SIGNAL")
|
|
580
638
|
}
|
|
581
|
-
return { kind: "SIGNAL", resolvedAt: at, payload: fixture, source: "live" }
|
|
639
|
+
return { kind: "SIGNAL", resolvedAt: at, payload: fixture, source: "live" }
|
|
582
640
|
}
|
|
583
641
|
if (wp.kind === "MANUAL") {
|
|
584
|
-
const tokenId = wp.meta.tokenId as string
|
|
585
|
-
const fixture = opts.waitForToken?.[tokenId]
|
|
586
|
-
if (fixture === undefined) return null
|
|
587
|
-
return { kind: "MANUAL", resolvedAt: at, payload: fixture, source: "live" }
|
|
642
|
+
const tokenId = wp.meta.tokenId as string
|
|
643
|
+
const fixture = opts.waitForToken?.[tokenId]
|
|
644
|
+
if (fixture === undefined) return null
|
|
645
|
+
return { kind: "MANUAL", resolvedAt: at, payload: fixture, source: "live" }
|
|
588
646
|
}
|
|
589
647
|
if (wp.kind === "RUN") {
|
|
590
|
-
const childWorkflowId = wp.meta.childWorkflowId as string
|
|
591
|
-
const childInput = wp.meta.childInput
|
|
648
|
+
const childWorkflowId = wp.meta.childWorkflowId as string
|
|
649
|
+
const childInput = wp.meta.childInput
|
|
650
|
+
const detach = wp.meta.detach === true
|
|
592
651
|
// Allow test-level override: `invoke: { [childId]: value | (input) => value }`.
|
|
593
|
-
const override = opts.invoke?.[childWorkflowId]
|
|
652
|
+
const override = opts.invoke?.[childWorkflowId]
|
|
594
653
|
if (override !== undefined) {
|
|
595
|
-
const payload =
|
|
596
|
-
|
|
597
|
-
|
|
654
|
+
const payload =
|
|
655
|
+
typeof override === "function"
|
|
656
|
+
? await (override as (input: unknown) => unknown | Promise<unknown>)(childInput)
|
|
657
|
+
: override
|
|
598
658
|
parentEvents.push({
|
|
599
659
|
type: "child.resolved-from-fixture",
|
|
600
660
|
at: now(),
|
|
601
|
-
data: { childWorkflowId, payload },
|
|
602
|
-
})
|
|
603
|
-
return {
|
|
661
|
+
data: { childWorkflowId, payload, detach },
|
|
662
|
+
})
|
|
663
|
+
return {
|
|
664
|
+
kind: "RUN",
|
|
665
|
+
resolvedAt: now(),
|
|
666
|
+
payload: detach ? undefined : payload,
|
|
667
|
+
source: "replay",
|
|
668
|
+
}
|
|
604
669
|
}
|
|
605
670
|
|
|
606
671
|
// Otherwise run the child workflow in-process.
|
|
607
|
-
const child = getWorkflow(childWorkflowId)
|
|
672
|
+
const child = getWorkflow(childWorkflowId)
|
|
608
673
|
if (!child) {
|
|
609
674
|
throw new Error(
|
|
610
675
|
`test harness: ctx.invoke target "${childWorkflowId}" is not registered. ` +
|
|
611
676
|
`Import the child workflow module, or provide a stub via TestOptions.invoke.`,
|
|
612
|
-
)
|
|
677
|
+
)
|
|
613
678
|
}
|
|
614
679
|
parentEvents.push({
|
|
615
680
|
type: "child.started",
|
|
616
681
|
at: now(),
|
|
617
682
|
data: { childWorkflowId, input: childInput },
|
|
618
|
-
})
|
|
683
|
+
})
|
|
619
684
|
const childResult = await runWorkflowForTest(
|
|
620
685
|
child as unknown as WorkflowHandle<unknown, unknown>,
|
|
621
686
|
childInput,
|
|
@@ -630,38 +695,42 @@ async function resolveWaitpoint(
|
|
|
630
695
|
now: opts.now,
|
|
631
696
|
random: opts.random,
|
|
632
697
|
maxInvocations: opts.maxInvocations,
|
|
698
|
+
pauseOnWait: detach || opts.pauseOnWait,
|
|
633
699
|
},
|
|
634
|
-
)
|
|
700
|
+
)
|
|
635
701
|
parentEvents.push({
|
|
636
702
|
type: "child.finished",
|
|
637
703
|
at: now(),
|
|
638
|
-
data: { childWorkflowId, status: childResult.status, output: childResult.output },
|
|
639
|
-
})
|
|
704
|
+
data: { childWorkflowId, status: childResult.status, output: childResult.output, detach },
|
|
705
|
+
})
|
|
706
|
+
if (detach) {
|
|
707
|
+
return { kind: "RUN", resolvedAt: now(), payload: undefined, source: "replay" }
|
|
708
|
+
}
|
|
640
709
|
if (childResult.status === "completed") {
|
|
641
|
-
return { kind: "RUN", resolvedAt: now(), payload: childResult.output, source: "replay" }
|
|
710
|
+
return { kind: "RUN", resolvedAt: now(), payload: childResult.output, source: "replay" }
|
|
642
711
|
}
|
|
643
712
|
// Child failed / cancelled / compensated with error / compensation_failed.
|
|
644
713
|
const err = childResult.error ?? {
|
|
645
714
|
category: "USER_ERROR" as const,
|
|
646
715
|
code: "CHILD_RUN_ENDED",
|
|
647
716
|
message: `child run ended with status ${childResult.status}`,
|
|
648
|
-
}
|
|
717
|
+
}
|
|
649
718
|
return {
|
|
650
719
|
kind: "RUN",
|
|
651
720
|
resolvedAt: now(),
|
|
652
721
|
source: "replay",
|
|
653
722
|
error: err,
|
|
654
|
-
}
|
|
723
|
+
}
|
|
655
724
|
}
|
|
656
|
-
return null
|
|
725
|
+
return null
|
|
657
726
|
}
|
|
658
727
|
|
|
659
728
|
export interface FixtureCursors {
|
|
660
|
-
event: Map<string, number
|
|
661
|
-
signal: Map<string, number
|
|
729
|
+
event: Map<string, number>
|
|
730
|
+
signal: Map<string, number>
|
|
662
731
|
}
|
|
663
732
|
|
|
664
|
-
const STREAM_END_MARKER = { __voyantStreamEnd: true } as const
|
|
733
|
+
const STREAM_END_MARKER = { __voyantStreamEnd: true } as const
|
|
665
734
|
|
|
666
735
|
function resolveIterableFixture(
|
|
667
736
|
fixture: unknown,
|
|
@@ -671,9 +740,9 @@ function resolveIterableFixture(
|
|
|
671
740
|
kind: "EVENT" | "SIGNAL",
|
|
672
741
|
matchedEventId?: string,
|
|
673
742
|
): WaitpointResolutionEntry {
|
|
674
|
-
const array = Array.isArray(fixture) ? fixture : [fixture]
|
|
675
|
-
const idx = cursors.get(key) ?? 0
|
|
676
|
-
cursors.set(key, idx + 1)
|
|
743
|
+
const array = Array.isArray(fixture) ? fixture : [fixture]
|
|
744
|
+
const idx = cursors.get(key) ?? 0
|
|
745
|
+
cursors.set(key, idx + 1)
|
|
677
746
|
if (idx >= array.length) {
|
|
678
747
|
// Stream is exhausted — signal the tenant-side iterator to terminate.
|
|
679
748
|
return {
|
|
@@ -682,7 +751,7 @@ function resolveIterableFixture(
|
|
|
682
751
|
payload: STREAM_END_MARKER,
|
|
683
752
|
source: "replay",
|
|
684
753
|
matchedEventId,
|
|
685
|
-
}
|
|
754
|
+
}
|
|
686
755
|
}
|
|
687
756
|
return {
|
|
688
757
|
kind,
|
|
@@ -690,31 +759,28 @@ function resolveIterableFixture(
|
|
|
690
759
|
payload: array[idx],
|
|
691
760
|
source: "live",
|
|
692
761
|
matchedEventId,
|
|
693
|
-
}
|
|
762
|
+
}
|
|
694
763
|
}
|
|
695
764
|
|
|
696
|
-
function applyMetadata(
|
|
697
|
-
state: Record<string, MetadataValue>,
|
|
698
|
-
updates: MetadataMutation[],
|
|
699
|
-
): void {
|
|
765
|
+
function applyMetadata(state: Record<string, MetadataValue>, updates: MetadataMutation[]): void {
|
|
700
766
|
for (const u of updates) {
|
|
701
767
|
switch (u.op) {
|
|
702
768
|
case "set":
|
|
703
|
-
state[u.key] = u.value as MetadataValue
|
|
704
|
-
break
|
|
769
|
+
state[u.key] = u.value as MetadataValue
|
|
770
|
+
break
|
|
705
771
|
case "increment": {
|
|
706
|
-
const cur = typeof state[u.key] === "number" ? (state[u.key] as number) : 0
|
|
707
|
-
state[u.key] = cur + ((u.value as number) ?? 1)
|
|
708
|
-
break
|
|
772
|
+
const cur = typeof state[u.key] === "number" ? (state[u.key] as number) : 0
|
|
773
|
+
state[u.key] = cur + ((u.value as number) ?? 1)
|
|
774
|
+
break
|
|
709
775
|
}
|
|
710
776
|
case "append": {
|
|
711
|
-
const cur = Array.isArray(state[u.key]) ? (state[u.key] as MetadataValue[]) : []
|
|
712
|
-
state[u.key] = [...cur, u.value as MetadataValue]
|
|
713
|
-
break
|
|
777
|
+
const cur = Array.isArray(state[u.key]) ? (state[u.key] as MetadataValue[]) : []
|
|
778
|
+
state[u.key] = [...cur, u.value as MetadataValue]
|
|
779
|
+
break
|
|
714
780
|
}
|
|
715
781
|
case "remove":
|
|
716
|
-
delete state[u.key]
|
|
717
|
-
break
|
|
782
|
+
delete state[u.key]
|
|
783
|
+
break
|
|
718
784
|
}
|
|
719
785
|
}
|
|
720
786
|
}
|
|
@@ -725,5 +791,5 @@ function stepsFromJournal(j: JournalSlice): TestResult<unknown>["steps"] {
|
|
|
725
791
|
status: entry.status,
|
|
726
792
|
duration: entry.finishedAt - entry.startedAt,
|
|
727
793
|
output: entry.output,
|
|
728
|
-
}))
|
|
794
|
+
}))
|
|
729
795
|
}
|