1import * as AWS from "@/AWS";
2import * as Core from "@/Test/Core";
3import * as Test from "@/Test/Alchemy";
4import { describe, expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import * as Schedule from "effect/Schedule";
7import * as HttpClient from "effect/http/HttpClient";
8import NestedEcsReproFunctionLive, {
9 NestedEcsReproFunction,
10} from "./fixtures/nested-ecs-lambda.ts";
11
12// Regression for the "Platform-in-Platform init OOM": a Platform (ECS.Task)
13// yielded DIRECTLY inside another Platform's (Lambda.Function) init program.
14// Before the fix, the nested Task's ConfigProvider interceptor recursed back
15// through the outer Lambda's interceptor at runtime (`ctx.get` → `Config` →
16// interceptor → `ctx.get` …), growing the heap without bound until the Lambda
17// sandbox died with `Runtime.OutOfMemory` at init.
18//
19// The fixture removes the `Resource.ref` workaround the ECS Bindings handler
20// uses. If the nested-Platform init still OOMed, the deployed Lambda would
21// never boot and this fetch would never return 200.
22const testOptions = { providers: AWS.providers() };
23const { beforeAll, afterAll, test } = Test.make(testOptions);
24const sharedStack = Core.scratchStack(testOptions, "NestedPlatformInit");
25
26let baseUrl: string;
27
28// Fresh workers/Lambda URLs take a few seconds to start serving 200s; a
29// sandbox that OOMs at init keeps returning 5xx until the readiness budget is
30// exhausted, which fails the suite loudly.
31const readinessPolicy = Schedule.max([
32 Schedule.fixed("2 seconds"),
33 Schedule.recurs(10),
34]);
35
36describe.sequential(
37 "ECS Nested Platform Init",
38 { tags: ["provider:aws", "provider:aws:ecs", "provider:aws:lambda", "live"] },
39 () => {
40 beforeAll(
41 Effect.gen(function* () {
42 yield* Effect.logInfo(
43 "nested-platform setup: destroying previous stack",
44 );
45 yield* sharedStack.destroy();
46
47 yield* Effect.logInfo(
48 "nested-platform setup: deploying nested-Task Lambda",
49 );
50 const { functionUrl } = yield* sharedStack.deploy(
51 Effect.gen(function* () {
52 return yield* NestedEcsReproFunction;
53 }).pipe(Effect.provide(NestedEcsReproFunctionLive)),
54 );
55
56 expect(functionUrl).toBeTruthy();
57 baseUrl = functionUrl!.replace(/\/+$/, "");
58
59 yield* Effect.logInfo(
60 `nested-platform setup: probing readiness at ${baseUrl}`,
61 );
62 yield* HttpClient.get(baseUrl).pipe(
63 Effect.flatMap((response) =>
64 response.status === 200
65 ? Effect.succeed(response)
66 : Effect.fail(
67 new Error(`Function not ready: ${response.status}`),
68 ),
69 ),
70 Effect.tapError((error) =>
71 Effect.logWarning(
72 `nested-platform setup: fixture not ready yet (${String(error)})`,
73 ),
74 ),
75 Effect.retry({ schedule: readinessPolicy }),
76 );
77 yield* Effect.logInfo("nested-platform setup: fixture ready");
78 }),
79 { timeout: 180_000 },
80 );
81
82 afterAll(sharedStack.destroy(), { timeout: 180_000 });
83
84 test(
85 "nested Platform (ECS.Task in Lambda) init does not OOM the sandbox",
86 Effect.gen(function* () {
87 const response = yield* HttpClient.get(baseUrl);
88 expect(response.status).toBe(200);
89 const body = (yield* response.json) as {
90 ok?: boolean;
91 taskType?: string;
92 containerName?: string;
93 };
94 expect(body.ok).toBe(true);
95 expect(body.taskType).toBe("AWS.ECS.Task");
96 expect(body.containerName).toBeTruthy();
97 }),
98 { timeout: 120_000 },
99 );
100 },
101);