1import * as Cloudflare from "@/Cloudflare/index.ts";
2import * as Test from "@/Test/Alchemy";
3import { expect } from "alchemy-test";
4import * as Data from "effect/Data";
5import * as Effect from "effect/Effect";
6import { MinimumLogLevel } from "effect/References";
7import * as Schedule from "effect/Schedule";
8import * as HttpClient from "effect/http/HttpClient";
9import * as pathe from "pathe";
10
11const { test } = Test.make({
12 providers: Cloudflare.providers(),
13 dev: true,
14});
15
16const logLevel = Effect.provideService(
17 MinimumLogLevel,
18 process.env.DEBUG ? "Debug" : "Info",
19);
20
21const fixtureDir = pathe.resolve(import.meta.dirname, "fixtures/assets-only");
22
23class WorkerNotReady extends Data.TaggedError("WorkerNotReady")<{
24 status: number;
25}> {}
26
27const getTextReady = (url: string) =>
28 Effect.gen(function* () {
29 const client = yield* HttpClient.HttpClient;
30 const res = yield* client.get(url).pipe(
31 Effect.flatMap((res) =>
32 res.status === 200
33 ? Effect.succeed(res)
34 : Effect.fail(new WorkerNotReady({ status: res.status })),
35 ),
36 Effect.retry({
37 while: (e): e is WorkerNotReady => e instanceof WorkerNotReady,
38 schedule: Schedule.max([
39 Schedule.min([
40 Schedule.exponential("500 millis"),
41 Schedule.spaced("2 seconds"),
42 ]),
43 Schedule.recurs(10),
44 ]),
45 }),
46 );
47 return yield* res.text;
48 }).pipe(Effect.orDie);
49
50/**
51 * An assets-only Worker (no `main`, no `script`) under `alchemy dev`: the
52 * local provider serves a stub that delegates every request to the ASSETS
53 * binding, so static files and the asset layer's 404 handling work without
54 * any entry module.
55 */
56test.provider(
57 "assets-only worker serves static files locally",
58 (stack) =>
59 Effect.gen(function* () {
60 yield* stack.destroy();
61
62 const deployed = yield* stack.deploy(
63 Effect.gen(function* () {
64 const worker = yield* Cloudflare.Worker("assets-only-local", {
65 assets: { directory: fixtureDir, notFoundHandling: "404-page" },
66 });
67 return { worker };
68 }),
69 );
70
71 // The local provider serves from the dev proxy — proof no cloud call
72 // ran.
73 expect(deployed.worker.url).toMatch(/^http:\/\/localhost:\d+$/);
74
75 const body = yield* getTextReady(`${deployed.worker.url}`);
76 expect(body).toContain("alchemy-assets-only-index");
77
78 // The asset layer's custom 404 page is applied by the assets worker
79 // itself — the stub only delegates.
80 const client = yield* HttpClient.HttpClient;
81 const missing = yield* client
82 .get(`${deployed.worker.url}/does-not-exist`)
83 .pipe(Effect.orDie);
84 expect(missing.status).toBe(404);
85 const missingBody = yield* missing.text.pipe(Effect.orDie);
86 expect(missingBody).toContain("alchemy-assets-only-404");
87
88 yield* stack.destroy();
89 }).pipe(logLevel),
90 {
91 tags: ["provider:cloudflare", "provider:cloudflare:worker", "local"],
92 timeout: 120_000,
93 },
94);