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 {
9 materializeIsolatedProject,
10 removeIsolatedProject,
11} from "../../IsolatedProject.ts";
12import { getDefaultVpcNetwork } from "../DefaultVpc.ts";
13import IsolatedProjectTaskLive, {
14 IsolatedProjectTask,
15 project,
16} from "./fixtures/isolated-project-task.ts";
17
18const { test } = Test.make({ providers: AWS.providers() });
19
20// Live proof that the generated bun bootstrap boots when the task's `main`
21// lives in a project that cannot resolve alchemy's dependencies itself — a
22// consumer installed with bun's isolated linker (or pnpm), where
23// `@distilled.cloud/aws`, `@effect/platform-bun`, … sit beside `alchemy`
24// rather than in the project's own `node_modules`.
25//
26// The fixture's `main` points at an isolated project OUTSIDE the repository
27// (see test/IsolatedProject.ts), so the bundle `cwd` resolves nothing bare.
28// We deploy it (Docker build + ECR push), launch it once on Fargate, and
29// assert the task runs to completion with container exit code 0. With the
30// bootstrap's imports left external the container dies at boot with
31// `Cannot find module …` (exit code 1).
32//
33// Docker + ECR + Fargate placement is minutes of wall clock, so like the Task
34// e2e smoke test this is gated out of the default sweep: run it explicitly
35// with `AWS_TEST_SLOW=1`.
36test.provider.skipIf(!process.env.AWS_TEST_SLOW || !!process.env.FAST)(
37 "task bundled from an isolated project boots and exits 0",
38 (stack) =>
39 Effect.gen(function* () {
40 yield* materializeIsolatedProject(project);
41 yield* stack.destroy();
42
43 try {
44 // Reuse the standing default VPC: a public subnet + public IP is
45 // required for the task ENI to pull the image from ECR.
46 const { subnetIds } = yield* getDefaultVpcNetwork;
47 const subnetId = subnetIds[0];
48 expect(subnetId).toBeTruthy();
49
50 const { clusterArn, taskDefinitionArn } = yield* stack.deploy(
51 Effect.gen(function* () {
52 const cluster = yield* Cluster("IsolatedProjectCluster", {
53 clusterName: "alchemy-test-isolated-project",
54 });
55 const task = yield* IsolatedProjectTask;
56 return {
57 clusterArn: cluster.clusterArn,
58 taskDefinitionArn: task.taskDefinitionArn,
59 };
60 }).pipe(Effect.provide(IsolatedProjectTaskLive)),
61 );
62
63 expect(taskDefinitionArn).toBeTruthy();
64
65 // Launch the one-shot task once, out-of-band. The task/execution
66 // roles were created seconds ago: ECS rejects `runTask` with
67 // "unable to assume the role" until IAM propagates, so retry that
68 // one typed error (bounded).
69 const started = yield* ecs
70 .runTask({
71 cluster: clusterArn,
72 taskDefinition: taskDefinitionArn!,
73 launchType: "FARGATE",
74 count: 1,
75 startedBy: "alchemy-isolated-project-test",
76 networkConfiguration: {
77 awsvpcConfiguration: {
78 subnets: [subnetId!],
79 assignPublicIp: "ENABLED",
80 },
81 },
82 })
83 .pipe(
84 Effect.retry({
85 while: (e) =>
86 e._tag === "ClientException" &&
87 (e.message ?? "").includes("unable to assume the role"),
88 schedule: Schedule.spaced("5 seconds"),
89 times: 12,
90 }),
91 );
92 expect(started.failures ?? []).toEqual([]);
93 const taskArn = started.tasks?.[0]?.taskArn;
94 expect(taskArn).toBeTruthy();
95
96 // Wait for the task to stop: image pull + container boot + the
97 // one-shot program running to completion (~1-3 minutes cold).
98 const stopped = yield* ecs
99 .describeTasks({ cluster: clusterArn, tasks: [taskArn!] })
100 .pipe(
101 Effect.flatMap((result) => {
102 const task = result.tasks?.[0];
103 return task?.lastStatus === "STOPPED"
104 ? Effect.succeed(task)
105 : Effect.fail(
106 new Error(`task not stopped yet: ${task?.lastStatus}`),
107 );
108 }),
109 Effect.tapError((error) => Effect.logInfo(String(error))),
110 Effect.retry({
111 schedule: Schedule.spaced("6 seconds"),
112 times: 50,
113 }),
114 );
115
116 // Exit code 0 proves the bootstrap's own imports were bundled and the
117 // program ran `{ run }` to completion; externalised imports crash bun
118 // at module load and the container exits 1.
119 expect(stopped.stoppedReason ?? "").not.toContain(
120 "CannotPullContainer",
121 );
122 expect(stopped.containers?.[0]?.exitCode).toBe(0);
123
124 yield* stack.destroy();
125 } finally {
126 yield* removeIsolatedProject(project);
127 }
128 }),
129 {
130 tags: ["provider:aws", "provider:aws:ec2", "provider:aws:ecs", "live"],
131 timeout: 900_000,
132 },
133);