workerflow 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -29
- package/package.json +1 -1
- package/src/definition.ts +126 -174
- package/src/json.ts +5 -7
- package/src/migrations/0000_initial.ts +98 -294
- package/src/runtime.ts +634 -998
- package/test/runtime.spec.ts +709 -1113
- package/test/tsconfig.json +1 -4
- package/test/worker.ts +1 -3
- package/demo/README.md +0 -73
- package/demo/index.html +0 -13
- package/demo/package.json +0 -33
- package/demo/public/vite.svg +0 -1
- package/demo/src/App.css +0 -0
- package/demo/src/App.tsx +0 -9
- package/demo/src/assets/Cloudflare_Logo.svg +0 -51
- package/demo/src/assets/react.svg +0 -1
- package/demo/src/index.css +0 -1
- package/demo/src/main.tsx +0 -10
- package/demo/tsconfig.app.json +0 -28
- package/demo/tsconfig.json +0 -14
- package/demo/tsconfig.node.json +0 -25
- package/demo/tsconfig.worker.json +0 -13
- package/demo/vite.config.ts +0 -9
- package/demo/worker/index.ts +0 -16
- package/demo/worker-configuration.d.ts +0 -12851
- package/demo/wrangler.jsonc +0 -32
package/test/runtime.spec.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { env } from "cloudflare:workers";
|
|
|
3
3
|
import { describe, expect, it, vi } from "vitest";
|
|
4
4
|
import {
|
|
5
5
|
WorkflowRuntimeContext,
|
|
6
|
+
type RunStep,
|
|
7
|
+
type RunStepAttempt,
|
|
6
8
|
type RunStepId,
|
|
7
9
|
type SleepStepId,
|
|
8
10
|
type WaitStepId,
|
|
@@ -46,7 +48,7 @@ describe("WorkflowRuntime", () => {
|
|
|
46
48
|
resolve(status);
|
|
47
49
|
};
|
|
48
50
|
|
|
49
|
-
await instance.create(
|
|
51
|
+
await instance.create();
|
|
50
52
|
await expect(promise).resolves.toBe("failed");
|
|
51
53
|
});
|
|
52
54
|
} finally {
|
|
@@ -69,7 +71,7 @@ describe("WorkflowRuntime", () => {
|
|
|
69
71
|
if (status === "running") return;
|
|
70
72
|
resolve(status);
|
|
71
73
|
};
|
|
72
|
-
await instance.create(
|
|
74
|
+
await instance.create();
|
|
73
75
|
await expect(promise).resolves.toBe("failed");
|
|
74
76
|
});
|
|
75
77
|
} finally {
|
|
@@ -94,16 +96,18 @@ describe("WorkflowRuntime", () => {
|
|
|
94
96
|
resolve(status);
|
|
95
97
|
};
|
|
96
98
|
|
|
97
|
-
await instance.create(
|
|
99
|
+
await instance.create();
|
|
98
100
|
await expect(promise).resolves.toBe("failed");
|
|
99
101
|
const steps = instance.getSteps_experimental();
|
|
100
102
|
expect(steps).toHaveLength(1);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
+
const step = steps[0]!;
|
|
104
|
+
expect(step.type).toBe("run");
|
|
105
|
+
const attempts = (step as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
106
|
+
expect(attempts).toHaveLength(1);
|
|
107
|
+
expect(attempts[0]).toMatchObject({
|
|
103
108
|
state: "failed",
|
|
104
109
|
errorMessage: "NonRetryableStepError: This is a non-retryable step error",
|
|
105
|
-
errorName: "NonRetryableStepError"
|
|
106
|
-
attemptCount: 1
|
|
110
|
+
errorName: "NonRetryableStepError"
|
|
107
111
|
});
|
|
108
112
|
});
|
|
109
113
|
} finally {
|
|
@@ -132,16 +136,18 @@ describe("WorkflowRuntime", () => {
|
|
|
132
136
|
resolve(status);
|
|
133
137
|
};
|
|
134
138
|
|
|
135
|
-
await instance.create(
|
|
139
|
+
await instance.create();
|
|
136
140
|
await expect(promise).resolves.toBe("failed");
|
|
137
141
|
const steps = instance.getSteps_experimental();
|
|
138
142
|
expect(steps).toHaveLength(1);
|
|
139
|
-
|
|
140
|
-
|
|
143
|
+
const step = steps[0]!;
|
|
144
|
+
expect(step.type).toBe("run");
|
|
145
|
+
const attempts = (step as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
146
|
+
expect(attempts).toHaveLength(2);
|
|
147
|
+
expect(attempts[1]).toMatchObject({
|
|
141
148
|
state: "failed",
|
|
142
149
|
errorMessage: "Error: test",
|
|
143
|
-
errorName: "Error"
|
|
144
|
-
attemptCount: 2
|
|
150
|
+
errorName: "Error"
|
|
145
151
|
});
|
|
146
152
|
});
|
|
147
153
|
} finally {
|
|
@@ -164,7 +170,7 @@ describe("WorkflowRuntime", () => {
|
|
|
164
170
|
resolve(status);
|
|
165
171
|
};
|
|
166
172
|
|
|
167
|
-
await instance.create(
|
|
173
|
+
await instance.create();
|
|
168
174
|
await expect(promise).resolves.toBe("failed");
|
|
169
175
|
});
|
|
170
176
|
} finally {
|
|
@@ -194,15 +200,16 @@ describe("WorkflowRuntime", () => {
|
|
|
194
200
|
if (status === "running") return;
|
|
195
201
|
resolve(status);
|
|
196
202
|
};
|
|
197
|
-
await instance.create(
|
|
203
|
+
await instance.create();
|
|
198
204
|
await expect(promise).resolves.toBe("completed");
|
|
199
205
|
const steps = instance.getSteps_experimental();
|
|
200
206
|
expect(steps).toHaveLength(1);
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
});
|
|
207
|
+
const step = steps[0]!;
|
|
208
|
+
expect(step.type).toBe("run");
|
|
209
|
+
const attempts = (step as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
210
|
+
expect(attempts).toHaveLength(2);
|
|
211
|
+
expect(attempts[0]).toMatchObject({ state: "failed" });
|
|
212
|
+
expect(attempts[1]).toMatchObject({ state: "succeeded" });
|
|
206
213
|
// First next(): failed attempt yields suspended. Retry alarm: second next() replays `execute()` and completes
|
|
207
214
|
// the successful attempt in the same invocation (no extra immediate loop).
|
|
208
215
|
expect(nextSpy).toHaveBeenCalledTimes(2);
|
|
@@ -230,7 +237,7 @@ describe("WorkflowRuntime", () => {
|
|
|
230
237
|
resolve(status);
|
|
231
238
|
};
|
|
232
239
|
|
|
233
|
-
await instance.create(
|
|
240
|
+
await instance.create();
|
|
234
241
|
await expect(promise).resolves.toBe("failed");
|
|
235
242
|
const steps = instance.getSteps_experimental();
|
|
236
243
|
expect(steps).toHaveLength(1);
|
|
@@ -261,13 +268,21 @@ describe("WorkflowRuntime", () => {
|
|
|
261
268
|
resolve(status);
|
|
262
269
|
};
|
|
263
270
|
|
|
264
|
-
await instance.create(
|
|
271
|
+
await instance.create();
|
|
265
272
|
await expect(promise).resolves.toBe("completed");
|
|
266
273
|
|
|
267
274
|
const steps = instance.getSteps_experimental();
|
|
268
275
|
expect(steps).toHaveLength(2);
|
|
269
|
-
|
|
270
|
-
expect(
|
|
276
|
+
const firstStep = steps[0]!;
|
|
277
|
+
expect(firstStep.id).toBe("step-a");
|
|
278
|
+
expect(firstStep.type).toBe("run");
|
|
279
|
+
const firstAttempts = (firstStep as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
280
|
+
expect(firstAttempts[firstAttempts.length - 1]).toMatchObject({ state: "succeeded" });
|
|
281
|
+
const secondStep = steps[1]!;
|
|
282
|
+
expect(secondStep.id).toBe("step-b");
|
|
283
|
+
expect(secondStep.type).toBe("run");
|
|
284
|
+
const secondAttempts = (secondStep as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
285
|
+
expect(secondAttempts[secondAttempts.length - 1]).toMatchObject({ state: "succeeded" });
|
|
271
286
|
|
|
272
287
|
expect(nextSpy).toHaveBeenCalledTimes(2);
|
|
273
288
|
});
|
|
@@ -296,11 +311,14 @@ describe("WorkflowRuntime", () => {
|
|
|
296
311
|
resolve(status);
|
|
297
312
|
};
|
|
298
313
|
|
|
299
|
-
await instance.create(
|
|
314
|
+
await instance.create();
|
|
300
315
|
await expect(promise).resolves.toBe("completed");
|
|
301
316
|
|
|
302
317
|
const steps = instance.getSteps_experimental();
|
|
303
|
-
|
|
318
|
+
const before = steps.find((s) => s.id === "before-sleep");
|
|
319
|
+
expect(before?.type).toBe("run");
|
|
320
|
+
const beforeAttempts = (before as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
321
|
+
expect(beforeAttempts[beforeAttempts.length - 1]).toMatchObject({ state: "succeeded" });
|
|
304
322
|
expect(steps.find((s) => s.id === "sleep-after-run")).toMatchObject({
|
|
305
323
|
type: "sleep",
|
|
306
324
|
state: "elapsed",
|
|
@@ -333,7 +351,7 @@ describe("WorkflowRuntime", () => {
|
|
|
333
351
|
if (status === "running") return;
|
|
334
352
|
resolve(status);
|
|
335
353
|
};
|
|
336
|
-
await instance.create(
|
|
354
|
+
await instance.create(input);
|
|
337
355
|
await expect(promise).resolves.toBe("completed");
|
|
338
356
|
});
|
|
339
357
|
expect(received.length).toBeGreaterThanOrEqual(1);
|
|
@@ -345,27 +363,80 @@ describe("WorkflowRuntime", () => {
|
|
|
345
363
|
}
|
|
346
364
|
});
|
|
347
365
|
|
|
348
|
-
it("
|
|
366
|
+
it("does not repin input after the workflow is initialized", async () => {
|
|
367
|
+
const received: unknown[] = [];
|
|
349
368
|
const executeSpy = vi
|
|
350
369
|
.spyOn(TestWorkflowDefinition.prototype, "execute")
|
|
351
370
|
.mockImplementation(async function (this: TestWorkflowDefinition) {
|
|
352
|
-
|
|
371
|
+
received.push(this.ctx.props.input);
|
|
372
|
+
await this.wait("wait-1", "event-done", {
|
|
353
373
|
timeoutAt: Date.now() + 86_400_000
|
|
354
374
|
});
|
|
355
375
|
});
|
|
356
376
|
try {
|
|
357
377
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
358
378
|
await runInDurableObject(stub, async (instance) => {
|
|
359
|
-
const { resolve, promise } = Promise.withResolvers<WorkflowStatus>();
|
|
379
|
+
const { resolve: resolveRunning, promise: running } = Promise.withResolvers<WorkflowStatus>();
|
|
380
|
+
const { resolve: resolveDone, promise: done } = Promise.withResolvers<WorkflowStatus>();
|
|
360
381
|
instance.onStatusChange_experimental = async (status) => {
|
|
361
|
-
|
|
382
|
+
if (status === "running") {
|
|
383
|
+
resolveRunning(status);
|
|
384
|
+
} else {
|
|
385
|
+
resolveDone(status);
|
|
386
|
+
}
|
|
362
387
|
};
|
|
363
|
-
|
|
364
|
-
await
|
|
388
|
+
const input = { key: "original" };
|
|
389
|
+
await instance.create(input);
|
|
390
|
+
await expect(running).resolves.toBe("running");
|
|
365
391
|
|
|
366
|
-
await
|
|
367
|
-
|
|
368
|
-
);
|
|
392
|
+
await instance.create({ key: "ignored" });
|
|
393
|
+
await instance.handleInboundEvent("event-done");
|
|
394
|
+
await expect(done).resolves.toBe("completed");
|
|
395
|
+
|
|
396
|
+
expect(received.length).toBeGreaterThanOrEqual(1);
|
|
397
|
+
for (const row of received) {
|
|
398
|
+
expect(row).toEqual(input);
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
} finally {
|
|
402
|
+
executeSpy.mockRestore();
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it("does not repin undefined input after the workflow is initialized", async () => {
|
|
407
|
+
const received: unknown[] = [];
|
|
408
|
+
const executeSpy = vi
|
|
409
|
+
.spyOn(TestWorkflowDefinition.prototype, "execute")
|
|
410
|
+
.mockImplementation(async function (this: TestWorkflowDefinition) {
|
|
411
|
+
received.push(this.ctx.props.input);
|
|
412
|
+
await this.wait("wait-1", "event-done", {
|
|
413
|
+
timeoutAt: Date.now() + 86_400_000
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
try {
|
|
417
|
+
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
418
|
+
await runInDurableObject(stub, async (instance) => {
|
|
419
|
+
const { resolve: resolveRunning, promise: running } = Promise.withResolvers<WorkflowStatus>();
|
|
420
|
+
const { resolve: resolveDone, promise: done } = Promise.withResolvers<WorkflowStatus>();
|
|
421
|
+
instance.onStatusChange_experimental = async (status) => {
|
|
422
|
+
if (status === "running") {
|
|
423
|
+
resolveRunning(status);
|
|
424
|
+
} else {
|
|
425
|
+
resolveDone(status);
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
await instance.create();
|
|
430
|
+
await expect(running).resolves.toBe("running");
|
|
431
|
+
|
|
432
|
+
await instance.create({ key: "ignored" });
|
|
433
|
+
await instance.handleInboundEvent("event-done");
|
|
434
|
+
await expect(done).resolves.toBe("completed");
|
|
435
|
+
|
|
436
|
+
expect(received.length).toBeGreaterThanOrEqual(1);
|
|
437
|
+
for (const row of received) {
|
|
438
|
+
expect(row).toBeUndefined();
|
|
439
|
+
}
|
|
369
440
|
});
|
|
370
441
|
} finally {
|
|
371
442
|
executeSpy.mockRestore();
|
|
@@ -380,11 +451,11 @@ describe("WorkflowRuntime", () => {
|
|
|
380
451
|
if (status === "running") return;
|
|
381
452
|
resolve(status);
|
|
382
453
|
};
|
|
383
|
-
await instance.create(
|
|
454
|
+
await instance.create();
|
|
384
455
|
await expect(promise).resolves.toBe("completed");
|
|
385
456
|
expect(instance.getStatus()).toBe("completed");
|
|
386
457
|
|
|
387
|
-
await instance.create(
|
|
458
|
+
await instance.create();
|
|
388
459
|
expect(instance.getStatus()).toBe("completed");
|
|
389
460
|
});
|
|
390
461
|
});
|
|
@@ -410,30 +481,20 @@ describe("WorkflowRuntime", () => {
|
|
|
410
481
|
resolve(status);
|
|
411
482
|
};
|
|
412
483
|
|
|
413
|
-
await instance.create(
|
|
484
|
+
await instance.create();
|
|
414
485
|
await expect(promise).resolves.toBe("completed");
|
|
415
486
|
|
|
416
487
|
const steps = instance.getSteps_experimental();
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
type: "run",
|
|
424
|
-
parentStepId: "L0",
|
|
425
|
-
state: "succeeded"
|
|
426
|
-
});
|
|
427
|
-
expect(steps.find((s) => s.id === "L2")).toMatchObject({
|
|
428
|
-
type: "run",
|
|
429
|
-
parentStepId: "L1",
|
|
430
|
-
state: "succeeded"
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
const events = await instance.getStepEvents_experimental();
|
|
434
|
-
for (const id of ["L0", "L1"] as const) {
|
|
435
|
-
expect(events.filter((event) => event.stepId === id && event.type === "attempt_failed")).toHaveLength(0);
|
|
488
|
+
for (const id of ["L0", "L1", "L2"] as const) {
|
|
489
|
+
const row = steps.find((s) => s.id === id);
|
|
490
|
+
expect(row?.type).toBe("run");
|
|
491
|
+
const attempts = (row as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
492
|
+
expect(attempts.filter((a) => a.state === "failed")).toHaveLength(0);
|
|
493
|
+
expect(attempts[attempts.length - 1]).toMatchObject({ state: "succeeded" });
|
|
436
494
|
}
|
|
495
|
+
expect(steps.find((s) => s.id === "L0")).toMatchObject({ parentStepId: null });
|
|
496
|
+
expect(steps.find((s) => s.id === "L1")).toMatchObject({ parentStepId: "L0" });
|
|
497
|
+
expect(steps.find((s) => s.id === "L2")).toMatchObject({ parentStepId: "L1" });
|
|
437
498
|
});
|
|
438
499
|
} finally {
|
|
439
500
|
executeSpy.mockRestore();
|
|
@@ -458,15 +519,15 @@ describe("WorkflowRuntime", () => {
|
|
|
458
519
|
resolve(status);
|
|
459
520
|
};
|
|
460
521
|
|
|
461
|
-
await instance.create(
|
|
522
|
+
await instance.create();
|
|
462
523
|
await expect(promise).resolves.toBe("completed");
|
|
463
524
|
|
|
464
525
|
const steps = instance.getSteps_experimental();
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
});
|
|
526
|
+
const rootAfter = steps.find((s) => s.id === "root-after");
|
|
527
|
+
expect(rootAfter?.type).toBe("run");
|
|
528
|
+
expect(rootAfter).toMatchObject({ parentStepId: null });
|
|
529
|
+
const raa = (rootAfter as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
530
|
+
expect(raa[raa.length - 1]).toMatchObject({ state: "succeeded" });
|
|
470
531
|
expect(steps.find((s) => s.id === "nest-inner")).toMatchObject({
|
|
471
532
|
parentStepId: "nest-outer"
|
|
472
533
|
});
|
|
@@ -500,34 +561,19 @@ describe("WorkflowRuntime", () => {
|
|
|
500
561
|
resolve(status);
|
|
501
562
|
};
|
|
502
563
|
|
|
503
|
-
await instance.create(
|
|
564
|
+
await instance.create();
|
|
504
565
|
await expect(promise).resolves.toBe("completed");
|
|
505
566
|
|
|
506
567
|
const steps = instance.getSteps_experimental();
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
});
|
|
517
|
-
expect(steps.find((s) => s.id === "branch-b")).toMatchObject({
|
|
518
|
-
type: "run",
|
|
519
|
-
parentStepId: null,
|
|
520
|
-
state: "succeeded"
|
|
521
|
-
});
|
|
522
|
-
expect(steps.find((s) => s.id === "branch-b-inner")).toMatchObject({
|
|
523
|
-
type: "run",
|
|
524
|
-
parentStepId: "branch-b",
|
|
525
|
-
state: "succeeded"
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
const events = await instance.getStepEvents_experimental();
|
|
529
|
-
expect(events.filter((event) => event.stepId === "branch-a" && event.type === "attempt_failed")).toHaveLength(0);
|
|
530
|
-
expect(events.filter((event) => event.stepId === "branch-b" && event.type === "attempt_failed")).toHaveLength(0);
|
|
568
|
+
for (const id of ["branch-a", "branch-a-inner", "branch-b", "branch-b-inner"] as const) {
|
|
569
|
+
const row = steps.find((s) => s.id === id);
|
|
570
|
+
expect(row?.type).toBe("run");
|
|
571
|
+
const attempts = (row as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
572
|
+
expect(attempts.filter((a) => a.state === "failed")).toHaveLength(0);
|
|
573
|
+
expect(attempts[attempts.length - 1]).toMatchObject({ state: "succeeded" });
|
|
574
|
+
}
|
|
575
|
+
expect(steps.find((s) => s.id === "branch-a-inner")).toMatchObject({ parentStepId: "branch-a" });
|
|
576
|
+
expect(steps.find((s) => s.id === "branch-b-inner")).toMatchObject({ parentStepId: "branch-b" });
|
|
531
577
|
});
|
|
532
578
|
} finally {
|
|
533
579
|
executeSpy.mockRestore();
|
|
@@ -556,15 +602,15 @@ describe("WorkflowRuntime", () => {
|
|
|
556
602
|
resolve(status);
|
|
557
603
|
};
|
|
558
604
|
|
|
559
|
-
await instance.create(
|
|
605
|
+
await instance.create();
|
|
560
606
|
await expect(promise).resolves.toBe("completed");
|
|
561
607
|
|
|
562
608
|
const steps = instance.getSteps_experimental();
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
});
|
|
609
|
+
const nbInner = steps.find((s) => s.id === "nested-branch-inner");
|
|
610
|
+
expect(nbInner?.type).toBe("run");
|
|
611
|
+
expect(nbInner).toMatchObject({ parentStepId: "nested-branch" });
|
|
612
|
+
const nbia = (nbInner as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
613
|
+
expect(nbia[nbia.length - 1]).toMatchObject({ state: "succeeded" });
|
|
568
614
|
expect(steps.find((s) => s.id === "parallel-wait-nested")).toMatchObject({
|
|
569
615
|
type: "wait",
|
|
570
616
|
state: "waiting"
|
|
@@ -592,7 +638,7 @@ describe("WorkflowRuntime", () => {
|
|
|
592
638
|
resolve(status);
|
|
593
639
|
};
|
|
594
640
|
|
|
595
|
-
await instance.create(
|
|
641
|
+
await instance.create();
|
|
596
642
|
await expect(promise).resolves.toBe("completed");
|
|
597
643
|
|
|
598
644
|
const steps = instance.getSteps_experimental();
|
|
@@ -626,10 +672,13 @@ describe("WorkflowRuntime", () => {
|
|
|
626
672
|
resolve(status);
|
|
627
673
|
};
|
|
628
674
|
|
|
629
|
-
await instance.create(
|
|
675
|
+
await instance.create();
|
|
630
676
|
|
|
631
677
|
await expect
|
|
632
|
-
.poll(() =>
|
|
678
|
+
.poll(() => {
|
|
679
|
+
const step = instance.getSteps_experimental().find((s) => s.id === "deep-wait");
|
|
680
|
+
return step?.type === "wait" ? step.state : undefined;
|
|
681
|
+
})
|
|
633
682
|
.toBe("waiting");
|
|
634
683
|
|
|
635
684
|
const stepsWaiting = instance.getSteps_experimental();
|
|
@@ -646,12 +695,12 @@ describe("WorkflowRuntime", () => {
|
|
|
646
695
|
type: "wait",
|
|
647
696
|
parentStepId: "outer-wait",
|
|
648
697
|
state: "satisfied",
|
|
649
|
-
payload:
|
|
650
|
-
});
|
|
651
|
-
expect(instance.getSteps_experimental().find((s) => s.id === "outer-wait")).toMatchObject({
|
|
652
|
-
type: "run",
|
|
653
|
-
state: "succeeded"
|
|
698
|
+
payload: { ok: true }
|
|
654
699
|
});
|
|
700
|
+
const outerWait = instance.getSteps_experimental().find((s) => s.id === "outer-wait");
|
|
701
|
+
expect(outerWait?.type).toBe("run");
|
|
702
|
+
const owa = (outerWait as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
703
|
+
expect(owa[owa.length - 1]).toMatchObject({ state: "succeeded" });
|
|
655
704
|
});
|
|
656
705
|
} finally {
|
|
657
706
|
executeSpy.mockRestore();
|
|
@@ -677,20 +726,18 @@ describe("WorkflowRuntime", () => {
|
|
|
677
726
|
resolve(status);
|
|
678
727
|
};
|
|
679
728
|
|
|
680
|
-
await instance.create(
|
|
729
|
+
await instance.create();
|
|
681
730
|
await expect(promise).resolves.toBe("failed");
|
|
682
731
|
|
|
683
732
|
const steps = instance.getSteps_experimental();
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
expect(
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
errorName: "NonRetryableStepError"
|
|
693
|
-
});
|
|
733
|
+
const fi = steps.find((s) => s.id === "fail-inner");
|
|
734
|
+
expect(fi?.type).toBe("run");
|
|
735
|
+
const fia = (fi as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
736
|
+
expect(fia[fia.length - 1]).toMatchObject({ state: "failed", errorName: "NonRetryableStepError" });
|
|
737
|
+
const fo = steps.find((s) => s.id === "fail-outer");
|
|
738
|
+
expect(fo?.type).toBe("run");
|
|
739
|
+
const foa = (fo as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
740
|
+
expect(foa[foa.length - 1]).toMatchObject({ state: "failed", errorName: "NonRetryableStepError" });
|
|
694
741
|
});
|
|
695
742
|
} finally {
|
|
696
743
|
executeSpy.mockRestore();
|
|
@@ -726,20 +773,19 @@ describe("WorkflowRuntime", () => {
|
|
|
726
773
|
resolve(status);
|
|
727
774
|
};
|
|
728
775
|
|
|
729
|
-
await instance.create(
|
|
776
|
+
await instance.create();
|
|
730
777
|
await expect(promise).resolves.toBe("completed");
|
|
731
778
|
|
|
732
779
|
expect(innerAttempts).toBe(2);
|
|
733
|
-
const
|
|
734
|
-
expect(
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
expect(
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
});
|
|
780
|
+
const outerRow = instance.getSteps_experimental().find((s) => s.id === "suspend-outer");
|
|
781
|
+
expect(outerRow?.type).toBe("run");
|
|
782
|
+
const oa = (outerRow as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
783
|
+
expect(oa.filter((a) => a.state === "failed")).toHaveLength(0);
|
|
784
|
+
expect(oa[oa.length - 1]).toMatchObject({ state: "succeeded" });
|
|
785
|
+
const innerRow = instance.getSteps_experimental().find((s) => s.id === "suspend-inner");
|
|
786
|
+
expect(innerRow?.type).toBe("run");
|
|
787
|
+
const ia = (innerRow as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
788
|
+
expect(ia[ia.length - 1]).toMatchObject({ state: "succeeded" });
|
|
743
789
|
});
|
|
744
790
|
} finally {
|
|
745
791
|
executeSpy.mockRestore();
|
|
@@ -769,21 +815,24 @@ describe("WorkflowRuntime", () => {
|
|
|
769
815
|
resolve(status);
|
|
770
816
|
};
|
|
771
817
|
|
|
772
|
-
await instance.create(
|
|
818
|
+
await instance.create();
|
|
773
819
|
await expect(promise).resolves.toBe("failed");
|
|
774
820
|
|
|
775
821
|
const steps = instance.getSteps_experimental();
|
|
776
|
-
|
|
777
|
-
|
|
822
|
+
const exInner = steps.find((s) => s.id === "ex-inner");
|
|
823
|
+
expect(exInner?.type).toBe("run");
|
|
824
|
+
const exIa = (exInner as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
825
|
+
expect(exIa[exIa.length - 1]).toMatchObject({
|
|
778
826
|
state: "failed",
|
|
779
827
|
errorName: "Error",
|
|
780
828
|
errorMessage: "Error: always fail"
|
|
781
829
|
});
|
|
782
|
-
|
|
783
|
-
|
|
830
|
+
const exOuter = steps.find((s) => s.id === "ex-outer");
|
|
831
|
+
expect(exOuter?.type).toBe("run");
|
|
832
|
+
const exOa = (exOuter as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
833
|
+
expect(exOa[exOa.length - 1]).toMatchObject({
|
|
784
834
|
state: "failed",
|
|
785
|
-
errorName: "Error"
|
|
786
|
-
errorMessage: "Error"
|
|
835
|
+
errorName: "Error"
|
|
787
836
|
});
|
|
788
837
|
});
|
|
789
838
|
} finally {
|
|
@@ -809,16 +858,18 @@ describe("WorkflowRuntime", () => {
|
|
|
809
858
|
resolve(status);
|
|
810
859
|
};
|
|
811
860
|
|
|
812
|
-
await instance.create(
|
|
861
|
+
await instance.create();
|
|
813
862
|
await expect(promise).resolves.toBe("failed");
|
|
814
863
|
|
|
815
864
|
const steps = instance.getSteps_experimental();
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
});
|
|
820
|
-
|
|
821
|
-
|
|
865
|
+
const pi = steps.find((s) => s.id === "post-inner");
|
|
866
|
+
expect(pi?.type).toBe("run");
|
|
867
|
+
const pia = (pi as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
868
|
+
expect(pia[pia.length - 1]).toMatchObject({ state: "succeeded" });
|
|
869
|
+
const po = steps.find((s) => s.id === "post-outer");
|
|
870
|
+
expect(po?.type).toBe("run");
|
|
871
|
+
const poa = (po as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
872
|
+
expect(poa[poa.length - 1]).toMatchObject({
|
|
822
873
|
state: "failed",
|
|
823
874
|
errorMessage: expect.stringContaining("outer-only failure")
|
|
824
875
|
});
|
|
@@ -847,23 +898,27 @@ describe("WorkflowRuntime", () => {
|
|
|
847
898
|
resolve(status);
|
|
848
899
|
};
|
|
849
900
|
|
|
850
|
-
await instance.create(
|
|
901
|
+
await instance.create();
|
|
851
902
|
await expect
|
|
852
|
-
.poll(() =>
|
|
903
|
+
.poll(() => {
|
|
904
|
+
const step = instance.getSteps_experimental().find((s) => s.id === "root-deep-wait");
|
|
905
|
+
return step?.type === "wait" ? step.state : undefined;
|
|
906
|
+
})
|
|
853
907
|
.toBe("waiting");
|
|
854
908
|
|
|
855
|
-
const
|
|
856
|
-
expect(
|
|
857
|
-
|
|
858
|
-
).toHaveLength(0);
|
|
909
|
+
const rootRunBefore = instance.getSteps_experimental().find((s) => s.id === "root-wait-run");
|
|
910
|
+
expect(rootRunBefore?.type).toBe("run");
|
|
911
|
+
const rba = (rootRunBefore as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
912
|
+
expect(rba.filter((a) => a.state === "failed")).toHaveLength(0);
|
|
859
913
|
|
|
860
914
|
await instance.handleInboundEvent("root-deep-ev", true);
|
|
861
915
|
await expect(promise).resolves.toBe("completed");
|
|
862
916
|
|
|
863
|
-
const
|
|
864
|
-
expect(
|
|
865
|
-
|
|
866
|
-
).toHaveLength(0);
|
|
917
|
+
const rootRunAfter = instance.getSteps_experimental().find((s) => s.id === "root-wait-run");
|
|
918
|
+
expect(rootRunAfter?.type).toBe("run");
|
|
919
|
+
const raa = (rootRunAfter as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
920
|
+
expect(raa.filter((a) => a.state === "failed")).toHaveLength(0);
|
|
921
|
+
expect(raa[raa.length - 1]).toMatchObject({ state: "succeeded" });
|
|
867
922
|
});
|
|
868
923
|
} finally {
|
|
869
924
|
executeSpy.mockRestore();
|
|
@@ -893,15 +948,15 @@ describe("WorkflowRuntime", () => {
|
|
|
893
948
|
resolve(status);
|
|
894
949
|
};
|
|
895
950
|
|
|
896
|
-
await instance.create(
|
|
951
|
+
await instance.create();
|
|
897
952
|
await expect(promise).resolves.toBe("completed");
|
|
898
953
|
|
|
899
954
|
const steps = instance.getSteps_experimental();
|
|
900
955
|
expect(steps).toHaveLength(2);
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
});
|
|
956
|
+
const prun = steps.find((s) => s.id === "parallel-run");
|
|
957
|
+
expect(prun?.type).toBe("run");
|
|
958
|
+
const pra = (prun as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
959
|
+
expect(pra[pra.length - 1]).toMatchObject({ state: "succeeded" });
|
|
905
960
|
expect(steps.find((s) => s.id === "parallel-wait")).toMatchObject({
|
|
906
961
|
type: "wait",
|
|
907
962
|
state: "waiting"
|
|
@@ -932,20 +987,19 @@ describe("WorkflowRuntime", () => {
|
|
|
932
987
|
resolve(status);
|
|
933
988
|
};
|
|
934
989
|
|
|
935
|
-
await instance.create(
|
|
990
|
+
await instance.create();
|
|
936
991
|
await expect(promise).resolves.toBe("completed");
|
|
937
992
|
|
|
938
993
|
const steps = instance.getSteps_experimental();
|
|
939
994
|
expect(steps).toHaveLength(2);
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
expect(
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
});
|
|
995
|
+
const pf = steps.find((s) => s.id === "parallel-fail");
|
|
996
|
+
expect(pf?.type).toBe("run");
|
|
997
|
+
const pfa = (pf as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
998
|
+
expect(pfa[pfa.length - 1]).toMatchObject({ state: "failed", errorName: "NonRetryableStepError" });
|
|
999
|
+
const pok = steps.find((s) => s.id === "parallel-ok");
|
|
1000
|
+
expect(pok?.type).toBe("run");
|
|
1001
|
+
const poka = (pok as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
1002
|
+
expect(poka[poka.length - 1]).toMatchObject({ state: "succeeded" });
|
|
949
1003
|
});
|
|
950
1004
|
} finally {
|
|
951
1005
|
executeSpy.mockRestore();
|
|
@@ -977,12 +1031,15 @@ describe("WorkflowRuntime", () => {
|
|
|
977
1031
|
terminalStatuses.push(status);
|
|
978
1032
|
};
|
|
979
1033
|
|
|
980
|
-
await instance.create(
|
|
1034
|
+
await instance.create();
|
|
981
1035
|
await expect.poll(() => instance.getStatus()).toBe("running");
|
|
982
1036
|
expect(terminalStatuses).toHaveLength(0);
|
|
983
1037
|
|
|
984
1038
|
await expect
|
|
985
|
-
.poll(() =>
|
|
1039
|
+
.poll(() => {
|
|
1040
|
+
const step = instance.getSteps_experimental().find((s) => s.id === "allsettled-rerun-wait");
|
|
1041
|
+
return step?.type === "wait" ? step.state : undefined;
|
|
1042
|
+
})
|
|
986
1043
|
.toBe("waiting");
|
|
987
1044
|
|
|
988
1045
|
const steps = instance.getSteps_experimental();
|
|
@@ -992,10 +1049,10 @@ describe("WorkflowRuntime", () => {
|
|
|
992
1049
|
});
|
|
993
1050
|
// Unlike `Promise.all`, `allSettled` waits for every branch before returning, so the run can finish
|
|
994
1051
|
// durably before we rethrow the `wait()` rejection.
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
});
|
|
1052
|
+
const asRun = steps.find((s) => s.id === "allsettled-rerun-run");
|
|
1053
|
+
expect(asRun?.type).toBe("run");
|
|
1054
|
+
const asra = (asRun as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
1055
|
+
expect(asra[asra.length - 1]).toMatchObject({ state: "succeeded" });
|
|
999
1056
|
});
|
|
1000
1057
|
} finally {
|
|
1001
1058
|
executeSpy.mockRestore();
|
|
@@ -1024,12 +1081,15 @@ describe("WorkflowRuntime", () => {
|
|
|
1024
1081
|
terminalStatuses.push(status);
|
|
1025
1082
|
};
|
|
1026
1083
|
|
|
1027
|
-
await instance.create(
|
|
1084
|
+
await instance.create();
|
|
1028
1085
|
await expect.poll(() => instance.getStatus()).toBe("running");
|
|
1029
1086
|
expect(terminalStatuses).toHaveLength(0);
|
|
1030
1087
|
|
|
1031
1088
|
await expect
|
|
1032
|
-
.poll(() =>
|
|
1089
|
+
.poll(() => {
|
|
1090
|
+
const step = instance.getSteps_experimental().find((s) => s.id === "parallel-wait");
|
|
1091
|
+
return step?.type === "wait" ? step.state : undefined;
|
|
1092
|
+
})
|
|
1033
1093
|
.toBe("waiting");
|
|
1034
1094
|
|
|
1035
1095
|
const steps = instance.getSteps_experimental();
|
|
@@ -1037,11 +1097,11 @@ describe("WorkflowRuntime", () => {
|
|
|
1037
1097
|
type: "wait",
|
|
1038
1098
|
state: "waiting"
|
|
1039
1099
|
});
|
|
1040
|
-
// `wait()` usually wins the race; the run branch can still be mid-flight so the
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
});
|
|
1100
|
+
// `wait()` usually wins the race; the run branch can still be mid-flight so the latest attempt may still be in flight.
|
|
1101
|
+
const parRun = steps.find((s) => s.id === "parallel-run");
|
|
1102
|
+
expect(parRun?.type).toBe("run");
|
|
1103
|
+
const paa = (parRun as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
1104
|
+
expect(paa[paa.length - 1]).toMatchObject({ state: "started" });
|
|
1045
1105
|
});
|
|
1046
1106
|
} finally {
|
|
1047
1107
|
executeSpy.mockRestore();
|
|
@@ -1058,7 +1118,7 @@ describe("WorkflowRuntime", () => {
|
|
|
1058
1118
|
if (status === "running") return;
|
|
1059
1119
|
resolve(status);
|
|
1060
1120
|
};
|
|
1061
|
-
await instance.create(
|
|
1121
|
+
await instance.create();
|
|
1062
1122
|
await expect(promise).resolves.toBe("completed");
|
|
1063
1123
|
});
|
|
1064
1124
|
|
|
@@ -1077,7 +1137,7 @@ describe("WorkflowRuntime", () => {
|
|
|
1077
1137
|
if (status === "running") return;
|
|
1078
1138
|
resolve(status);
|
|
1079
1139
|
};
|
|
1080
|
-
await instance.create(
|
|
1140
|
+
await instance.create();
|
|
1081
1141
|
await expect(promise).resolves.toBe("failed");
|
|
1082
1142
|
});
|
|
1083
1143
|
|
|
@@ -1110,10 +1170,13 @@ describe("WorkflowRuntime", () => {
|
|
|
1110
1170
|
try {
|
|
1111
1171
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1112
1172
|
await runInDurableObject(stub, async (instance) => {
|
|
1113
|
-
await instance.create(
|
|
1173
|
+
await instance.create();
|
|
1114
1174
|
await expect.poll(() => instance.getStatus()).toBe("running");
|
|
1115
1175
|
await expect
|
|
1116
|
-
.poll(() =>
|
|
1176
|
+
.poll(() => {
|
|
1177
|
+
const step = instance.getSteps_experimental().find((s) => s.id === "wait-1");
|
|
1178
|
+
return step?.type === "wait" ? step.state : undefined;
|
|
1179
|
+
})
|
|
1117
1180
|
.toBe("waiting");
|
|
1118
1181
|
|
|
1119
1182
|
await instance.pause();
|
|
@@ -1141,7 +1204,7 @@ describe("WorkflowRuntime", () => {
|
|
|
1141
1204
|
if (status === "paused") resolve();
|
|
1142
1205
|
};
|
|
1143
1206
|
|
|
1144
|
-
await instance.create(
|
|
1207
|
+
await instance.create();
|
|
1145
1208
|
await instance.pause();
|
|
1146
1209
|
await promise;
|
|
1147
1210
|
});
|
|
@@ -1169,7 +1232,7 @@ describe("WorkflowRuntime", () => {
|
|
|
1169
1232
|
try {
|
|
1170
1233
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1171
1234
|
await runInDurableObject(stub, async (instance) => {
|
|
1172
|
-
await instance.create(
|
|
1235
|
+
await instance.create();
|
|
1173
1236
|
await expect.poll(() => instance.getStatus()).toBe("running");
|
|
1174
1237
|
await instance.pause();
|
|
1175
1238
|
expect(instance.getStatus()).toBe("paused");
|
|
@@ -1198,7 +1261,7 @@ describe("WorkflowRuntime", () => {
|
|
|
1198
1261
|
resolve(status);
|
|
1199
1262
|
};
|
|
1200
1263
|
|
|
1201
|
-
await instance.create(
|
|
1264
|
+
await instance.create();
|
|
1202
1265
|
await expect.poll(() => instance.getStatus()).toBe("running");
|
|
1203
1266
|
await instance.pause();
|
|
1204
1267
|
expect(instance.getStatus()).toBe("paused");
|
|
@@ -1231,7 +1294,7 @@ describe("WorkflowRuntime", () => {
|
|
|
1231
1294
|
try {
|
|
1232
1295
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1233
1296
|
await runInDurableObject(stub, async (instance) => {
|
|
1234
|
-
await instance.create(
|
|
1297
|
+
await instance.create();
|
|
1235
1298
|
await expect.poll(() => instance.getStatus()).toBe("running");
|
|
1236
1299
|
await expect(instance.resume()).rejects.toThrow(
|
|
1237
1300
|
"Cannot resume workflow: expected status 'paused' but got 'running'."
|
|
@@ -1252,10 +1315,13 @@ describe("WorkflowRuntime", () => {
|
|
|
1252
1315
|
try {
|
|
1253
1316
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1254
1317
|
await runInDurableObject(stub, async (instance) => {
|
|
1255
|
-
await instance.create(
|
|
1318
|
+
await instance.create();
|
|
1256
1319
|
|
|
1257
1320
|
await expect
|
|
1258
|
-
.poll(() =>
|
|
1321
|
+
.poll(() => {
|
|
1322
|
+
const step = instance.getSteps_experimental().find((s) => s.id === "wait-1");
|
|
1323
|
+
return step?.type === "wait" ? step.state : undefined;
|
|
1324
|
+
})
|
|
1259
1325
|
.toBe("waiting");
|
|
1260
1326
|
|
|
1261
1327
|
await instance.pause();
|
|
@@ -1293,9 +1359,12 @@ describe("WorkflowRuntime", () => {
|
|
|
1293
1359
|
resolve(status);
|
|
1294
1360
|
};
|
|
1295
1361
|
|
|
1296
|
-
await instance.create(
|
|
1362
|
+
await instance.create();
|
|
1297
1363
|
await expect
|
|
1298
|
-
.poll(() =>
|
|
1364
|
+
.poll(() => {
|
|
1365
|
+
const step = instance.getSteps_experimental().find((s) => s.id === "wait-1");
|
|
1366
|
+
return step?.type === "wait" ? step.state : undefined;
|
|
1367
|
+
})
|
|
1299
1368
|
.toBe("waiting");
|
|
1300
1369
|
await instance.pause();
|
|
1301
1370
|
|
|
@@ -1307,7 +1376,7 @@ describe("WorkflowRuntime", () => {
|
|
|
1307
1376
|
expect(instance.getSteps_experimental().find((s) => s.id === "wait-1")).toMatchObject({
|
|
1308
1377
|
type: "wait",
|
|
1309
1378
|
state: "satisfied",
|
|
1310
|
-
payload:
|
|
1379
|
+
payload: { data: "test" }
|
|
1311
1380
|
});
|
|
1312
1381
|
});
|
|
1313
1382
|
} finally {
|
|
@@ -1327,10 +1396,13 @@ describe("WorkflowRuntime", () => {
|
|
|
1327
1396
|
try {
|
|
1328
1397
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1329
1398
|
await runInDurableObject(stub, async (instance) => {
|
|
1330
|
-
await instance.create(
|
|
1399
|
+
await instance.create();
|
|
1331
1400
|
await expect.poll(() => instance.getStatus()).toBe("running");
|
|
1332
1401
|
await expect
|
|
1333
|
-
.poll(() =>
|
|
1402
|
+
.poll(() => {
|
|
1403
|
+
const step = instance.getSteps_experimental().find((s) => s.id === "wait-1");
|
|
1404
|
+
return step?.type === "wait" ? step.state : undefined;
|
|
1405
|
+
})
|
|
1334
1406
|
.toBe("waiting");
|
|
1335
1407
|
|
|
1336
1408
|
await instance.pause();
|
|
@@ -1348,6 +1420,235 @@ describe("WorkflowRuntime", () => {
|
|
|
1348
1420
|
});
|
|
1349
1421
|
});
|
|
1350
1422
|
|
|
1423
|
+
describe("handleInboundEvent()", () => {
|
|
1424
|
+
it("persists satisfied wait payload on inbound_events with claimed_by pointing at the wait step", async () => {
|
|
1425
|
+
const executeSpy = vi
|
|
1426
|
+
.spyOn(TestWorkflowDefinition.prototype, "execute")
|
|
1427
|
+
.mockImplementation(async function (this: TestWorkflowDefinition) {
|
|
1428
|
+
await this.wait("wait-inbound-row", "evt-claim", {
|
|
1429
|
+
timeoutAt: Date.now() + 86_400_000
|
|
1430
|
+
});
|
|
1431
|
+
});
|
|
1432
|
+
|
|
1433
|
+
try {
|
|
1434
|
+
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1435
|
+
await runInDurableObject(stub, async (instance, state) => {
|
|
1436
|
+
const { resolve, promise } = Promise.withResolvers<WorkflowStatus>();
|
|
1437
|
+
instance.onStatusChange_experimental = async (status) => {
|
|
1438
|
+
if (status === "running") return;
|
|
1439
|
+
resolve(status);
|
|
1440
|
+
};
|
|
1441
|
+
|
|
1442
|
+
await instance.create();
|
|
1443
|
+
|
|
1444
|
+
await expect
|
|
1445
|
+
.poll(() => {
|
|
1446
|
+
const step = instance.getSteps_experimental().find((s) => s.id === "wait-inbound-row");
|
|
1447
|
+
return step?.type === "wait" ? step.state : undefined;
|
|
1448
|
+
})
|
|
1449
|
+
.toBe("waiting");
|
|
1450
|
+
|
|
1451
|
+
await instance.handleInboundEvent("evt-claim", { trace: "x" });
|
|
1452
|
+
await expect(promise).resolves.toBe("completed");
|
|
1453
|
+
|
|
1454
|
+
const formatted = instance.getSteps_experimental().find((s) => s.id === "wait-inbound-row");
|
|
1455
|
+
expect(formatted).toMatchObject({
|
|
1456
|
+
type: "wait",
|
|
1457
|
+
state: "satisfied",
|
|
1458
|
+
payload: { trace: "x" }
|
|
1459
|
+
});
|
|
1460
|
+
|
|
1461
|
+
const rows = state.storage.sql
|
|
1462
|
+
.exec<{ payload: string; claimed_by: string | null }>(
|
|
1463
|
+
`SELECT payload, claimed_by FROM inbound_events WHERE claimed_by = ?`,
|
|
1464
|
+
"wait-inbound-row"
|
|
1465
|
+
)
|
|
1466
|
+
.toArray();
|
|
1467
|
+
expect(rows).toHaveLength(1);
|
|
1468
|
+
expect(rows[0]!.claimed_by).toBe("wait-inbound-row");
|
|
1469
|
+
expect(JSON.parse(rows[0]!.payload)).toEqual({ trace: "x" });
|
|
1470
|
+
});
|
|
1471
|
+
} finally {
|
|
1472
|
+
executeSpy.mockRestore();
|
|
1473
|
+
}
|
|
1474
|
+
});
|
|
1475
|
+
|
|
1476
|
+
it("satisfies a waiting wait step when called without a payload", async () => {
|
|
1477
|
+
const executeSpy = vi
|
|
1478
|
+
.spyOn(TestWorkflowDefinition.prototype, "execute")
|
|
1479
|
+
.mockImplementation(async function (this: TestWorkflowDefinition) {
|
|
1480
|
+
const payload = await this.wait<undefined>("wait-no-payload", "evt");
|
|
1481
|
+
await this.run("after-wait-no-payload", async () => payload);
|
|
1482
|
+
});
|
|
1483
|
+
|
|
1484
|
+
try {
|
|
1485
|
+
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1486
|
+
await runInDurableObject(stub, async (instance) => {
|
|
1487
|
+
const { resolve, promise } = Promise.withResolvers<WorkflowStatus>();
|
|
1488
|
+
instance.onStatusChange_experimental = async (status) => {
|
|
1489
|
+
if (status === "running") return;
|
|
1490
|
+
resolve(status);
|
|
1491
|
+
};
|
|
1492
|
+
|
|
1493
|
+
await instance.create();
|
|
1494
|
+
|
|
1495
|
+
await expect
|
|
1496
|
+
.poll(() => {
|
|
1497
|
+
const step = instance.getSteps_experimental().find((s) => s.id === "wait-no-payload");
|
|
1498
|
+
return step?.type === "wait" ? step.state : undefined;
|
|
1499
|
+
})
|
|
1500
|
+
.toBe("waiting");
|
|
1501
|
+
|
|
1502
|
+
await instance.handleInboundEvent("evt");
|
|
1503
|
+
await expect(promise).resolves.toBe("completed");
|
|
1504
|
+
|
|
1505
|
+
expect(instance.getSteps_experimental().find((s) => s.id === "wait-no-payload")).toMatchObject({
|
|
1506
|
+
type: "wait",
|
|
1507
|
+
state: "satisfied",
|
|
1508
|
+
payload: undefined
|
|
1509
|
+
});
|
|
1510
|
+
|
|
1511
|
+
const afterWait = instance.getSteps_experimental().find((s) => s.id === "after-wait-no-payload");
|
|
1512
|
+
expect(afterWait?.type).toBe("run");
|
|
1513
|
+
const attempts = (afterWait as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
1514
|
+
expect(attempts[attempts.length - 1]).toMatchObject({
|
|
1515
|
+
state: "succeeded",
|
|
1516
|
+
resultType: "none"
|
|
1517
|
+
});
|
|
1518
|
+
});
|
|
1519
|
+
} finally {
|
|
1520
|
+
executeSpy.mockRestore();
|
|
1521
|
+
}
|
|
1522
|
+
});
|
|
1523
|
+
|
|
1524
|
+
it("satisfies a waiting wait step when called with an explicit null payload", async () => {
|
|
1525
|
+
const executeSpy = vi
|
|
1526
|
+
.spyOn(TestWorkflowDefinition.prototype, "execute")
|
|
1527
|
+
.mockImplementation(async function (this: TestWorkflowDefinition) {
|
|
1528
|
+
const payload = await this.wait<null>("wait-null", "evt");
|
|
1529
|
+
await this.run("after-wait-null", async () => payload);
|
|
1530
|
+
});
|
|
1531
|
+
|
|
1532
|
+
try {
|
|
1533
|
+
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1534
|
+
await runInDurableObject(stub, async (instance) => {
|
|
1535
|
+
const { resolve, promise } = Promise.withResolvers<WorkflowStatus>();
|
|
1536
|
+
instance.onStatusChange_experimental = async (status) => {
|
|
1537
|
+
if (status === "running") return;
|
|
1538
|
+
resolve(status);
|
|
1539
|
+
};
|
|
1540
|
+
|
|
1541
|
+
await instance.create();
|
|
1542
|
+
|
|
1543
|
+
await expect
|
|
1544
|
+
.poll(() => {
|
|
1545
|
+
const step = instance.getSteps_experimental().find((s) => s.id === "wait-null");
|
|
1546
|
+
return step?.type === "wait" ? step.state : undefined;
|
|
1547
|
+
})
|
|
1548
|
+
.toBe("waiting");
|
|
1549
|
+
|
|
1550
|
+
await instance.handleInboundEvent("evt", null);
|
|
1551
|
+
await expect(promise).resolves.toBe("completed");
|
|
1552
|
+
|
|
1553
|
+
expect(instance.getSteps_experimental().find((s) => s.id === "wait-null")).toMatchObject({
|
|
1554
|
+
type: "wait",
|
|
1555
|
+
state: "satisfied",
|
|
1556
|
+
payload: null
|
|
1557
|
+
});
|
|
1558
|
+
|
|
1559
|
+
const afterWait = instance.getSteps_experimental().find((s) => s.id === "after-wait-null");
|
|
1560
|
+
expect(afterWait?.type).toBe("run");
|
|
1561
|
+
const attempts = (afterWait as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
1562
|
+
expect(attempts[attempts.length - 1]).toMatchObject({
|
|
1563
|
+
state: "succeeded",
|
|
1564
|
+
resultType: "json",
|
|
1565
|
+
resultJson: "null"
|
|
1566
|
+
});
|
|
1567
|
+
});
|
|
1568
|
+
} finally {
|
|
1569
|
+
executeSpy.mockRestore();
|
|
1570
|
+
}
|
|
1571
|
+
});
|
|
1572
|
+
|
|
1573
|
+
it("satisfies a waiting wait step via queued payloadless event claimed on resume", async () => {
|
|
1574
|
+
const executeSpy = vi
|
|
1575
|
+
.spyOn(TestWorkflowDefinition.prototype, "execute")
|
|
1576
|
+
.mockImplementation(async function (this: TestWorkflowDefinition) {
|
|
1577
|
+
await this.wait("wait-queued", "evt");
|
|
1578
|
+
});
|
|
1579
|
+
|
|
1580
|
+
try {
|
|
1581
|
+
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1582
|
+
await runInDurableObject(stub, async (instance) => {
|
|
1583
|
+
const { resolve, promise } = Promise.withResolvers<WorkflowStatus>();
|
|
1584
|
+
instance.onStatusChange_experimental = async (status) => {
|
|
1585
|
+
if (status === "running" || status === "paused") return;
|
|
1586
|
+
resolve(status);
|
|
1587
|
+
};
|
|
1588
|
+
|
|
1589
|
+
await instance.create();
|
|
1590
|
+
|
|
1591
|
+
await expect
|
|
1592
|
+
.poll(() => {
|
|
1593
|
+
const step = instance.getSteps_experimental().find((s) => s.id === "wait-queued");
|
|
1594
|
+
return step?.type === "wait" ? step.state : undefined;
|
|
1595
|
+
})
|
|
1596
|
+
.toBe("waiting");
|
|
1597
|
+
|
|
1598
|
+
await instance.pause();
|
|
1599
|
+
|
|
1600
|
+
// Queue the event without a payload while paused
|
|
1601
|
+
await instance.handleInboundEvent("evt");
|
|
1602
|
+
|
|
1603
|
+
// The wait step should still be waiting (event was only queued, not claimed)
|
|
1604
|
+
expect(instance.getSteps_experimental().find((s) => s.id === "wait-queued")).toMatchObject({
|
|
1605
|
+
type: "wait",
|
|
1606
|
+
state: "waiting"
|
|
1607
|
+
});
|
|
1608
|
+
|
|
1609
|
+
await instance.resume();
|
|
1610
|
+
await expect(promise).resolves.toBe("completed");
|
|
1611
|
+
|
|
1612
|
+
expect(instance.getSteps_experimental().find((s) => s.id === "wait-queued")).toMatchObject({
|
|
1613
|
+
type: "wait",
|
|
1614
|
+
state: "satisfied",
|
|
1615
|
+
payload: undefined
|
|
1616
|
+
});
|
|
1617
|
+
});
|
|
1618
|
+
} finally {
|
|
1619
|
+
executeSpy.mockRestore();
|
|
1620
|
+
}
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
it("is a no-op when the workflow is in a terminal state", async () => {
|
|
1624
|
+
const executeSpy = vi
|
|
1625
|
+
.spyOn(TestWorkflowDefinition.prototype, "execute")
|
|
1626
|
+
.mockImplementation(async function (this: TestWorkflowDefinition) {
|
|
1627
|
+
await this.run("step-1", async () => "done");
|
|
1628
|
+
});
|
|
1629
|
+
|
|
1630
|
+
try {
|
|
1631
|
+
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1632
|
+
await runInDurableObject(stub, async (instance) => {
|
|
1633
|
+
const { resolve, promise } = Promise.withResolvers<WorkflowStatus>();
|
|
1634
|
+
instance.onStatusChange_experimental = async (status) => {
|
|
1635
|
+
if (status === "running") return;
|
|
1636
|
+
resolve(status);
|
|
1637
|
+
};
|
|
1638
|
+
|
|
1639
|
+
await instance.create();
|
|
1640
|
+
await expect(promise).resolves.toBe("completed");
|
|
1641
|
+
|
|
1642
|
+
// Should not throw even though there is no matching wait step
|
|
1643
|
+
await instance.handleInboundEvent("any-event", { data: 1 });
|
|
1644
|
+
expect(instance.getStatus()).toBe("completed");
|
|
1645
|
+
});
|
|
1646
|
+
} finally {
|
|
1647
|
+
executeSpy.mockRestore();
|
|
1648
|
+
}
|
|
1649
|
+
});
|
|
1650
|
+
});
|
|
1651
|
+
|
|
1351
1652
|
describe("getWorkflowEvents_experimental()", () => {
|
|
1352
1653
|
it("records 'created' when workflow is first initialized", async () => {
|
|
1353
1654
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
@@ -1361,7 +1662,7 @@ describe("WorkflowRuntime", () => {
|
|
|
1361
1662
|
});
|
|
1362
1663
|
});
|
|
1363
1664
|
|
|
1364
|
-
it("records 'started' when workflow transitions from
|
|
1665
|
+
it("records 'started' when workflow transitions from initialized to running", async () => {
|
|
1365
1666
|
const executeSpy = vi
|
|
1366
1667
|
.spyOn(TestWorkflowDefinition.prototype, "execute")
|
|
1367
1668
|
.mockImplementation(async function (this: TestWorkflowDefinition) {
|
|
@@ -1377,7 +1678,7 @@ describe("WorkflowRuntime", () => {
|
|
|
1377
1678
|
resolve(status);
|
|
1378
1679
|
};
|
|
1379
1680
|
|
|
1380
|
-
await instance.create(
|
|
1681
|
+
await instance.create();
|
|
1381
1682
|
await expect(promise).resolves.toBe("completed");
|
|
1382
1683
|
|
|
1383
1684
|
const events = instance.getWorkflowEvents_experimental();
|
|
@@ -1398,7 +1699,7 @@ describe("WorkflowRuntime", () => {
|
|
|
1398
1699
|
try {
|
|
1399
1700
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1400
1701
|
await runInDurableObject(stub, async (instance) => {
|
|
1401
|
-
await instance.create(
|
|
1702
|
+
await instance.create();
|
|
1402
1703
|
await expect.poll(() => instance.getStatus()).toBe("running");
|
|
1403
1704
|
await instance.pause();
|
|
1404
1705
|
|
|
@@ -1426,7 +1727,7 @@ describe("WorkflowRuntime", () => {
|
|
|
1426
1727
|
resolve(status);
|
|
1427
1728
|
};
|
|
1428
1729
|
|
|
1429
|
-
await instance.create(
|
|
1730
|
+
await instance.create();
|
|
1430
1731
|
await expect.poll(() => instance.getStatus()).toBe("running");
|
|
1431
1732
|
await instance.pause();
|
|
1432
1733
|
await instance.resume();
|
|
@@ -1458,7 +1759,7 @@ describe("WorkflowRuntime", () => {
|
|
|
1458
1759
|
resolve(status);
|
|
1459
1760
|
};
|
|
1460
1761
|
|
|
1461
|
-
await instance.create(
|
|
1762
|
+
await instance.create();
|
|
1462
1763
|
await expect(promise).resolves.toBe("failed");
|
|
1463
1764
|
|
|
1464
1765
|
const events = instance.getWorkflowEvents_experimental();
|
|
@@ -1479,7 +1780,7 @@ describe("WorkflowRuntime", () => {
|
|
|
1479
1780
|
try {
|
|
1480
1781
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1481
1782
|
await runInDurableObject(stub, async (instance) => {
|
|
1482
|
-
await instance.create(
|
|
1783
|
+
await instance.create();
|
|
1483
1784
|
await expect.poll(() => instance.getStatus()).toBe("running");
|
|
1484
1785
|
await instance.cancel("user requested cancellation");
|
|
1485
1786
|
|
|
@@ -1515,18 +1816,19 @@ describe("WorkflowRuntime", () => {
|
|
|
1515
1816
|
|
|
1516
1817
|
describe("WorkflowRuntimeContext", () => {
|
|
1517
1818
|
describe("run steps", () => {
|
|
1518
|
-
describe("
|
|
1819
|
+
describe("getOrCreateRunStep()", () => {
|
|
1519
1820
|
it("creates a new run step", async () => {
|
|
1520
1821
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1521
1822
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1522
1823
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1523
|
-
const step =
|
|
1824
|
+
const step = context.getOrCreateRunStep(createRunStepId("step-1"), { parentStepId: null });
|
|
1524
1825
|
expect(step).toMatchObject({
|
|
1525
1826
|
id: "step-1",
|
|
1526
1827
|
type: "run",
|
|
1527
|
-
|
|
1528
|
-
|
|
1828
|
+
maxAttempts: 3,
|
|
1829
|
+
parentStepId: null
|
|
1529
1830
|
});
|
|
1831
|
+
expect(step.attempts).toEqual([]);
|
|
1530
1832
|
});
|
|
1531
1833
|
});
|
|
1532
1834
|
|
|
@@ -1534,25 +1836,19 @@ describe("WorkflowRuntime", () => {
|
|
|
1534
1836
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1535
1837
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1536
1838
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1537
|
-
const first =
|
|
1538
|
-
|
|
1539
|
-
parentStepId: null
|
|
1540
|
-
});
|
|
1541
|
-
const second = await context.getOrCreateStep(createRunStepId("step-1"), {
|
|
1542
|
-
type: "run",
|
|
1543
|
-
parentStepId: null
|
|
1544
|
-
});
|
|
1839
|
+
const first = context.getOrCreateRunStep(createRunStepId("step-1"), { parentStepId: null });
|
|
1840
|
+
const second = context.getOrCreateRunStep(createRunStepId("step-1"), { parentStepId: null });
|
|
1545
1841
|
expect(first).toEqual(second);
|
|
1546
1842
|
});
|
|
1547
1843
|
});
|
|
1548
1844
|
|
|
1549
|
-
it("
|
|
1845
|
+
it("leaves attempts empty until handleRunAttemptStarted", async () => {
|
|
1550
1846
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1551
|
-
await runInDurableObject(stub, async (
|
|
1847
|
+
await runInDurableObject(stub, async (_instance, state) => {
|
|
1552
1848
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1849
|
+
context.getOrCreateRunStep(createRunStepId("step-1"), { parentStepId: null });
|
|
1850
|
+
const step = context.getOrCreateRunStep(createRunStepId("step-1"), { parentStepId: null });
|
|
1851
|
+
expect(step.attempts).toEqual([]);
|
|
1556
1852
|
});
|
|
1557
1853
|
});
|
|
1558
1854
|
|
|
@@ -1560,8 +1856,7 @@ describe("WorkflowRuntime", () => {
|
|
|
1560
1856
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1561
1857
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1562
1858
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1563
|
-
const step =
|
|
1564
|
-
type: "run",
|
|
1859
|
+
const step = context.getOrCreateRunStep(createRunStepId("step-1"), {
|
|
1565
1860
|
maxAttempts: 5,
|
|
1566
1861
|
parentStepId: null
|
|
1567
1862
|
});
|
|
@@ -1574,14 +1869,14 @@ describe("WorkflowRuntime", () => {
|
|
|
1574
1869
|
});
|
|
1575
1870
|
});
|
|
1576
1871
|
|
|
1577
|
-
describe("
|
|
1872
|
+
describe("hasInProgressChildSteps()", () => {
|
|
1578
1873
|
it("returns false when the run step has no direct child rows", async () => {
|
|
1579
1874
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1580
1875
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1581
1876
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1877
|
+
context.getOrCreateRunStep(createRunStepId("leaf"), { parentStepId: null });
|
|
1878
|
+
context.handleRunAttemptStarted(createRunStepId("leaf"));
|
|
1879
|
+
expect(context.hasInProgressChildSteps(createRunStepId("leaf"))).toBe(false);
|
|
1585
1880
|
});
|
|
1586
1881
|
});
|
|
1587
1882
|
|
|
@@ -1589,13 +1884,12 @@ describe("WorkflowRuntime", () => {
|
|
|
1589
1884
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1590
1885
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1591
1886
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
type: "run",
|
|
1887
|
+
context.getOrCreateRunStep(createRunStepId("parent"), { parentStepId: null });
|
|
1888
|
+
context.handleRunAttemptStarted(createRunStepId("parent"));
|
|
1889
|
+
context.getOrCreateRunStep(createRunStepId("child"), {
|
|
1596
1890
|
parentStepId: createRunStepId("parent")
|
|
1597
1891
|
});
|
|
1598
|
-
|
|
1892
|
+
expect(context.hasInProgressChildSteps(createRunStepId("parent"))).toBe(true);
|
|
1599
1893
|
});
|
|
1600
1894
|
});
|
|
1601
1895
|
|
|
@@ -1603,14 +1897,13 @@ describe("WorkflowRuntime", () => {
|
|
|
1603
1897
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1604
1898
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1605
1899
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
type: "run",
|
|
1900
|
+
context.getOrCreateRunStep(createRunStepId("parent"), { parentStepId: null });
|
|
1901
|
+
context.handleRunAttemptStarted(createRunStepId("parent"));
|
|
1902
|
+
context.getOrCreateRunStep(createRunStepId("child"), {
|
|
1610
1903
|
parentStepId: createRunStepId("parent")
|
|
1611
1904
|
});
|
|
1612
|
-
|
|
1613
|
-
|
|
1905
|
+
context.handleRunAttemptStarted(createRunStepId("child"));
|
|
1906
|
+
expect(context.hasInProgressChildSteps(createRunStepId("parent"))).toBe(true);
|
|
1614
1907
|
});
|
|
1615
1908
|
});
|
|
1616
1909
|
|
|
@@ -1618,14 +1911,13 @@ describe("WorkflowRuntime", () => {
|
|
|
1618
1911
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1619
1912
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1620
1913
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
type: "run",
|
|
1914
|
+
context.getOrCreateRunStep(createRunStepId("gp"), { parentStepId: null });
|
|
1915
|
+
context.handleRunAttemptStarted(createRunStepId("gp"));
|
|
1916
|
+
context.getOrCreateRunStep(createRunStepId("mid"), { parentStepId: createRunStepId("gp") });
|
|
1917
|
+
context.getOrCreateRunStep(createRunStepId("leaf"), {
|
|
1626
1918
|
parentStepId: createRunStepId("mid")
|
|
1627
1919
|
});
|
|
1628
|
-
|
|
1920
|
+
expect(context.hasInProgressChildSteps(createRunStepId("gp"))).toBe(true);
|
|
1629
1921
|
});
|
|
1630
1922
|
});
|
|
1631
1923
|
|
|
@@ -1633,14 +1925,13 @@ describe("WorkflowRuntime", () => {
|
|
|
1633
1925
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1634
1926
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1635
1927
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
type: "run",
|
|
1928
|
+
context.getOrCreateRunStep(createRunStepId("mid"), { parentStepId: null });
|
|
1929
|
+
context.handleRunAttemptStarted(createRunStepId("mid"));
|
|
1930
|
+
context.getOrCreateRunStep(createRunStepId("leaf"), {
|
|
1640
1931
|
parentStepId: createRunStepId("mid")
|
|
1641
1932
|
});
|
|
1642
|
-
|
|
1643
|
-
|
|
1933
|
+
expect(context.hasInProgressChildSteps(createRunStepId("mid"))).toBe(true);
|
|
1934
|
+
expect(context.hasInProgressChildSteps(createRunStepId("leaf"))).toBe(false);
|
|
1644
1935
|
});
|
|
1645
1936
|
});
|
|
1646
1937
|
|
|
@@ -1648,20 +1939,15 @@ describe("WorkflowRuntime", () => {
|
|
|
1648
1939
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1649
1940
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1650
1941
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
});
|
|
1657
|
-
await context.handleRunAttemptEvent(createRunStepId("bad-child"), { type: "running", attemptCount: 1 });
|
|
1658
|
-
await context.handleRunAttemptEvent(createRunStepId("bad-child"), {
|
|
1659
|
-
type: "failed",
|
|
1660
|
-
errorMessage: "x",
|
|
1661
|
-
attemptCount: 1,
|
|
1662
|
-
isNonRetryableStepError: true
|
|
1942
|
+
context.getOrCreateRunStep(createRunStepId("par"), { parentStepId: null });
|
|
1943
|
+
context.handleRunAttemptStarted(createRunStepId("par"));
|
|
1944
|
+
context.getOrCreateRunStep(createRunStepId("bad-child"), {
|
|
1945
|
+
parentStepId: createRunStepId("par"),
|
|
1946
|
+
maxAttempts: 1
|
|
1663
1947
|
});
|
|
1664
|
-
|
|
1948
|
+
context.handleRunAttemptStarted(createRunStepId("bad-child"));
|
|
1949
|
+
context.handleRunAttemptFailed(createRunStepId("bad-child"), { errorMessage: "x" });
|
|
1950
|
+
expect(context.hasInProgressChildSteps(createRunStepId("par"))).toBe(false);
|
|
1665
1951
|
});
|
|
1666
1952
|
});
|
|
1667
1953
|
|
|
@@ -1669,57 +1955,28 @@ describe("WorkflowRuntime", () => {
|
|
|
1669
1955
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1670
1956
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1671
1957
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
type: "sleep",
|
|
1677
|
-
wakeAt,
|
|
1958
|
+
context.getOrCreateRunStep(createRunStepId("run-parent"), { parentStepId: null });
|
|
1959
|
+
context.handleRunAttemptStarted(createRunStepId("run-parent"));
|
|
1960
|
+
context.getOrCreateSleepStep(createSleepStepId("child-sleep"), {
|
|
1961
|
+
wakeAt: new Date(Date.now() + 60_000),
|
|
1678
1962
|
parentStepId: createRunStepId("run-parent")
|
|
1679
1963
|
});
|
|
1680
|
-
|
|
1964
|
+
expect(context.hasInProgressChildSteps(createRunStepId("run-parent"))).toBe(true);
|
|
1681
1965
|
});
|
|
1682
1966
|
});
|
|
1683
1967
|
});
|
|
1684
1968
|
|
|
1685
|
-
describe("
|
|
1686
|
-
it("
|
|
1969
|
+
describe("handleRunAttemptStarted()", () => {
|
|
1970
|
+
it("inserts a started attempt", async () => {
|
|
1687
1971
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1688
1972
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1689
1973
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
type: "run",
|
|
1697
|
-
parentStepId: null
|
|
1698
|
-
});
|
|
1699
|
-
expect(updatedStep).toMatchObject({
|
|
1700
|
-
state: "running",
|
|
1701
|
-
attemptCount: 1
|
|
1702
|
-
});
|
|
1703
|
-
});
|
|
1704
|
-
});
|
|
1705
|
-
|
|
1706
|
-
it("writes an 'attempt_started' step event when a run step is started", async () => {
|
|
1707
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1708
|
-
await runInDurableObject(stub, async (instance, state) => {
|
|
1709
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
1710
|
-
await context.getOrCreateStep(createRunStepId("step-1"), { type: "run", parentStepId: null });
|
|
1711
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
1712
|
-
type: "running",
|
|
1713
|
-
attemptCount: 1
|
|
1714
|
-
});
|
|
1715
|
-
expect(await instance.getStepEvents_experimental()).toMatchObject([
|
|
1716
|
-
{
|
|
1717
|
-
type: "attempt_started",
|
|
1718
|
-
stepId: "step-1",
|
|
1719
|
-
attemptNumber: 1,
|
|
1720
|
-
recordedAt: expect.any(Date)
|
|
1721
|
-
}
|
|
1722
|
-
]);
|
|
1974
|
+
context.getOrCreateRunStep(createRunStepId("step-1"), { parentStepId: null });
|
|
1975
|
+
const started = context.handleRunAttemptStarted(createRunStepId("step-1"));
|
|
1976
|
+
expect(started).toMatchObject({ state: "started", stepId: "step-1" });
|
|
1977
|
+
const step = context.getOrCreateRunStep(createRunStepId("step-1"), { parentStepId: null });
|
|
1978
|
+
expect(step.attempts).toHaveLength(1);
|
|
1979
|
+
expect(step.attempts[0]).toMatchObject({ state: "started" });
|
|
1723
1980
|
});
|
|
1724
1981
|
});
|
|
1725
1982
|
|
|
@@ -1727,148 +1984,55 @@ describe("WorkflowRuntime", () => {
|
|
|
1727
1984
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1728
1985
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1729
1986
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1730
|
-
|
|
1731
|
-
context.handleRunAttemptEvent(createRunStepId("nonexistent"), {
|
|
1732
|
-
type: "running",
|
|
1733
|
-
attemptCount: 1
|
|
1734
|
-
})
|
|
1735
|
-
).rejects.toThrow(/not found/);
|
|
1736
|
-
});
|
|
1737
|
-
});
|
|
1738
|
-
|
|
1739
|
-
it("throws when the step is not in 'pending' state", async () => {
|
|
1740
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1741
|
-
await runInDurableObject(stub, async (_instance, state) => {
|
|
1742
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
1743
|
-
await context.getOrCreateStep(createRunStepId("step-1"), { type: "run", parentStepId: null });
|
|
1744
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
1745
|
-
type: "running",
|
|
1746
|
-
attemptCount: 1
|
|
1747
|
-
});
|
|
1748
|
-
await expect(
|
|
1749
|
-
context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
1750
|
-
type: "running",
|
|
1751
|
-
attemptCount: 2
|
|
1752
|
-
})
|
|
1753
|
-
).rejects.toThrow(/Expected 'pending' but got running/);
|
|
1987
|
+
expect(() => context.handleRunAttemptStarted(createRunStepId("nonexistent"))).toThrow(/not found/);
|
|
1754
1988
|
});
|
|
1755
1989
|
});
|
|
1756
1990
|
|
|
1757
|
-
it("
|
|
1991
|
+
it("throws when an attempt is already in progress", async () => {
|
|
1758
1992
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1759
1993
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1760
1994
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
attemptCount: 1
|
|
1765
|
-
});
|
|
1766
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
1767
|
-
type: "failed",
|
|
1768
|
-
attemptCount: 1,
|
|
1769
|
-
errorMessage: "backoff"
|
|
1770
|
-
});
|
|
1771
|
-
const future = Date.now() + 3600_000;
|
|
1772
|
-
state.storage.sql.exec("UPDATE steps SET next_attempt_at = ? WHERE id = 'step-1'", future);
|
|
1773
|
-
await expect(
|
|
1774
|
-
context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
1775
|
-
type: "running",
|
|
1776
|
-
attemptCount: 2
|
|
1777
|
-
})
|
|
1778
|
-
).rejects.toThrow(/next attempt at/);
|
|
1779
|
-
});
|
|
1780
|
-
});
|
|
1781
|
-
|
|
1782
|
-
it("rejects when 'attemptCount' does not match the expected next attempt", async () => {
|
|
1783
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1784
|
-
await runInDurableObject(stub, async (_instance, state) => {
|
|
1785
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
1786
|
-
await context.getOrCreateStep(createRunStepId("step-1"), { type: "run", parentStepId: null });
|
|
1787
|
-
await expect(
|
|
1788
|
-
context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
1789
|
-
type: "running",
|
|
1790
|
-
attemptCount: 99
|
|
1791
|
-
})
|
|
1792
|
-
).rejects.toThrow(/Expected 98 but got 0/);
|
|
1995
|
+
context.getOrCreateRunStep(createRunStepId("step-1"), { parentStepId: null });
|
|
1996
|
+
context.handleRunAttemptStarted(createRunStepId("step-1"));
|
|
1997
|
+
expect(() => context.handleRunAttemptStarted(createRunStepId("step-1"))).toThrow(/already in progress/);
|
|
1793
1998
|
});
|
|
1794
1999
|
});
|
|
1795
2000
|
});
|
|
1796
2001
|
|
|
1797
|
-
describe("
|
|
1798
|
-
it("
|
|
2002
|
+
describe("handleRunAttemptSucceeded()", () => {
|
|
2003
|
+
it("marks the in-flight attempt succeeded with a json result", async () => {
|
|
1799
2004
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1800
2005
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1801
2006
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
attemptCount: 1,
|
|
1810
|
-
result: JSON.stringify({ value: 0 })
|
|
1811
|
-
});
|
|
1812
|
-
const updatedStep = await context.getOrCreateStep(createRunStepId("step-1"), {
|
|
1813
|
-
type: "run",
|
|
1814
|
-
parentStepId: null
|
|
2007
|
+
context.getOrCreateRunStep(createRunStepId("step-1"), { parentStepId: null });
|
|
2008
|
+
context.handleRunAttemptStarted(createRunStepId("step-1"));
|
|
2009
|
+
const done = context.handleRunAttemptSucceeded(createRunStepId("step-1"), JSON.stringify(0));
|
|
2010
|
+
expect(done).toMatchObject({
|
|
2011
|
+
state: "succeeded",
|
|
2012
|
+
resultType: "json",
|
|
2013
|
+
resultJson: JSON.stringify(0)
|
|
1815
2014
|
});
|
|
1816
|
-
|
|
2015
|
+
const step = context.getOrCreateRunStep(createRunStepId("step-1"), { parentStepId: null });
|
|
2016
|
+
expect(step.attempts).toHaveLength(1);
|
|
2017
|
+
expect(step.attempts[0]).toMatchObject({
|
|
1817
2018
|
state: "succeeded",
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
resolvedAt: expect.any(Date)
|
|
2019
|
+
resultType: "json",
|
|
2020
|
+
resultJson: JSON.stringify(0)
|
|
1821
2021
|
});
|
|
1822
2022
|
});
|
|
1823
2023
|
});
|
|
1824
2024
|
|
|
1825
|
-
it("
|
|
1826
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1827
|
-
await runInDurableObject(stub, async (instance, state) => {
|
|
1828
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
1829
|
-
await context.getOrCreateStep(createRunStepId("step-1"), { type: "run", parentStepId: null });
|
|
1830
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
1831
|
-
type: "running",
|
|
1832
|
-
attemptCount: 1
|
|
1833
|
-
});
|
|
1834
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
1835
|
-
type: "succeeded",
|
|
1836
|
-
attemptCount: 1,
|
|
1837
|
-
result: JSON.stringify({ value: 0 })
|
|
1838
|
-
});
|
|
1839
|
-
expect(await instance.getStepEvents_experimental()).toMatchObject([
|
|
1840
|
-
{
|
|
1841
|
-
type: "attempt_started",
|
|
1842
|
-
stepId: "step-1",
|
|
1843
|
-
attemptNumber: 1,
|
|
1844
|
-
recordedAt: expect.any(Date)
|
|
1845
|
-
},
|
|
1846
|
-
{
|
|
1847
|
-
type: "attempt_succeeded",
|
|
1848
|
-
stepId: "step-1",
|
|
1849
|
-
attemptNumber: 1,
|
|
1850
|
-
recordedAt: expect.any(Date)
|
|
1851
|
-
}
|
|
1852
|
-
]);
|
|
1853
|
-
});
|
|
1854
|
-
});
|
|
1855
|
-
|
|
1856
|
-
it("throws when the attempt number does not match", async () => {
|
|
2025
|
+
it("marks the in-flight attempt succeeded with result_type none", async () => {
|
|
1857
2026
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1858
2027
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1859
2028
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
2029
|
+
context.getOrCreateRunStep(createRunStepId("step-1"), { parentStepId: null });
|
|
2030
|
+
context.handleRunAttemptStarted(createRunStepId("step-1"));
|
|
2031
|
+
const done = context.handleRunAttemptSucceeded(createRunStepId("step-1"), null);
|
|
2032
|
+
expect(done).toMatchObject({
|
|
2033
|
+
state: "succeeded",
|
|
2034
|
+
resultType: "none"
|
|
1864
2035
|
});
|
|
1865
|
-
await expect(
|
|
1866
|
-
context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
1867
|
-
type: "succeeded",
|
|
1868
|
-
attemptCount: 999,
|
|
1869
|
-
result: JSON.stringify({ value: 0 })
|
|
1870
|
-
})
|
|
1871
|
-
).rejects.toThrow(/Unexpected attempt count/);
|
|
1872
2036
|
});
|
|
1873
2037
|
});
|
|
1874
2038
|
|
|
@@ -1876,246 +2040,69 @@ describe("WorkflowRuntime", () => {
|
|
|
1876
2040
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1877
2041
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1878
2042
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1879
|
-
|
|
1880
|
-
context.handleRunAttemptEvent(createRunStepId("nonexistent"), {
|
|
1881
|
-
type: "succeeded",
|
|
1882
|
-
attemptCount: 1,
|
|
1883
|
-
result: JSON.stringify({ value: 0 })
|
|
1884
|
-
})
|
|
1885
|
-
).rejects.toThrow(/not found/);
|
|
2043
|
+
expect(() => context.handleRunAttemptSucceeded(createRunStepId("nonexistent"), null)).toThrow(/not found/);
|
|
1886
2044
|
});
|
|
1887
2045
|
});
|
|
1888
2046
|
|
|
1889
|
-
it("
|
|
2047
|
+
it("throws when no attempt is in progress", async () => {
|
|
1890
2048
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1891
2049
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1892
2050
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
attemptCount: 1,
|
|
1898
|
-
result: JSON.stringify({ value: 1 })
|
|
1899
|
-
})
|
|
1900
|
-
).rejects.toThrow(/Expected 'running' but got pending/);
|
|
2051
|
+
context.getOrCreateRunStep(createRunStepId("step-1"), { parentStepId: null });
|
|
2052
|
+
expect(() => context.handleRunAttemptSucceeded(createRunStepId("step-1"), null)).toThrow(
|
|
2053
|
+
/No attempt in progress/
|
|
2054
|
+
);
|
|
1901
2055
|
});
|
|
1902
2056
|
});
|
|
1903
2057
|
});
|
|
1904
2058
|
|
|
1905
|
-
describe("
|
|
1906
|
-
it("
|
|
2059
|
+
describe("handleRunAttemptFailed()", () => {
|
|
2060
|
+
it("marks terminal failed when max attempts exhausted", async () => {
|
|
1907
2061
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1908
2062
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1909
2063
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
});
|
|
1915
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
1916
|
-
type: "running",
|
|
1917
|
-
attemptCount: 1
|
|
1918
|
-
});
|
|
1919
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
1920
|
-
type: "failed",
|
|
1921
|
-
attemptCount: 1,
|
|
1922
|
-
errorMessage: "error"
|
|
1923
|
-
});
|
|
1924
|
-
const updatedStep = await context.getOrCreateStep(createRunStepId("step-1"), {
|
|
1925
|
-
type: "run",
|
|
1926
|
-
maxAttempts: 1,
|
|
1927
|
-
parentStepId: null
|
|
1928
|
-
});
|
|
1929
|
-
expect(updatedStep).toMatchObject({
|
|
2064
|
+
context.getOrCreateRunStep(createRunStepId("step-1"), { maxAttempts: 1, parentStepId: null });
|
|
2065
|
+
context.handleRunAttemptStarted(createRunStepId("step-1"));
|
|
2066
|
+
const failed = context.handleRunAttemptFailed(createRunStepId("step-1"), { errorMessage: "error" });
|
|
2067
|
+
expect(failed).toMatchObject({
|
|
1930
2068
|
state: "failed",
|
|
1931
|
-
|
|
1932
|
-
|
|
2069
|
+
errorMessage: "error",
|
|
2070
|
+
nextAttemptAt: undefined
|
|
1933
2071
|
});
|
|
2072
|
+
const step = context.getOrCreateRunStep(createRunStepId("step-1"), { parentStepId: null });
|
|
2073
|
+
expect(step.attempts).toHaveLength(1);
|
|
2074
|
+
expect(step.attempts[0]).toMatchObject({ state: "failed", errorMessage: "error" });
|
|
1934
2075
|
});
|
|
1935
2076
|
});
|
|
1936
2077
|
|
|
1937
|
-
it("
|
|
1938
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1939
|
-
await runInDurableObject(stub, async (instance, state) => {
|
|
1940
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
1941
|
-
await context.getOrCreateStep(createRunStepId("step-1"), {
|
|
1942
|
-
type: "run",
|
|
1943
|
-
maxAttempts: 1,
|
|
1944
|
-
parentStepId: null
|
|
1945
|
-
});
|
|
1946
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
1947
|
-
type: "running",
|
|
1948
|
-
attemptCount: 1
|
|
1949
|
-
});
|
|
1950
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
1951
|
-
type: "failed",
|
|
1952
|
-
attemptCount: 1,
|
|
1953
|
-
errorMessage: "error"
|
|
1954
|
-
});
|
|
1955
|
-
expect(await instance.getStepEvents_experimental()).toMatchObject([
|
|
1956
|
-
{
|
|
1957
|
-
type: "attempt_started",
|
|
1958
|
-
stepId: "step-1",
|
|
1959
|
-
attemptNumber: 1,
|
|
1960
|
-
recordedAt: expect.any(Date)
|
|
1961
|
-
},
|
|
1962
|
-
{
|
|
1963
|
-
type: "attempt_failed",
|
|
1964
|
-
stepId: "step-1",
|
|
1965
|
-
attemptNumber: 1,
|
|
1966
|
-
errorMessage: "error",
|
|
1967
|
-
recordedAt: expect.any(Date)
|
|
1968
|
-
}
|
|
1969
|
-
]);
|
|
1970
|
-
});
|
|
1971
|
-
});
|
|
1972
|
-
|
|
1973
|
-
it("moves a run step back to 'pending' when retries are available", async () => {
|
|
2078
|
+
it("records next_attempt_at when retries remain", async () => {
|
|
1974
2079
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1975
2080
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1976
2081
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
const updatedStep = await context.getOrCreateStep(createRunStepId("step-1"), {
|
|
1988
|
-
type: "run",
|
|
1989
|
-
parentStepId: null
|
|
1990
|
-
});
|
|
1991
|
-
expect(updatedStep).toMatchObject({
|
|
1992
|
-
state: "pending",
|
|
1993
|
-
attemptCount: 1
|
|
1994
|
-
});
|
|
1995
|
-
expect(updatedStep).toHaveProperty("nextAttemptAt");
|
|
1996
|
-
expect((updatedStep as { nextAttemptAt: Date }).nextAttemptAt.getTime()).toBeGreaterThan(Date.now());
|
|
2082
|
+
const before = Date.now();
|
|
2083
|
+
context.getOrCreateRunStep(createRunStepId("step-1"), { parentStepId: null });
|
|
2084
|
+
context.handleRunAttemptStarted(createRunStepId("step-1"));
|
|
2085
|
+
const failed = context.handleRunAttemptFailed(createRunStepId("step-1"), { errorMessage: "transient" });
|
|
2086
|
+
const after = Date.now();
|
|
2087
|
+
expect(failed.state).toBe("failed");
|
|
2088
|
+
if (failed.state !== "failed") throw new Error("expected failed");
|
|
2089
|
+
expect(failed.nextAttemptAt).toBeDefined();
|
|
2090
|
+
expect(failed.nextAttemptAt!.getTime()).toBeGreaterThanOrEqual(before + 250);
|
|
2091
|
+
expect(failed.nextAttemptAt!.getTime()).toBeLessThanOrEqual(after + 500 + 100);
|
|
1997
2092
|
});
|
|
1998
2093
|
});
|
|
1999
2094
|
|
|
2000
|
-
it("
|
|
2095
|
+
it("marks terminal failed when isNonRetryableStepError is true", async () => {
|
|
2001
2096
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2002
2097
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2003
2098
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
});
|
|
2009
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
2010
|
-
type: "running",
|
|
2011
|
-
attemptCount: 1
|
|
2012
|
-
});
|
|
2013
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
2014
|
-
type: "failed",
|
|
2015
|
-
attemptCount: 1,
|
|
2016
|
-
errorMessage: "transient error",
|
|
2099
|
+
context.getOrCreateRunStep(createRunStepId("step-1"), { maxAttempts: 10, parentStepId: null });
|
|
2100
|
+
context.handleRunAttemptStarted(createRunStepId("step-1"));
|
|
2101
|
+
const failed = context.handleRunAttemptFailed(createRunStepId("step-1"), {
|
|
2102
|
+
errorMessage: "x",
|
|
2017
2103
|
isNonRetryableStepError: true
|
|
2018
2104
|
});
|
|
2019
|
-
|
|
2020
|
-
type: "run",
|
|
2021
|
-
parentStepId: null
|
|
2022
|
-
});
|
|
2023
|
-
expect(updatedStep).toMatchObject({
|
|
2024
|
-
state: "failed",
|
|
2025
|
-
attemptCount: 1,
|
|
2026
|
-
errorMessage: "transient error"
|
|
2027
|
-
});
|
|
2028
|
-
});
|
|
2029
|
-
});
|
|
2030
|
-
|
|
2031
|
-
it("schedules an alarm when retries are available", async () => {
|
|
2032
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2033
|
-
await runInDurableObject(stub, async (_instance, state) => {
|
|
2034
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2035
|
-
await context.getOrCreateStep(createRunStepId("step-1"), { type: "run", parentStepId: null });
|
|
2036
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
2037
|
-
type: "running",
|
|
2038
|
-
attemptCount: 1
|
|
2039
|
-
});
|
|
2040
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
2041
|
-
type: "failed",
|
|
2042
|
-
attemptCount: 1,
|
|
2043
|
-
errorMessage: "transient error"
|
|
2044
|
-
});
|
|
2045
|
-
const alarm = await state.storage.getAlarm();
|
|
2046
|
-
expect(alarm).not.toBeNull();
|
|
2047
|
-
expect(alarm).toBeGreaterThan(Date.now());
|
|
2048
|
-
});
|
|
2049
|
-
});
|
|
2050
|
-
|
|
2051
|
-
it("replaces an existing alarm when retries are available", async () => {
|
|
2052
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2053
|
-
await runInDurableObject(stub, async (_instance, state) => {
|
|
2054
|
-
await state.storage.setAlarm(Date.now() + 999_999);
|
|
2055
|
-
|
|
2056
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2057
|
-
await context.getOrCreateStep(createRunStepId("step-1"), { type: "run", parentStepId: null });
|
|
2058
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
2059
|
-
type: "running",
|
|
2060
|
-
attemptCount: 1
|
|
2061
|
-
});
|
|
2062
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
2063
|
-
type: "failed",
|
|
2064
|
-
attemptCount: 1,
|
|
2065
|
-
errorMessage: "transient error"
|
|
2066
|
-
});
|
|
2067
|
-
const alarm = await state.storage.getAlarm();
|
|
2068
|
-
expect(alarm).not.toBeNull();
|
|
2069
|
-
expect(alarm).toBeLessThan(Date.now() + 999_999);
|
|
2070
|
-
});
|
|
2071
|
-
});
|
|
2072
|
-
|
|
2073
|
-
it("writes an 'attempt_failed' step event when retrying", async () => {
|
|
2074
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2075
|
-
await runInDurableObject(stub, async (instance, state) => {
|
|
2076
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2077
|
-
await context.getOrCreateStep(createRunStepId("step-1"), { type: "run", parentStepId: null });
|
|
2078
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
2079
|
-
type: "running",
|
|
2080
|
-
attemptCount: 1
|
|
2081
|
-
});
|
|
2082
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
2083
|
-
type: "failed",
|
|
2084
|
-
attemptCount: 1,
|
|
2085
|
-
errorMessage: "transient error"
|
|
2086
|
-
});
|
|
2087
|
-
expect(await instance.getStepEvents_experimental()).toMatchObject([
|
|
2088
|
-
{
|
|
2089
|
-
type: "attempt_started",
|
|
2090
|
-
stepId: "step-1",
|
|
2091
|
-
attemptNumber: 1
|
|
2092
|
-
},
|
|
2093
|
-
{
|
|
2094
|
-
type: "attempt_failed",
|
|
2095
|
-
stepId: "step-1",
|
|
2096
|
-
attemptNumber: 1,
|
|
2097
|
-
errorMessage: "transient error"
|
|
2098
|
-
}
|
|
2099
|
-
]);
|
|
2100
|
-
});
|
|
2101
|
-
});
|
|
2102
|
-
|
|
2103
|
-
it("throws when the attempt number does not match", async () => {
|
|
2104
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2105
|
-
await runInDurableObject(stub, async (_instance, state) => {
|
|
2106
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2107
|
-
await context.getOrCreateStep(createRunStepId("step-1"), { type: "run", parentStepId: null });
|
|
2108
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
2109
|
-
type: "running",
|
|
2110
|
-
attemptCount: 1
|
|
2111
|
-
});
|
|
2112
|
-
await expect(
|
|
2113
|
-
context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
2114
|
-
type: "failed",
|
|
2115
|
-
attemptCount: 999,
|
|
2116
|
-
errorMessage: "error"
|
|
2117
|
-
})
|
|
2118
|
-
).rejects.toThrow(/Unexpected attempt count/);
|
|
2105
|
+
expect(failed).toMatchObject({ state: "failed", nextAttemptAt: undefined });
|
|
2119
2106
|
});
|
|
2120
2107
|
});
|
|
2121
2108
|
|
|
@@ -2123,80 +2110,41 @@ describe("WorkflowRuntime", () => {
|
|
|
2123
2110
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2124
2111
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2125
2112
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
attemptCount: 1,
|
|
2130
|
-
errorMessage: "error"
|
|
2131
|
-
})
|
|
2132
|
-
).rejects.toThrow(/not found/);
|
|
2133
|
-
});
|
|
2134
|
-
});
|
|
2135
|
-
|
|
2136
|
-
it("uses backoff delay for next attempt when retrying", async () => {
|
|
2137
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2138
|
-
await runInDurableObject(stub, async (_instance, state) => {
|
|
2139
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2140
|
-
const before = Date.now();
|
|
2141
|
-
await context.getOrCreateStep(createRunStepId("step-1"), { type: "run", parentStepId: null });
|
|
2142
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
2143
|
-
type: "running",
|
|
2144
|
-
attemptCount: 1
|
|
2145
|
-
});
|
|
2146
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
2147
|
-
type: "failed",
|
|
2148
|
-
attemptCount: 1,
|
|
2149
|
-
errorMessage: "transient error"
|
|
2150
|
-
});
|
|
2151
|
-
const after = Date.now();
|
|
2152
|
-
const updatedStep = await context.getOrCreateStep(createRunStepId("step-1"), {
|
|
2153
|
-
type: "run",
|
|
2154
|
-
parentStepId: null
|
|
2155
|
-
});
|
|
2156
|
-
expect(updatedStep).toMatchObject({
|
|
2157
|
-
state: "pending",
|
|
2158
|
-
attemptCount: 1
|
|
2159
|
-
});
|
|
2160
|
-
const nextAttemptAt = (updatedStep as { nextAttemptAt: Date }).nextAttemptAt.getTime();
|
|
2161
|
-
expect(nextAttemptAt).toBeGreaterThanOrEqual(before + 250);
|
|
2162
|
-
expect(nextAttemptAt).toBeLessThanOrEqual(after + 500 + 100);
|
|
2113
|
+
expect(() => context.handleRunAttemptFailed(createRunStepId("nonexistent"), { errorMessage: "e" })).toThrow(
|
|
2114
|
+
/not found/
|
|
2115
|
+
);
|
|
2163
2116
|
});
|
|
2164
2117
|
});
|
|
2165
2118
|
|
|
2166
|
-
it("
|
|
2119
|
+
it("throws when no attempt is in progress", async () => {
|
|
2167
2120
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2168
2121
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2169
2122
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
attemptCount: 1,
|
|
2175
|
-
errorMessage: "bad"
|
|
2176
|
-
})
|
|
2177
|
-
).rejects.toThrow(/Expected 'running' but got pending/);
|
|
2123
|
+
context.getOrCreateRunStep(createRunStepId("step-1"), { parentStepId: null });
|
|
2124
|
+
expect(() => context.handleRunAttemptFailed(createRunStepId("step-1"), { errorMessage: "bad" })).toThrow(
|
|
2125
|
+
/No attempt in progress/
|
|
2126
|
+
);
|
|
2178
2127
|
});
|
|
2179
2128
|
});
|
|
2180
2129
|
});
|
|
2181
2130
|
});
|
|
2182
2131
|
|
|
2183
2132
|
describe("sleep steps", () => {
|
|
2184
|
-
describe("
|
|
2133
|
+
describe("getOrCreateSleepStep()", () => {
|
|
2185
2134
|
it("creates a sleep step", async () => {
|
|
2186
2135
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2187
2136
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2188
2137
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2189
2138
|
const wakeAt = new Date(Date.now() + 60_000);
|
|
2190
|
-
const step =
|
|
2191
|
-
|
|
2192
|
-
wakeAt: wakeAt,
|
|
2139
|
+
const step = context.getOrCreateSleepStep(createSleepStepId("sleep-1"), {
|
|
2140
|
+
wakeAt,
|
|
2193
2141
|
parentStepId: null
|
|
2194
2142
|
});
|
|
2195
2143
|
expect(step).toMatchObject({
|
|
2196
2144
|
id: "sleep-1",
|
|
2197
2145
|
type: "sleep",
|
|
2198
2146
|
state: "waiting",
|
|
2199
|
-
wakeAt
|
|
2147
|
+
wakeAt
|
|
2200
2148
|
});
|
|
2201
2149
|
});
|
|
2202
2150
|
});
|
|
@@ -2205,13 +2153,9 @@ describe("WorkflowRuntime", () => {
|
|
|
2205
2153
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2206
2154
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2207
2155
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2208
|
-
const
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
parentStepId: null
|
|
2212
|
-
});
|
|
2213
|
-
const second = await context.getOrCreateStep(createSleepStepId("sleep-1"), {
|
|
2214
|
-
type: "sleep",
|
|
2156
|
+
const w = new Date();
|
|
2157
|
+
const first = context.getOrCreateSleepStep(createSleepStepId("sleep-1"), { wakeAt: w, parentStepId: null });
|
|
2158
|
+
const second = context.getOrCreateSleepStep(createSleepStepId("sleep-1"), {
|
|
2215
2159
|
wakeAt: new Date(),
|
|
2216
2160
|
parentStepId: null
|
|
2217
2161
|
});
|
|
@@ -2219,178 +2163,75 @@ describe("WorkflowRuntime", () => {
|
|
|
2219
2163
|
});
|
|
2220
2164
|
});
|
|
2221
2165
|
|
|
2222
|
-
it("
|
|
2223
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2224
|
-
await runInDurableObject(stub, async (instance, state) => {
|
|
2225
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2226
|
-
const wakeAt = new Date();
|
|
2227
|
-
await context.getOrCreateStep(createSleepStepId("sleep-1"), {
|
|
2228
|
-
type: "sleep",
|
|
2229
|
-
wakeAt: wakeAt,
|
|
2230
|
-
parentStepId: null
|
|
2231
|
-
});
|
|
2232
|
-
expect(await instance.getStepEvents_experimental()).toMatchObject([
|
|
2233
|
-
{
|
|
2234
|
-
type: "sleep_waiting",
|
|
2235
|
-
stepId: "sleep-1",
|
|
2236
|
-
wakeAt: wakeAt,
|
|
2237
|
-
recordedAt: expect.any(Date)
|
|
2238
|
-
}
|
|
2239
|
-
]);
|
|
2240
|
-
});
|
|
2241
|
-
});
|
|
2242
|
-
|
|
2243
|
-
it("schedules an alarm", async () => {
|
|
2166
|
+
it("does not set a durable object alarm by itself", async () => {
|
|
2244
2167
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2245
2168
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2246
2169
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2247
2170
|
const wakeAt = new Date(Date.now() + 60_000);
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
wakeAt: wakeAt,
|
|
2251
|
-
parentStepId: null
|
|
2252
|
-
});
|
|
2253
|
-
expect(await state.storage.getAlarm()).toBe(wakeAt.getTime());
|
|
2171
|
+
context.getOrCreateSleepStep(createSleepStepId("sleep-1"), { wakeAt, parentStepId: null });
|
|
2172
|
+
expect(await state.storage.getAlarm()).toBeNull();
|
|
2254
2173
|
});
|
|
2255
2174
|
});
|
|
2256
2175
|
|
|
2257
|
-
it("
|
|
2176
|
+
it("leaves an existing alarm unchanged when creating a sleep step", async () => {
|
|
2258
2177
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2259
2178
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2260
|
-
|
|
2261
|
-
|
|
2179
|
+
const prior = Date.now() + 999_999;
|
|
2180
|
+
await state.storage.setAlarm(prior);
|
|
2262
2181
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
type: "sleep",
|
|
2266
|
-
wakeAt: wakeAt,
|
|
2182
|
+
context.getOrCreateSleepStep(createSleepStepId("sleep-1"), {
|
|
2183
|
+
wakeAt: new Date(Date.now() + 60_000),
|
|
2267
2184
|
parentStepId: null
|
|
2268
2185
|
});
|
|
2269
|
-
expect(await state.storage.getAlarm()).toBe(
|
|
2186
|
+
expect(await state.storage.getAlarm()).toBe(prior);
|
|
2270
2187
|
});
|
|
2271
2188
|
});
|
|
2272
2189
|
|
|
2273
|
-
it("does not
|
|
2190
|
+
it("does not set an alarm when re-reading an existing sleep step after deleteAlarm", async () => {
|
|
2274
2191
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2275
2192
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2276
2193
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2277
2194
|
const pastWakeAt = new Date(Date.now() - 10_000);
|
|
2278
|
-
|
|
2279
|
-
type: "sleep",
|
|
2195
|
+
context.getOrCreateSleepStep(createSleepStepId("sleep-1"), {
|
|
2280
2196
|
wakeAt: pastWakeAt,
|
|
2281
2197
|
parentStepId: null
|
|
2282
2198
|
});
|
|
2283
2199
|
await state.storage.deleteAlarm();
|
|
2284
|
-
|
|
2285
|
-
await context.getOrCreateStep(createSleepStepId("sleep-1"), {
|
|
2286
|
-
type: "sleep",
|
|
2200
|
+
context.getOrCreateSleepStep(createSleepStepId("sleep-1"), {
|
|
2287
2201
|
wakeAt: pastWakeAt,
|
|
2288
2202
|
parentStepId: null
|
|
2289
2203
|
});
|
|
2290
2204
|
expect(await state.storage.getAlarm()).toBeNull();
|
|
2291
2205
|
});
|
|
2292
2206
|
});
|
|
2293
|
-
|
|
2294
|
-
it("does not append a second sleep_waiting step_events row when the sleep step already exists", async () => {
|
|
2295
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2296
|
-
await runInDurableObject(stub, async (instance, state) => {
|
|
2297
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2298
|
-
const wakeAt = new Date(Date.now() + 30_000);
|
|
2299
|
-
await context.getOrCreateStep(createSleepStepId("sleep-1"), { type: "sleep", wakeAt, parentStepId: null });
|
|
2300
|
-
await context.getOrCreateStep(createSleepStepId("sleep-1"), { type: "sleep", wakeAt, parentStepId: null });
|
|
2301
|
-
const events = await instance.getStepEvents_experimental();
|
|
2302
|
-
expect(events).toHaveLength(1);
|
|
2303
|
-
expect(events[0]).toMatchObject({
|
|
2304
|
-
type: "sleep_waiting",
|
|
2305
|
-
stepId: "sleep-1",
|
|
2306
|
-
wakeAt: wakeAt,
|
|
2307
|
-
recordedAt: expect.any(Date)
|
|
2308
|
-
});
|
|
2309
|
-
});
|
|
2310
|
-
});
|
|
2311
|
-
|
|
2312
|
-
it("does not set an alarm when re-reading an already elapsed sleep step", async () => {
|
|
2313
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2314
|
-
await runInDurableObject(stub, async (_instance, state) => {
|
|
2315
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2316
|
-
await context.getOrCreateStep(createSleepStepId("sleep-1"), {
|
|
2317
|
-
type: "sleep",
|
|
2318
|
-
wakeAt: new Date(Date.now() + 60_000),
|
|
2319
|
-
parentStepId: null
|
|
2320
|
-
});
|
|
2321
|
-
await state.storage.deleteAlarm();
|
|
2322
|
-
const now = Date.now();
|
|
2323
|
-
state.storage.sql.exec(
|
|
2324
|
-
"UPDATE steps SET state = 'elapsed', wake_at = NULL, resolved_at = ? WHERE id = 'sleep-1'",
|
|
2325
|
-
now
|
|
2326
|
-
);
|
|
2327
|
-
await context.getOrCreateStep(createSleepStepId("sleep-1"), {
|
|
2328
|
-
type: "sleep",
|
|
2329
|
-
wakeAt: new Date(),
|
|
2330
|
-
parentStepId: null
|
|
2331
|
-
});
|
|
2332
|
-
expect(await state.storage.getAlarm()).toBeNull();
|
|
2333
|
-
});
|
|
2334
|
-
});
|
|
2335
2207
|
});
|
|
2336
2208
|
|
|
2337
|
-
describe("
|
|
2209
|
+
describe("handleSleepStepElapsed()", () => {
|
|
2338
2210
|
it("moves a sleep step from 'waiting' to 'elapsed'", async () => {
|
|
2339
2211
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2340
2212
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2341
2213
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2342
|
-
|
|
2343
|
-
type: "sleep",
|
|
2214
|
+
context.getOrCreateSleepStep(createSleepStepId("sleep-1"), {
|
|
2344
2215
|
wakeAt: new Date(),
|
|
2345
2216
|
parentStepId: null
|
|
2346
2217
|
});
|
|
2347
|
-
context.
|
|
2348
|
-
const
|
|
2349
|
-
type: "sleep",
|
|
2218
|
+
context.handleSleepStepElapsed(createSleepStepId("sleep-1"));
|
|
2219
|
+
const step = context.getOrCreateSleepStep(createSleepStepId("sleep-1"), {
|
|
2350
2220
|
wakeAt: new Date(),
|
|
2351
2221
|
parentStepId: null
|
|
2352
2222
|
});
|
|
2353
|
-
expect(
|
|
2223
|
+
expect(step).toMatchObject({
|
|
2354
2224
|
state: "elapsed",
|
|
2355
2225
|
resolvedAt: expect.any(Date)
|
|
2356
2226
|
});
|
|
2357
2227
|
});
|
|
2358
2228
|
});
|
|
2359
2229
|
|
|
2360
|
-
it("writes a 'sleep_elapsed' step event when a sleep step is elapsed", async () => {
|
|
2361
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2362
|
-
await runInDurableObject(stub, async (instance, state) => {
|
|
2363
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2364
|
-
const wakeAt = new Date(Date.now() + 60_000);
|
|
2365
|
-
await context.getOrCreateStep(createSleepStepId("sleep-1"), {
|
|
2366
|
-
type: "sleep",
|
|
2367
|
-
wakeAt: wakeAt,
|
|
2368
|
-
parentStepId: null
|
|
2369
|
-
});
|
|
2370
|
-
context.handleSleepStepEvent(createSleepStepId("sleep-1"), { type: "elapsed" });
|
|
2371
|
-
expect(await instance.getStepEvents_experimental()).toMatchObject([
|
|
2372
|
-
{
|
|
2373
|
-
type: "sleep_waiting",
|
|
2374
|
-
stepId: "sleep-1",
|
|
2375
|
-
wakeAt: wakeAt,
|
|
2376
|
-
recordedAt: expect.any(Date)
|
|
2377
|
-
},
|
|
2378
|
-
{
|
|
2379
|
-
type: "sleep_elapsed",
|
|
2380
|
-
stepId: "sleep-1",
|
|
2381
|
-
recordedAt: expect.any(Date)
|
|
2382
|
-
}
|
|
2383
|
-
]);
|
|
2384
|
-
});
|
|
2385
|
-
});
|
|
2386
|
-
|
|
2387
2230
|
it("throws when the step does not exist", async () => {
|
|
2388
2231
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2389
2232
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2390
2233
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2391
|
-
expect(() => context.
|
|
2392
|
-
/not found/
|
|
2393
|
-
);
|
|
2234
|
+
expect(() => context.handleSleepStepElapsed(createSleepStepId("nonexistent"))).toThrow(/not found/);
|
|
2394
2235
|
});
|
|
2395
2236
|
});
|
|
2396
2237
|
|
|
@@ -2398,13 +2239,12 @@ describe("WorkflowRuntime", () => {
|
|
|
2398
2239
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2399
2240
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2400
2241
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
wakeAt: new Date(),
|
|
2242
|
+
context.getOrCreateSleepStep(createSleepStepId("sleep-1"), {
|
|
2243
|
+
wakeAt: new Date(Date.now() + 60_000),
|
|
2404
2244
|
parentStepId: null
|
|
2405
2245
|
});
|
|
2406
|
-
context.
|
|
2407
|
-
expect(() => context.
|
|
2246
|
+
context.handleSleepStepElapsed(createSleepStepId("sleep-1"));
|
|
2247
|
+
expect(() => context.handleSleepStepElapsed(createSleepStepId("sleep-1"))).toThrow(
|
|
2408
2248
|
/Expected 'waiting' but got elapsed/
|
|
2409
2249
|
);
|
|
2410
2250
|
});
|
|
@@ -2413,13 +2253,12 @@ describe("WorkflowRuntime", () => {
|
|
|
2413
2253
|
});
|
|
2414
2254
|
|
|
2415
2255
|
describe("wait steps", () => {
|
|
2416
|
-
describe("
|
|
2256
|
+
describe("getOrCreateWaitStep()", () => {
|
|
2417
2257
|
it("creates a wait step when no timeout is provided", async () => {
|
|
2418
2258
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2419
2259
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2420
2260
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2421
|
-
const step =
|
|
2422
|
-
type: "wait",
|
|
2261
|
+
const step = context.getOrCreateWaitStep(createWaitStepId("wait-1"), {
|
|
2423
2262
|
eventName: "event-1",
|
|
2424
2263
|
parentStepId: null
|
|
2425
2264
|
});
|
|
@@ -2437,371 +2276,131 @@ describe("WorkflowRuntime", () => {
|
|
|
2437
2276
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2438
2277
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2439
2278
|
const timeoutAt = new Date(Date.now() + 60_000);
|
|
2440
|
-
const step =
|
|
2441
|
-
type: "wait",
|
|
2279
|
+
const step = context.getOrCreateWaitStep(createWaitStepId("wait-1"), {
|
|
2442
2280
|
eventName: "event-1",
|
|
2443
2281
|
parentStepId: null,
|
|
2444
|
-
timeoutAt
|
|
2282
|
+
timeoutAt
|
|
2445
2283
|
});
|
|
2446
2284
|
expect(step).toMatchObject({
|
|
2447
2285
|
id: "wait-1",
|
|
2448
2286
|
type: "wait",
|
|
2449
2287
|
state: "waiting",
|
|
2450
2288
|
eventName: "event-1",
|
|
2451
|
-
timeoutAt
|
|
2289
|
+
timeoutAt
|
|
2452
2290
|
});
|
|
2453
2291
|
});
|
|
2454
2292
|
});
|
|
2455
2293
|
|
|
2456
|
-
it("writes a 'wait_waiting' step event", async () => {
|
|
2457
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2458
|
-
await runInDurableObject(stub, async (instance, state) => {
|
|
2459
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2460
|
-
const timeoutAt = new Date(Date.now() + 60_000);
|
|
2461
|
-
await context.getOrCreateStep(createWaitStepId("wait-1"), {
|
|
2462
|
-
type: "wait",
|
|
2463
|
-
eventName: "event-1",
|
|
2464
|
-
parentStepId: null,
|
|
2465
|
-
timeoutAt: timeoutAt
|
|
2466
|
-
});
|
|
2467
|
-
expect(await instance.getStepEvents_experimental()).toMatchObject([
|
|
2468
|
-
{
|
|
2469
|
-
type: "wait_waiting",
|
|
2470
|
-
stepId: "wait-1",
|
|
2471
|
-
eventName: "event-1",
|
|
2472
|
-
timeoutAt: timeoutAt,
|
|
2473
|
-
recordedAt: expect.any(Date)
|
|
2474
|
-
}
|
|
2475
|
-
]);
|
|
2476
|
-
});
|
|
2477
|
-
});
|
|
2478
|
-
|
|
2479
2294
|
it("creates a wait step once and returns the same durable row on subsequent reads", async () => {
|
|
2480
2295
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2481
2296
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2482
2297
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2483
|
-
const first =
|
|
2484
|
-
type: "wait",
|
|
2298
|
+
const first = context.getOrCreateWaitStep(createWaitStepId("wait-1"), {
|
|
2485
2299
|
eventName: "event-1",
|
|
2486
|
-
parentStepId: null
|
|
2487
|
-
timeoutAt: new Date(Date.now() + 60_000)
|
|
2300
|
+
parentStepId: null
|
|
2488
2301
|
});
|
|
2489
|
-
const second =
|
|
2490
|
-
type: "wait",
|
|
2302
|
+
const second = context.getOrCreateWaitStep(createWaitStepId("wait-1"), {
|
|
2491
2303
|
eventName: "event-1",
|
|
2492
|
-
parentStepId: null
|
|
2493
|
-
timeoutAt: new Date(Date.now() + 60_000)
|
|
2304
|
+
parentStepId: null
|
|
2494
2305
|
});
|
|
2495
2306
|
expect(first).toEqual(second);
|
|
2496
2307
|
});
|
|
2497
2308
|
});
|
|
2498
2309
|
|
|
2499
|
-
it("
|
|
2500
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2501
|
-
await runInDurableObject(stub, async (_instance, state) => {
|
|
2502
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2503
|
-
const timeoutAt = new Date(Date.now() + 60_000);
|
|
2504
|
-
await context.getOrCreateStep(createWaitStepId("wait-1"), {
|
|
2505
|
-
type: "wait",
|
|
2506
|
-
eventName: "event-1",
|
|
2507
|
-
parentStepId: null,
|
|
2508
|
-
timeoutAt: timeoutAt
|
|
2509
|
-
});
|
|
2510
|
-
expect(await state.storage.getAlarm()).toBe(timeoutAt.getTime());
|
|
2511
|
-
});
|
|
2512
|
-
});
|
|
2513
|
-
|
|
2514
|
-
it("replaces any existing alarm with a new alarm when a timeout is provided", async () => {
|
|
2310
|
+
it("does not set a durable object alarm by itself", async () => {
|
|
2515
2311
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2516
2312
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2517
|
-
await state.storage.setAlarm(Date.now() + 999_999);
|
|
2518
|
-
|
|
2519
2313
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2520
|
-
|
|
2521
|
-
await context.getOrCreateStep(createWaitStepId("wait-1"), {
|
|
2522
|
-
type: "wait",
|
|
2314
|
+
context.getOrCreateWaitStep(createWaitStepId("wait-1"), {
|
|
2523
2315
|
eventName: "event-1",
|
|
2524
2316
|
parentStepId: null,
|
|
2525
|
-
timeoutAt:
|
|
2526
|
-
});
|
|
2527
|
-
expect(await state.storage.getAlarm()).toBe(timeoutAt.getTime());
|
|
2528
|
-
});
|
|
2529
|
-
});
|
|
2530
|
-
|
|
2531
|
-
it("doesn't schedule an alarm when no timeout is provided", async () => {
|
|
2532
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2533
|
-
await runInDurableObject(stub, async (_instance, state) => {
|
|
2534
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2535
|
-
await context.getOrCreateStep(createWaitStepId("wait-1"), {
|
|
2536
|
-
type: "wait",
|
|
2537
|
-
eventName: "event-1",
|
|
2538
|
-
parentStepId: null
|
|
2317
|
+
timeoutAt: new Date(Date.now() + 60_000)
|
|
2539
2318
|
});
|
|
2540
2319
|
expect(await state.storage.getAlarm()).toBeNull();
|
|
2541
2320
|
});
|
|
2542
2321
|
});
|
|
2543
2322
|
|
|
2544
|
-
it("
|
|
2323
|
+
it("leaves an existing alarm unchanged when creating a wait step with timeout", async () => {
|
|
2545
2324
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2546
2325
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2326
|
+
const prior = Date.now() + 999_999;
|
|
2327
|
+
await state.storage.setAlarm(prior);
|
|
2547
2328
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2548
|
-
|
|
2549
|
-
await context.getOrCreateStep(createWaitStepId("wait-1"), {
|
|
2550
|
-
type: "wait",
|
|
2329
|
+
context.getOrCreateWaitStep(createWaitStepId("wait-1"), {
|
|
2551
2330
|
eventName: "event-1",
|
|
2552
2331
|
parentStepId: null,
|
|
2553
|
-
timeoutAt:
|
|
2554
|
-
});
|
|
2555
|
-
await state.storage.deleteAlarm();
|
|
2556
|
-
|
|
2557
|
-
const shiftedTimeout = new Date(Date.now() + 120_000);
|
|
2558
|
-
await context.getOrCreateStep(createWaitStepId("wait-1"), {
|
|
2559
|
-
type: "wait",
|
|
2560
|
-
eventName: "event-1",
|
|
2561
|
-
parentStepId: null,
|
|
2562
|
-
timeoutAt: shiftedTimeout
|
|
2332
|
+
timeoutAt: new Date(Date.now() + 60_000)
|
|
2563
2333
|
});
|
|
2564
|
-
expect(await state.storage.getAlarm()).toBe(
|
|
2334
|
+
expect(await state.storage.getAlarm()).toBe(prior);
|
|
2565
2335
|
});
|
|
2566
2336
|
});
|
|
2567
2337
|
|
|
2568
|
-
it("
|
|
2338
|
+
it("satisfies from a queued inbound event when creating the wait step", async () => {
|
|
2569
2339
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2570
2340
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2571
2341
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
});
|
|
2579
|
-
await state.storage.deleteAlarm();
|
|
2580
|
-
|
|
2581
|
-
await context.getOrCreateStep(createWaitStepId("wait-1"), {
|
|
2582
|
-
type: "wait",
|
|
2583
|
-
eventName: "event-1",
|
|
2584
|
-
parentStepId: null,
|
|
2585
|
-
timeoutAt: pastTimeout
|
|
2586
|
-
});
|
|
2587
|
-
expect(await state.storage.getAlarm()).toBeNull();
|
|
2588
|
-
});
|
|
2589
|
-
});
|
|
2590
|
-
});
|
|
2591
|
-
|
|
2592
|
-
describe("handleInboundEvent()", () => {
|
|
2593
|
-
it("moves a wait step from 'waiting' to 'satisfied' when event is delivered", async () => {
|
|
2594
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2595
|
-
await runInDurableObject(stub, async (instance, state) => {
|
|
2596
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2597
|
-
await context.getOrCreateStep(createWaitStepId("wait-1"), {
|
|
2598
|
-
type: "wait",
|
|
2599
|
-
eventName: "event-1",
|
|
2600
|
-
parentStepId: null
|
|
2601
|
-
});
|
|
2602
|
-
await instance.handleInboundEvent("event-1", "payload");
|
|
2603
|
-
const updatedStep = await context.getOrCreateStep(createWaitStepId("wait-1"), {
|
|
2604
|
-
type: "wait",
|
|
2605
|
-
eventName: "event-1",
|
|
2606
|
-
parentStepId: null
|
|
2607
|
-
});
|
|
2608
|
-
expect(updatedStep).toMatchObject({
|
|
2609
|
-
state: "satisfied",
|
|
2610
|
-
payload: JSON.stringify("payload"),
|
|
2611
|
-
resolvedAt: expect.any(Date)
|
|
2612
|
-
});
|
|
2613
|
-
});
|
|
2614
|
-
});
|
|
2615
|
-
|
|
2616
|
-
it("writes a 'wait_satisfied' step event when a wait step is satisfied", async () => {
|
|
2617
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2618
|
-
await runInDurableObject(stub, async (instance, state) => {
|
|
2619
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2620
|
-
await context.getOrCreateStep(createWaitStepId("wait-1"), {
|
|
2621
|
-
type: "wait",
|
|
2622
|
-
eventName: "event-1",
|
|
2623
|
-
parentStepId: null
|
|
2624
|
-
});
|
|
2625
|
-
await instance.handleInboundEvent("event-1", "payload");
|
|
2626
|
-
expect(await instance.getStepEvents_experimental()).toMatchObject([
|
|
2627
|
-
{
|
|
2628
|
-
type: "wait_waiting",
|
|
2629
|
-
stepId: "wait-1",
|
|
2630
|
-
eventName: "event-1",
|
|
2631
|
-
recordedAt: expect.any(Date)
|
|
2632
|
-
},
|
|
2633
|
-
{
|
|
2634
|
-
type: "wait_satisfied",
|
|
2635
|
-
stepId: "wait-1",
|
|
2636
|
-
payload: JSON.stringify("payload"),
|
|
2637
|
-
recordedAt: expect.any(Date)
|
|
2638
|
-
}
|
|
2639
|
-
]);
|
|
2640
|
-
});
|
|
2641
|
-
});
|
|
2642
|
-
|
|
2643
|
-
it("queues the event when no matching wait step exists", async () => {
|
|
2644
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2645
|
-
await runInDurableObject(stub, async (instance, state) => {
|
|
2646
|
-
await instance.handleInboundEvent("event-1", "queued-payload");
|
|
2647
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2648
|
-
const step = await context.getOrCreateStep(createWaitStepId("wait-1"), {
|
|
2649
|
-
type: "wait",
|
|
2342
|
+
state.storage.sql.exec(
|
|
2343
|
+
`INSERT INTO inbound_events (event_name, payload) VALUES (?, ?)`,
|
|
2344
|
+
"event-1",
|
|
2345
|
+
JSON.stringify({ v: 1 })
|
|
2346
|
+
);
|
|
2347
|
+
const step = context.getOrCreateWaitStep(createWaitStepId("wait-1"), {
|
|
2650
2348
|
eventName: "event-1",
|
|
2651
2349
|
parentStepId: null
|
|
2652
2350
|
});
|
|
2653
2351
|
expect(step).toMatchObject({
|
|
2654
2352
|
state: "satisfied",
|
|
2655
|
-
payload:
|
|
2656
|
-
});
|
|
2657
|
-
});
|
|
2658
|
-
});
|
|
2659
|
-
|
|
2660
|
-
it("consumes queued inbound events in FIFO order when several waits are created", async () => {
|
|
2661
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2662
|
-
await runInDurableObject(stub, async (instance, state) => {
|
|
2663
|
-
await instance.handleInboundEvent("event-1", "first");
|
|
2664
|
-
// Distinct `created_at` so FIFO ordering does not depend on random `inbound_events.id` when timestamps tie.
|
|
2665
|
-
await new Promise((r) => setTimeout(r, 2));
|
|
2666
|
-
await instance.handleInboundEvent("event-1", "second");
|
|
2667
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2668
|
-
const step1 = await context.getOrCreateStep(createWaitStepId("wait-1"), {
|
|
2669
|
-
type: "wait",
|
|
2670
|
-
eventName: "event-1",
|
|
2671
|
-
parentStepId: null
|
|
2672
|
-
});
|
|
2673
|
-
const step2 = await context.getOrCreateStep(createWaitStepId("wait-2"), {
|
|
2674
|
-
type: "wait",
|
|
2675
|
-
eventName: "event-1",
|
|
2676
|
-
parentStepId: null
|
|
2677
|
-
});
|
|
2678
|
-
expect(step1).toMatchObject({
|
|
2679
|
-
state: "satisfied",
|
|
2680
|
-
payload: JSON.stringify("first")
|
|
2681
|
-
});
|
|
2682
|
-
expect(step2).toMatchObject({
|
|
2683
|
-
state: "satisfied",
|
|
2684
|
-
payload: JSON.stringify("second")
|
|
2353
|
+
payload: { v: 1 }
|
|
2685
2354
|
});
|
|
2686
2355
|
});
|
|
2687
2356
|
});
|
|
2688
2357
|
|
|
2689
|
-
it("
|
|
2358
|
+
it("rejects a second inbound_events row with the same claimed_by", async () => {
|
|
2690
2359
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2691
|
-
await runInDurableObject(stub, async (
|
|
2360
|
+
await runInDurableObject(stub, async (_instance, state) => {
|
|
2692
2361
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2693
|
-
|
|
2694
|
-
type: "wait",
|
|
2695
|
-
eventName: "event-1",
|
|
2696
|
-
parentStepId: null
|
|
2697
|
-
});
|
|
2698
|
-
await context.getOrCreateStep(createWaitStepId("wait-2"), {
|
|
2699
|
-
type: "wait",
|
|
2700
|
-
eventName: "event-1",
|
|
2701
|
-
parentStepId: null
|
|
2702
|
-
});
|
|
2703
|
-
await instance.handleInboundEvent("event-1", "payload");
|
|
2704
|
-
const step1 = await context.getOrCreateStep(createWaitStepId("wait-1"), {
|
|
2705
|
-
type: "wait",
|
|
2362
|
+
context.getOrCreateWaitStep(createWaitStepId("wait-1"), {
|
|
2706
2363
|
eventName: "event-1",
|
|
2707
2364
|
parentStepId: null
|
|
2708
2365
|
});
|
|
2709
|
-
const
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2366
|
+
const t = Date.now();
|
|
2367
|
+
state.storage.sql.exec(
|
|
2368
|
+
`INSERT INTO inbound_events (event_name, payload, claimed_by, claimed_at) VALUES (?, ?, ?, ?)`,
|
|
2369
|
+
"event-1",
|
|
2370
|
+
null,
|
|
2371
|
+
"wait-1",
|
|
2372
|
+
t
|
|
2373
|
+
);
|
|
2374
|
+
expect(() =>
|
|
2375
|
+
state.storage.sql.exec(
|
|
2376
|
+
`INSERT INTO inbound_events (event_name, payload, claimed_by, claimed_at) VALUES (?, ?, ?, ?)`,
|
|
2377
|
+
"event-1",
|
|
2378
|
+
null,
|
|
2379
|
+
"wait-1",
|
|
2380
|
+
t + 1
|
|
2381
|
+
)
|
|
2382
|
+
).toThrow(/UNIQUE/);
|
|
2719
2383
|
});
|
|
2720
2384
|
});
|
|
2721
|
-
|
|
2722
|
-
it("does not change steps when the workflow is terminal", async () => {
|
|
2723
|
-
const executeSpy = vi
|
|
2724
|
-
.spyOn(TestWorkflowDefinition.prototype, "execute")
|
|
2725
|
-
.mockImplementation(async function (this: TestWorkflowDefinition) {
|
|
2726
|
-
await this.run("step-1", async () => 1);
|
|
2727
|
-
});
|
|
2728
|
-
try {
|
|
2729
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2730
|
-
await runInDurableObject(stub, async (instance) => {
|
|
2731
|
-
const { resolve, promise } = Promise.withResolvers<WorkflowStatus>();
|
|
2732
|
-
instance.onStatusChange_experimental = async (status) => {
|
|
2733
|
-
if (status === "running") return;
|
|
2734
|
-
resolve(status);
|
|
2735
|
-
};
|
|
2736
|
-
await instance.create({ definitionVersion: "2026-03-19" });
|
|
2737
|
-
await expect(promise).resolves.toBe("completed");
|
|
2738
|
-
|
|
2739
|
-
const stepsBefore = instance.getSteps_experimental();
|
|
2740
|
-
const eventsBefore = instance.getStepEvents_experimental();
|
|
2741
|
-
|
|
2742
|
-
await instance.handleInboundEvent("event-1", { ignored: true });
|
|
2743
|
-
|
|
2744
|
-
expect(instance.getSteps_experimental()).toEqual(stepsBefore);
|
|
2745
|
-
expect(instance.getStepEvents_experimental()).toEqual(eventsBefore);
|
|
2746
|
-
});
|
|
2747
|
-
} finally {
|
|
2748
|
-
executeSpy.mockRestore();
|
|
2749
|
-
}
|
|
2750
|
-
});
|
|
2751
2385
|
});
|
|
2752
2386
|
|
|
2753
|
-
describe("
|
|
2387
|
+
describe("handleWaitStepTimedOut()", () => {
|
|
2754
2388
|
it("moves a wait step from 'waiting' to 'timed_out'", async () => {
|
|
2755
2389
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2756
2390
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2757
2391
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2758
2392
|
const timeoutAt = new Date(Date.now() - 1000);
|
|
2759
|
-
|
|
2760
|
-
type: "wait",
|
|
2393
|
+
context.getOrCreateWaitStep(createWaitStepId("wait-1"), {
|
|
2761
2394
|
eventName: "event-1",
|
|
2762
2395
|
parentStepId: null,
|
|
2763
|
-
timeoutAt
|
|
2396
|
+
timeoutAt
|
|
2764
2397
|
});
|
|
2765
|
-
context.
|
|
2766
|
-
const
|
|
2767
|
-
type: "wait",
|
|
2398
|
+
context.handleWaitStepTimedOut(createWaitStepId("wait-1"));
|
|
2399
|
+
const step = context.getOrCreateWaitStep(createWaitStepId("wait-1"), {
|
|
2768
2400
|
eventName: "event-1",
|
|
2769
|
-
parentStepId: null
|
|
2770
|
-
timeoutAt: timeoutAt
|
|
2771
|
-
});
|
|
2772
|
-
expect(updatedStep).toMatchObject({
|
|
2773
|
-
state: "timed_out",
|
|
2774
|
-
resolvedAt: expect.any(Date)
|
|
2775
|
-
});
|
|
2776
|
-
});
|
|
2777
|
-
});
|
|
2778
|
-
|
|
2779
|
-
it("writes a 'wait_timed_out' step event when a wait step is timed out", async () => {
|
|
2780
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2781
|
-
await runInDurableObject(stub, async (instance, state) => {
|
|
2782
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2783
|
-
const timeoutAt = new Date(Date.now() - 1000);
|
|
2784
|
-
await context.getOrCreateStep(createWaitStepId("wait-1"), {
|
|
2785
|
-
type: "wait",
|
|
2786
|
-
eventName: "event-1",
|
|
2787
|
-
parentStepId: null,
|
|
2788
|
-
timeoutAt: timeoutAt
|
|
2401
|
+
parentStepId: null
|
|
2789
2402
|
});
|
|
2790
|
-
|
|
2791
|
-
expect(await instance.getStepEvents_experimental()).toMatchObject([
|
|
2792
|
-
{
|
|
2793
|
-
type: "wait_waiting",
|
|
2794
|
-
stepId: "wait-1",
|
|
2795
|
-
eventName: "event-1",
|
|
2796
|
-
timeoutAt: timeoutAt,
|
|
2797
|
-
recordedAt: expect.any(Date)
|
|
2798
|
-
},
|
|
2799
|
-
{
|
|
2800
|
-
type: "wait_timed_out",
|
|
2801
|
-
stepId: "wait-1",
|
|
2802
|
-
recordedAt: expect.any(Date)
|
|
2803
|
-
}
|
|
2804
|
-
]);
|
|
2403
|
+
expect(step).toMatchObject({ state: "timed_out" });
|
|
2805
2404
|
});
|
|
2806
2405
|
});
|
|
2807
2406
|
|
|
@@ -2809,9 +2408,7 @@ describe("WorkflowRuntime", () => {
|
|
|
2809
2408
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2810
2409
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2811
2410
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2812
|
-
expect(() => context.
|
|
2813
|
-
/not found/
|
|
2814
|
-
);
|
|
2411
|
+
expect(() => context.handleWaitStepTimedOut(createWaitStepId("nonexistent"))).toThrow(/not found/);
|
|
2815
2412
|
});
|
|
2816
2413
|
});
|
|
2817
2414
|
|
|
@@ -2819,14 +2416,13 @@ describe("WorkflowRuntime", () => {
|
|
|
2819
2416
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2820
2417
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2821
2418
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2822
|
-
|
|
2823
|
-
type: "wait",
|
|
2419
|
+
context.getOrCreateWaitStep(createWaitStepId("wait-1"), {
|
|
2824
2420
|
eventName: "event-1",
|
|
2825
2421
|
parentStepId: null,
|
|
2826
2422
|
timeoutAt: new Date(Date.now() - 1000)
|
|
2827
2423
|
});
|
|
2828
|
-
context.
|
|
2829
|
-
expect(() => context.
|
|
2424
|
+
context.handleWaitStepTimedOut(createWaitStepId("wait-1"));
|
|
2425
|
+
expect(() => context.handleWaitStepTimedOut(createWaitStepId("wait-1"))).toThrow(
|
|
2830
2426
|
/Expected 'waiting' but got timed_out/
|
|
2831
2427
|
);
|
|
2832
2428
|
});
|