1import * as AWS from "@/AWS";
2import { Cluster } from "@/AWS/ECS/Cluster.ts";
3import * as Test from "@/Test/Alchemy";
4import * as ecs from "@distilled.cloud/aws/ecs";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import * as Schedule from "effect/Schedule";
8import { getDefaultVpcNetwork } from "../DefaultVpc.ts";
9import CjsDualPackageTaskLive, {
10 CjsDualPackageTask,
11} from "./fixtures/pg-task.ts";
12
13const { test } = Test.make({ providers: AWS.providers() });
14
15// Live behavioral proof for CommonJS dual-package resolution: a deployed
16// task whose program imports `pg` (a CJS consumer of the dual-package
17// `pg-pool`, whose exports list `import` before `require`). With the former
18// bundler conditions (`"import"` in the set for both import kinds) the
19// bundle died AT LOAD with `TypeError: The superclass is not a constructor`
20// and the container exited 1; the fixture must boot, construct a Pool, and
21// exit 0. The unit-level pin is test/Bundle/ConditionNames.test.ts — this
22// is the same defect observed from the cloud.
23//
24// Docker + ECR + Fargate placement is minutes of wall clock, so like the
25// Task e2e smoke test it is gated out of the default sweep: run it
26// explicitly with `AWS_TEST_SLOW=1`.
27test.provider.skipIf(!process.env.AWS_TEST_SLOW || !!process.env.FAST)(
28 "task importing a CJS dual-package consumer (pg) boots and exits 0",
29 (stack) =>
30 Effect.gen(function* () {
31 yield* stack.destroy();
32
33 // Reuse the standing default VPC: a public subnet + public IP is
34 // required for the task ENI to pull the image from ECR.
35 const { subnetIds } = yield* getDefaultVpcNetwork;
36 const subnetId = subnetIds[0];
37 expect(subnetId).toBeTruthy();
38
39 const { clusterArn, taskDefinitionArn } = yield* stack.deploy(
40 Effect.gen(function* () {
41 const cluster = yield* Cluster("CjsDualCluster", {
42 clusterName: "alchemy-test-cjs-dual",
43 });
44 const task = yield* CjsDualPackageTask;
45 return {
46 clusterArn: cluster.clusterArn,
47 taskDefinitionArn: task.taskDefinitionArn,
48 };
49 }).pipe(Effect.provide(CjsDualPackageTaskLive)),
50 );
51 expect(taskDefinitionArn).toBeTruthy();
52
53 // Launch once, out-of-band; retry the IAM-propagation rejection.
54 const started = yield* ecs
55 .runTask({
56 cluster: clusterArn,
57 taskDefinition: taskDefinitionArn!,
58 launchType: "FARGATE",
59 count: 1,
60 startedBy: "alchemy-cjs-dual-test",
61 networkConfiguration: {
62 awsvpcConfiguration: {
63 subnets: [subnetId!],
64 assignPublicIp: "ENABLED",
65 },
66 },
67 })
68 .pipe(
69 Effect.retry({
70 while: (e) =>
71 e._tag === "ClientException" &&
72 (e.message ?? "").includes("unable to assume the role"),
73 schedule: Schedule.spaced("5 seconds"),
74 times: 12,
75 }),
76 );
77 expect(started.failures ?? []).toEqual([]);
78 const taskArn = started.tasks?.[0]?.taskArn;
79 expect(taskArn).toBeTruthy();
80
81 const stopped = yield* ecs
82 .describeTasks({ cluster: clusterArn, tasks: [taskArn!] })
83 .pipe(
84 Effect.flatMap((result) => {
85 const task = result.tasks?.[0];
86 return task?.lastStatus === "STOPPED"
87 ? Effect.succeed(task)
88 : Effect.fail(
89 new Error(`task not stopped yet: ${task?.lastStatus}`),
90 );
91 }),
92 Effect.tapError((error) => Effect.logInfo(String(error))),
93 Effect.retry({ schedule: Schedule.spaced("6 seconds"), times: 50 }),
94 );
95
96 // Exit 0 = pg loaded, BoundPool constructed. A mis-resolved bundle
97 // dies at module load and exits 1.
98 expect(stopped.stoppedReason ?? "").not.toContain("CannotPullContainer");
99 expect(stopped.containers?.[0]?.exitCode).toBe(0);
100
101 yield* stack.destroy();
102 }),
103 {
104 tags: ["provider:aws", "provider:aws:ec2", "provider:aws:ecs", "live"],
105 timeout: 900_000,
106 },
107);