| 1 | import * as Cloudflare from "@/Cloudflare"; |
| 2 | import * as Test from "@/Test/Alchemy"; |
| 3 | import { describe, expect } from "alchemy-test"; |
| 4 | import * as Effect from "effect/Effect"; |
| 5 | import { MinimumLogLevel } from "effect/References"; |
| 6 | import * as Schedule from "effect/Schedule"; |
| 7 | import * as HttpClient from "effect/http/HttpClient"; |
| 8 | import LocalRemoteStack, { state } from "./fixtures/remote/local-stack.ts"; |
| 9 | |
| 10 | const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ |
| 11 | providers: Cloudflare.providers(), |
| 12 | state, |
| 13 | dev: true, |
| 14 | }); |
| 15 | |
| 16 | const logLevel = Effect.provideService( |
| 17 | MinimumLogLevel, |
| 18 | process.env.DEBUG ? "Debug" : "Info", |
| 19 | ); |
| 20 | |
| 21 | // First request has to wait for the local runtime to `docker pull` the image |
| 22 | // and boot the container, so give it plenty of room. |
| 23 | const HOOK_TIMEOUT = 300_000; |
| 24 | const TEST_TIMEOUT = 240_000; |
| 25 | |
| 26 | const readinessSchedule = Schedule.min([ |
| 27 | Schedule.exponential("500 millis"), |
| 28 | Schedule.spaced("3 seconds"), |
| 29 | ]); |
| 30 | |
| 31 | /** |
| 32 | * Remote container (`image`) under `alchemy dev`: the local provider must |
| 33 | * resolve `dev: { imageUri }` so the local runtime can `docker pull` the |
| 34 | * pre-built image (regression: it previously died with "Container requires a |
| 35 | * `main` entrypoint" because it only handled the Effect-native variant). |
| 36 | * The DO proxies a request to the echo server running inside the container. |
| 37 | */ |
| 38 | describe( |
| 39 | "local remote container (image)", |
| 40 | { |
| 41 | tags: [ |
| 42 | "provider:cloudflare", |
| 43 | "provider:cloudflare:container", |
| 44 | "provider:cloudflare:worker", |
| 45 | ], |
| 46 | }, |
| 47 | () => { |
| 48 | const stack = beforeAll(deploy(LocalRemoteStack), { |
| 49 | timeout: HOOK_TIMEOUT, |
| 50 | }); |
| 51 | afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(LocalRemoteStack), { |
| 52 | timeout: HOOK_TIMEOUT, |
| 53 | }); |
| 54 | |
| 55 | test( |
| 56 | "pulls the remote image and serves it over its TCP port", |
| 57 | Effect.gen(function* () { |
| 58 | const { url } = yield* stack; |
| 59 | const client = yield* HttpClient.HttpClient; |
| 60 | |
| 61 | // `new URL` (not `${url}/hello`): the dev url has a trailing slash, and |
| 62 | // a `//hello` path is protocol-relative — the local dev proxy would |
| 63 | // resolve it to host "hello" and die with a DNS error. |
| 64 | const body = yield* client.get(new URL("/hello", url)).pipe( |
| 65 | Effect.flatMap((r) => |
| 66 | r.status !== 200 |
| 67 | ? Effect.fail(new Error(`not ready: ${r.status}`)) |
| 68 | : Effect.flatMap(r.text, (text) => |
| 69 | // mendhak/http-https-echo echoes the request (method, path, |
| 70 | // headers) back as JSON. |
| 71 | text.includes("method") |
| 72 | ? Effect.succeed(text) |
| 73 | : Effect.fail(new Error(`not ready: got ${text}`)), |
| 74 | ), |
| 75 | ), |
| 76 | Effect.timeout("30 seconds"), |
| 77 | Effect.retry({ schedule: readinessSchedule, times: 30 }), |
| 78 | ); |
| 79 | expect(body).toContain("method"); |
| 80 | }).pipe(logLevel), |
| 81 | { tags: ["local"], timeout: TEST_TIMEOUT }, |
| 82 | ); |
| 83 | }, |
| 84 | ); |