1import * as AWS from "@/AWS";
2import * as Test from "@/Test/Alchemy";
3import * as batch from "@distilled.cloud/aws/batch";
4import * as logs from "@distilled.cloud/aws/cloudwatch-logs";
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 IsolatedProjectJob, {
13 MARKER,
14 project,
15} from "./fixtures/isolated-project-job.ts";
16import { BatchTestNetwork } from "./TestNetwork.ts";
17
18const { test } = Test.make({ providers: AWS.providers() });
19
20// Live proof that the Batch bun bootstrap boots when the job's `main` lives
21// in an isolated project (see test/IsolatedProject.ts) — the bundle `cwd`
22// resolves none of alchemy's dependencies, so the bootstrap's
23// `@distilled.cloud/aws/*` / `@effect/platform-bun` imports must be bundled
24// by the virtual-entry plugin rather than found from the project root. With
25// them left external bun dies at module load and the job FAILS.
26//
27// Heavy (Docker build + ECR push + compute environment + Fargate job run),
28// so gated behind AWS_TEST_SLOW=1 like the platform e2e.
29test.provider.skipIf(!process.env.AWS_TEST_SLOW || !!process.env.FAST)(
30 "job bundled from an isolated project runs to SUCCEEDED",
31 (stack) =>
32 Effect.gen(function* () {
33 yield* materializeIsolatedProject(project);
34 yield* stack.destroy();
35
36 try {
37 const outputs = yield* stack.deploy(
38 Effect.gen(function* () {
39 const network = yield* BatchTestNetwork;
40 const computeEnvironment = yield* AWS.Batch.ComputeEnvironment(
41 "IsolatedProjectCE",
42 {
43 subnets: network.subnetIds,
44 securityGroupIds: network.securityGroupIds,
45 },
46 );
47 const queue = yield* AWS.Batch.JobQueue("IsolatedProjectQueue", {
48 computeEnvironments: [computeEnvironment.computeEnvironmentArn],
49 });
50 const job = yield* IsolatedProjectJob;
51 return {
52 jobQueueArn: queue.jobQueueArn,
53 jobDefinitionArn: job.jobDefinitionArn,
54 };
55 }),
56 );
57
58 const submitted = yield* batch.submitJob({
59 jobName: "alchemy-isolated-project-run",
60 jobQueue: outputs.jobQueueArn,
61 jobDefinition: outputs.jobDefinitionArn!,
62 });
63 const job = yield* batch.describeJobs({ jobs: [submitted.jobId] }).pipe(
64 Effect.map((result) => result.jobs?.[0]),
65 Effect.repeat({
66 schedule: Schedule.spaced("10 seconds"),
67 until: (j): boolean =>
68 j?.status === "SUCCEEDED" || j?.status === "FAILED",
69 times: 60,
70 }),
71 );
72 // SUCCEEDED = container exit 0 = the bootstrap booted and ran `run`
73 // (the reason is included so a FAILED run shows why).
74 expect(`${job?.status}: ${job?.statusReason ?? ""}`).toMatch(
75 /^SUCCEEDED:/,
76 );
77
78 // The bundled program actually executed: its marker is in the job's
79 // log stream (log delivery can lag the state change slightly).
80 const logStreamName = job?.container?.logStreamName;
81 expect(logStreamName).toBeTruthy();
82 const seen = yield* logs
83 .getLogEvents({
84 logGroupName: "/aws/batch/job",
85 logStreamName: logStreamName!,
86 })
87 .pipe(
88 Effect.flatMap((result) =>
89 (result.events ?? []).some((e) => e.message?.includes(MARKER))
90 ? Effect.succeed(true)
91 : Effect.fail(new Error("marker not in log stream yet")),
92 ),
93 Effect.retry({ schedule: Schedule.spaced("5 seconds"), times: 12 }),
94 );
95 expect(seen).toBe(true);
96
97 yield* stack.destroy();
98 } finally {
99 yield* removeIsolatedProject(project);
100 }
101 }),
102 // Docker build + push (~2-4 min) + CE/queue (~1-2 min) + Fargate job run
103 // (~2-4 min) + destroy (~2-3 min).
104 {
105 tags: ["provider:aws", "provider:aws:batch", "provider:aws:ec2", "live"],
106 timeout: 1_200_000,
107 },
108);