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";
10import type { Api } from "./fixtures/worker-entrypoint-binding/entrypoint-target-worker.ts";
11
12// `dev: true` runs local providers behind the RPC sidecar proxy by default,
13// matching the process topology of the real `alchemy dev` command.
14const { test } = Test.make({
15 providers: Cloudflare.providers(),
16 dev: true,
17});
18
19const logLevel = Effect.provideService(
20 MinimumLogLevel,
21 process.env.DEBUG ? "Debug" : "Info",
22);
23
24class WorkerNotReady extends Data.TaggedError("WorkerNotReady")<{
25 status: number;
26}> {}
27
28const getReady = (url: string) =>
29 Effect.gen(function* () {
30 const client = yield* HttpClient.HttpClient;
31 return yield* client.get(url).pipe(
32 Effect.flatMap((res) =>
33 res.status === 200
34 ? Effect.succeed(res)
35 : Effect.fail(new WorkerNotReady({ status: res.status })),
36 ),
37 Effect.retry({
38 while: (error) => error instanceof WorkerNotReady,
39 schedule: Schedule.exponential("250 millis"),
40 times: 20,
41 }),
42 );
43 }).pipe(Effect.orDie);
44
45/**
46 * Under `alchemy dev` both Workers run on local workerd, and the
47 * `Cloudflare.WorkerEntrypoint` env entry lowers to a local service
48 * designator with `entrypoint` AND `props` — unlike live deploys, workerd
49 * delivers `ctx.props` today, so this test covers the full contract.
50 */
51test.provider(
52 "WorkerEntrypoint binding targets the named entrypoint and delivers ctx.props locally",
53 (stack) =>
54 Effect.gen(function* () {
55 yield* stack.destroy();
56
57 const deployed = yield* stack.deploy(
58 Effect.gen(function* () {
59 const target = yield* Cloudflare.Worker("entrypoint-local-target", {
60 main: pathe.resolve(
61 import.meta.dirname,
62 "fixtures/worker-entrypoint-binding/entrypoint-target-worker.ts",
63 ),
64 });
65 const caller = yield* Cloudflare.Worker("entrypoint-local-caller", {
66 main: pathe.resolve(
67 import.meta.dirname,
68 "fixtures/worker-entrypoint-binding/entrypoint-caller-worker.ts",
69 ),
70 env: {
71 API: Cloudflare.WorkerEntrypoint<Api>(target, {
72 entrypoint: "Api",
73 props: { tenant: "acme" },
74 }),
75 },
76 });
77 return { target, caller };
78 }),
79 );
80
81 // Local dev URLs — proof no cloud call ran.
82 expect(deployed.caller.url).toMatch(/^http:\/\/localhost:\d+$/);
83
84 const greeting = yield* getReady(
85 `${deployed.caller.url}/greet?name=alice`,
86 ).pipe(Effect.flatMap((res) => res.text));
87 expect(greeting).toBe("hello alice from Api");
88
89 const props = (yield* getReady(`${deployed.caller.url}/props`).pipe(
90 Effect.flatMap((res) => res.json),
91 )) as Record<string, unknown>;
92 expect(props).toEqual({ tenant: "acme" });
93
94 yield* stack.destroy();
95 }).pipe(logLevel),
96 {
97 tags: ["provider:cloudflare", "provider:cloudflare:worker", "local"],
98 timeout: 120_000,
99 },
100);