1import * as AWS from "@/AWS";
2import { encodeDurableEnvelope } from "@/AWS/Lambda/DurableBridge.ts";
3import * as Core from "@/Test/Core";
4import * as Test from "@/Test/Alchemy";
5import * as Lambda from "@distilled.cloud/aws/lambda";
6import { describe, expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import * as Redacted from "effect/Redacted";
9import * as Result from "effect/Result";
10import * as Schedule from "effect/Schedule";
11import DurableFlowLive, { DurableFlow } from "./fixtures/durable-handler";
12
13const testOptions = { providers: AWS.providers() };
14const { test, beforeAll, afterAll } = Test.make(testOptions);
15const sharedStack = Core.scratchStack(testOptions, "LambdaDurable");
16
17const unwrapSensitive = (
18 value: string | Redacted.Redacted<string> | undefined,
19): string | undefined =>
20 value === undefined
21 ? undefined
22 : Redacted.isRedacted(value)
23 ? Redacted.value(value)
24 : value;
25
26describe(
27 "Lambda DurableFunction",
28 { tags: ["provider:aws", "provider:aws:lambda", "live"] },
29 () => {
30 // Ungated typed-error probe: proves the durable-execution management API is
31 // reachable in this region and that its errors are typed tags in the
32 // distilled union — at near-zero cost, forever.
33 test.provider(
34 "durable management API returns typed errors",
35 (_stack) =>
36 Effect.gen(function* () {
37 const result = yield* Effect.result(
38 Lambda.listDurableExecutionsByFunction({
39 FunctionName: "alchemy-durable-probe-nonexistent",
40 }),
41 );
42 expect(Result.isFailure(result)).toBe(true);
43 if (Result.isFailure(result)) {
44 expect(result.failure._tag).toBe("ResourceNotFoundException");
45 }
46 }),
47 { timeout: 30_000 },
48 );
49
50 // Full lifecycle: a real deploy (bundling + vendoring the Durable Execution
51 // SDK + waiting out IAM role propagation) plus a live durable execution with
52 // a suspend/resume.
53 describe("lifecycle", () => {
54 let functionName: string;
55 let qualifier: string;
56
57 beforeAll(
58 Effect.gen(function* () {
59 yield* Effect.logInfo(
60 "Durable test setup: destroying previous stack",
61 );
62 yield* sharedStack.destroy();
63 yield* Effect.logInfo(
64 "Durable test setup: deploying the DurableFunction",
65 );
66 const outputs = yield* sharedStack.deploy(
67 Effect.gen(function* () {
68 const flow = yield* DurableFlow;
69 const version = yield* AWS.Lambda.Version("DurableFlowVersion", {
70 function: flow.function,
71 });
72 const live = yield* AWS.Lambda.Alias("DurableFlowLive", {
73 version,
74 aliasName: "live",
75 });
76 return {
77 functionName: flow.functionName,
78 qualifier: live.aliasName,
79 };
80 }).pipe(Effect.provide(DurableFlowLive)),
81 );
82 functionName = outputs.functionName;
83 qualifier = outputs.qualifier;
84 expect(functionName).toBeTruthy();
85 expect(qualifier).toBe("live");
86 }),
87 // ~35s in isolation, but the deploy bundles + npm-vendors the Durable
88 // Execution SDK in-process; under full-suite CPU saturation the
89 // reconcile alone has been observed to exceed 420s.
90 { timeout: 600_000 },
91 );
92
93 afterAll(sharedStack.destroy(), { timeout: 120_000 });
94
95 test.provider(
96 "runs a 2-step + sleep orchestration to completion",
97 (_stack) =>
98 Effect.gen(function* () {
99 // Start: async Invoke with the alchemy envelope and an idempotent
100 // execution name (safe to re-run — same name + same payload
101 // reattaches to the existing execution).
102 const started = yield* Lambda.invoke({
103 FunctionName: functionName,
104 Qualifier: qualifier,
105 InvocationType: "Event",
106 DurableExecutionName: "durable-test-flow-1",
107 Payload: encodeDurableEnvelope("DurableFlow", {
108 orderId: "order-1",
109 }),
110 });
111 expect(started.StatusCode).toBe(202);
112
113 // Resolve the execution ARN — from the Invoke response when
114 // present, otherwise via the list API (bounded).
115 const executionArn = started.DurableExecutionArn
116 ? started.DurableExecutionArn
117 : yield* Lambda.listDurableExecutionsByFunction({
118 FunctionName: functionName,
119 DurableExecutionName: "durable-test-flow-1",
120 }).pipe(
121 Effect.map(
122 (r) => r.DurableExecutions?.[0]?.DurableExecutionArn,
123 ),
124 Effect.repeat({
125 schedule: Schedule.spaced("2 seconds"),
126 until: (arn) => arn !== undefined,
127 times: 10,
128 }),
129 Effect.map((arn) => arn!),
130 );
131
132 // The flow sleeps 5s mid-way (a real suspend/resume), so poll the
133 // execution to a terminal state with a bounded schedule.
134 const execution = yield* Lambda.getDurableExecution({
135 DurableExecutionArn: executionArn,
136 }).pipe(
137 Effect.repeat({
138 schedule: Schedule.spaced("3 seconds"),
139 until: (r) => r.Status !== "RUNNING",
140 times: 30,
141 }),
142 );
143
144 expect(execution.Status).toBe("SUCCEEDED");
145 const result = JSON.parse(
146 unwrapSensitive(execution.Result) ?? "{}",
147 );
148 expect(result).toEqual({
149 orderId: "order-1",
150 reserved: true,
151 total: 42,
152 });
153 }),
154 { timeout: 150_000 },
155 );
156 });
157 },
158);