|
1 | | -import { describe, expect, it } from "@effect/vitest" |
2 | | -import { DurableClock, Workflow, WorkflowEngine } from "@effect/workflow" |
| 1 | +import { assert, describe, expect, it } from "@effect/vitest" |
| 2 | +import { DurableClock, DurableDeferred, Workflow, WorkflowEngine } from "@effect/workflow" |
| 3 | +import * as Cause from "effect/Cause" |
3 | 4 | import * as Effect from "effect/Effect" |
4 | 5 | import * as Exit from "effect/Exit" |
| 6 | +import * as FiberId from "effect/FiberId" |
5 | 7 | import * as Layer from "effect/Layer" |
6 | 8 | import * as Schema from "effect/Schema" |
7 | 9 | import * as TestClock from "effect/TestClock" |
@@ -36,8 +38,112 @@ describe("WorkflowEngine", () => { |
36 | 38 | ) |
37 | 39 | ) |
38 | 40 | )) |
| 41 | + |
| 42 | + it.effect("does not squash workflow failures after suspension", () => |
| 43 | + Effect.gen(function*() { |
| 44 | + const instance = WorkflowEngine.WorkflowInstance.initial(TestWorkflow, "workflow-failure") |
| 45 | + instance.suspended = true |
| 46 | + |
| 47 | + const result = yield* Workflow.intoResult(Effect.fail("boom")).pipe( |
| 48 | + Effect.provideService(WorkflowEngine.WorkflowInstance, instance) |
| 49 | + ) |
| 50 | + |
| 51 | + assert.deepStrictEqual(result, new Workflow.Complete({ exit: Exit.fail("boom") })) |
| 52 | + })) |
| 53 | + |
| 54 | + it.effect("removes suspension interrupts from mixed workflow failures", () => |
| 55 | + Effect.gen(function*() { |
| 56 | + const instance = WorkflowEngine.WorkflowInstance.initial(TestWorkflow, "mixed-workflow-failure") |
| 57 | + const cause = Cause.parallel(Cause.fail("boom"), Cause.interrupt(FiberId.none)) |
| 58 | + |
| 59 | + const result = yield* Workflow.intoResult(Effect.failCause(cause)).pipe( |
| 60 | + Effect.provideService(WorkflowEngine.WorkflowInstance, instance) |
| 61 | + ) |
| 62 | + |
| 63 | + assert.deepStrictEqual(result, new Workflow.Complete({ exit: Exit.fail("boom") })) |
| 64 | + })) |
| 65 | + |
| 66 | + it.effect("DurableDeferred.into isolates inner suspension from failure recording", () => |
| 67 | + Effect.gen(function*() { |
| 68 | + const exits: Array<Exit.Exit<number, string>> = [] |
| 69 | + const instance = WorkflowEngine.WorkflowInstance.initial(TestWorkflow, "deferred-failure") |
| 70 | + const deferred = DurableDeferred.make("deferred-failure", { |
| 71 | + success: Schema.Number, |
| 72 | + error: Schema.String |
| 73 | + }) |
| 74 | + |
| 75 | + yield* DurableDeferred.into( |
| 76 | + Effect.flatMap(WorkflowEngine.WorkflowInstance, (instance) => |
| 77 | + Effect.zipRight( |
| 78 | + Effect.sync(() => { |
| 79 | + instance.suspended = true |
| 80 | + }), |
| 81 | + Effect.fail("boom") |
| 82 | + )), |
| 83 | + deferred |
| 84 | + ).pipe( |
| 85 | + Effect.exit, |
| 86 | + Effect.provideService(WorkflowEngine.WorkflowInstance, instance), |
| 87 | + Effect.provideService(WorkflowEngine.WorkflowEngine, makeDeferredEngine(exits)) |
| 88 | + ) |
| 89 | + |
| 90 | + assert.isFalse(instance.suspended) |
| 91 | + assert.deepStrictEqual(exits, [Exit.fail("boom")]) |
| 92 | + })) |
| 93 | + |
| 94 | + it.effect("DurableDeferred.into propagates interrupt-only suspension to the parent", () => |
| 95 | + Effect.gen(function*() { |
| 96 | + const exits: Array<Exit.Exit<number, string>> = [] |
| 97 | + const instance = WorkflowEngine.WorkflowInstance.initial(TestWorkflow, "deferred-suspended") |
| 98 | + const deferred = DurableDeferred.make("deferred-suspended", { |
| 99 | + success: Schema.Number, |
| 100 | + error: Schema.String |
| 101 | + }) |
| 102 | + |
| 103 | + yield* DurableDeferred.into( |
| 104 | + Effect.flatMap(WorkflowEngine.WorkflowInstance, (instance) => |
| 105 | + Effect.zipRight( |
| 106 | + Effect.sync(() => { |
| 107 | + instance.suspended = true |
| 108 | + }), |
| 109 | + Effect.interrupt |
| 110 | + )), |
| 111 | + deferred |
| 112 | + ).pipe( |
| 113 | + Effect.exit, |
| 114 | + Effect.provideService(WorkflowEngine.WorkflowInstance, instance), |
| 115 | + Effect.provideService(WorkflowEngine.WorkflowEngine, makeDeferredEngine(exits)) |
| 116 | + ) |
| 117 | + |
| 118 | + assert.isTrue(instance.suspended) |
| 119 | + assert.deepStrictEqual(exits, []) |
| 120 | + })) |
39 | 121 | }) |
40 | 122 |
|
| 123 | +const TestWorkflow = Workflow.make({ |
| 124 | + name: "TestWorkflow", |
| 125 | + payload: {}, |
| 126 | + idempotencyKey: () => "test", |
| 127 | + success: Schema.Number, |
| 128 | + error: Schema.String |
| 129 | +}) |
| 130 | + |
| 131 | +const makeDeferredEngine = (exits: Array<Exit.Exit<number, string>>): WorkflowEngine.WorkflowEngine["Type"] => |
| 132 | + WorkflowEngine.WorkflowEngine.of({ |
| 133 | + register: () => Effect.void, |
| 134 | + execute: () => Effect.die("not implemented"), |
| 135 | + poll: () => Effect.succeed(undefined), |
| 136 | + interrupt: () => Effect.void, |
| 137 | + resume: () => Effect.void, |
| 138 | + activityExecute: () => Effect.die("not implemented"), |
| 139 | + deferredResult: () => Effect.succeed(undefined), |
| 140 | + deferredDone: (_deferred: DurableDeferred.Any, options: { readonly exit: Exit.Exit<unknown, unknown> }) => |
| 141 | + Effect.sync(() => { |
| 142 | + exits.push(options.exit as Exit.Exit<number, string>) |
| 143 | + }), |
| 144 | + scheduleClock: () => Effect.void |
| 145 | + } as any) |
| 146 | + |
41 | 147 | const LongWorkflow = Workflow.make({ |
42 | 148 | name: "LongWorkflow", |
43 | 149 | payload: { |
|
0 commit comments