| 1 | import * as AWS from "@/AWS"; |
| 2 | import * as Test from "@/Test/Alchemy"; |
| 3 | import * as apprunner from "@distilled.cloud/aws/apprunner"; |
| 4 | import { expect } from "alchemy-test"; |
| 5 | import * as Effect from "effect/Effect"; |
| 6 | import * as Schedule from "effect/Schedule"; |
| 7 | import * as HttpClient from "effect/http/HttpClient"; |
| 8 | import { |
| 9 | materializeIsolatedProject, |
| 10 | removeIsolatedProject, |
| 11 | } from "../../IsolatedProject.ts"; |
| 12 | import IsolatedProjectService, { |
| 13 | project, |
| 14 | } from "./fixtures/isolated-project-service.ts"; |
| 15 | |
| 16 | const { test } = Test.make({ providers: AWS.providers() }); |
| 17 | |
| 18 | // Live proof that the App Runner bun bootstrap boots when the service's |
| 19 | // `main` lives in an isolated project (see test/IsolatedProject.ts) — the |
| 20 | // bundle `cwd` resolves none of alchemy's dependencies, so the bootstrap's |
| 21 | // `@distilled.cloud/aws/*` / `@effect/platform-bun` imports must be bundled |
| 22 | // by the virtual-entry plugin rather than found from the project root. With |
| 23 | // them left external the container dies at module load, the health check |
| 24 | // never passes, and the service never reaches RUNNING. |
| 25 | // |
| 26 | // Heavy (Docker build + ECR push + 3-5 min App Runner provisioning, bills |
| 27 | // while running), so gated behind AWS_TEST_SLOW=1 like the platform e2e. |
| 28 | test.provider.skipIf(!process.env.AWS_TEST_SLOW || !!process.env.FAST)( |
| 29 | "service bundled from an isolated project boots and serves HTTP", |
| 30 | (stack) => |
| 31 | Effect.gen(function* () { |
| 32 | yield* materializeIsolatedProject(project); |
| 33 | yield* stack.destroy(); |
| 34 | |
| 35 | try { |
| 36 | const service = yield* stack.deploy( |
| 37 | Effect.gen(function* () { |
| 38 | return yield* IsolatedProjectService; |
| 39 | }), |
| 40 | ); |
| 41 | expect(service.status).toBe("RUNNING"); |
| 42 | expect(service.serviceUrl).toBeTruthy(); |
| 43 | |
| 44 | const health = yield* HttpClient.get( |
| 45 | `https://${service.serviceUrl}/health`, |
| 46 | ).pipe( |
| 47 | Effect.flatMap((res) => |
| 48 | res.status === 200 |
| 49 | ? res.json |
| 50 | : Effect.fail(new Error(`/health returned ${res.status}`)), |
| 51 | ), |
| 52 | Effect.retry({ |
| 53 | schedule: Schedule.max([ |
| 54 | Schedule.fixed("3 seconds"), |
| 55 | Schedule.recurs(20), |
| 56 | ]), |
| 57 | }), |
| 58 | ); |
| 59 | expect(health).toEqual({ ok: true }); |
| 60 | |
| 61 | // Destroy immediately — App Runner services bill while running. |
| 62 | const { serviceArn } = service; |
| 63 | yield* stack.destroy(); |
| 64 | const after = yield* apprunner |
| 65 | .describeService({ ServiceArn: serviceArn }) |
| 66 | .pipe( |
| 67 | Effect.map((r) => (r.Service.Status ?? "UNKNOWN").toUpperCase()), |
| 68 | Effect.catchTag("ResourceNotFoundException", () => |
| 69 | Effect.succeed("GONE" as const), |
| 70 | ), |
| 71 | ); |
| 72 | expect(["GONE", "DELETED"]).toContain(after); |
| 73 | } finally { |
| 74 | yield* removeIsolatedProject(project); |
| 75 | } |
| 76 | }), |
| 77 | // Docker build + push (~2-4 min) + create (~3-5 min) + delete (~2-3 min). |
| 78 | { |
| 79 | tags: ["provider:aws", "provider:aws:apprunner", "live"], |
| 80 | timeout: 1_200_000, |
| 81 | }, |
| 82 | ); |