1import * as Cloudflare from "@/Cloudflare";
2import * as Test from "@/Test/Alchemy";
3import { describe, expect } from "alchemy-test";
4import * as Effect from "effect/Effect";
5import { MinimumLogLevel } from "effect/References";
6import * as Schedule from "effect/Schedule";
7import * as HttpClient from "effect/http/HttpClient";
8import LocalRemoteStack, { state } from "./fixtures/remote/local-stack.ts";
9
10const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
11 providers: Cloudflare.providers(),
12 state,
13 dev: true,
14});
15
16const 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.
23const HOOK_TIMEOUT = 300_000;
24const TEST_TIMEOUT = 240_000;
25
26const 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 */
38describe(
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);