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 FetchCallerWorker from "./fixtures/fetch-binding/fetch-caller.ts";
10import FetchTargetWorker from "./fixtures/fetch-binding/fetch-target.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 body: string;
27}> {}
28
29/**
30 * GET a route, retrying until the freshly started workerd serves a 200 —
31 * including 500s from the caller while the target's dev-registry entry is
32 * still propagating to the caller's service-binding proxy.
33 */
34const getTextReady = (url: string) =>
35 Effect.gen(function* () {
36 const client = yield* HttpClient.HttpClient;
37 const res = yield* client.get(url).pipe(
38 Effect.flatMap((res) =>
39 res.status === 200
40 ? Effect.succeed(res)
41 : res.text.pipe(
42 Effect.flatMap((body) =>
43 Effect.fail(new WorkerNotReady({ status: res.status, body })),
44 ),
45 ),
46 ),
47 Effect.retry({
48 while: (e): e is WorkerNotReady => e instanceof WorkerNotReady,
49 schedule: Schedule.max([
50 Schedule.min([
51 Schedule.exponential("500 millis"),
52 Schedule.spaced("2 seconds"),
53 ]),
54 Schedule.recurs(10),
55 ]),
56 }),
57 );
58 return yield* res.text;
59 }).pipe(Effect.orDie);
60
61/**
62 * Pins the `Cloudflare.Workers.Fetch` capability: the caller registers a
63 * service binding for the *target* worker (named by the target's LogicalId,
64 * pointing at the target's workerName) and fetches through it with the
65 * HttpClient-shaped runtime client. Under `alchemy dev` the binding resolves
66 * through the dev registry proxy between two locally-running workerds, so a
67 * green roundtrip proves both the deploy-time registration and the
68 * `env[worker.LogicalId]` runtime lookup line up — the exact pair the
69 * self-binding bug broke.
70 */
71test.provider(
72 "local caller fetches local target through the Fetch service binding",
73 (stack) =>
74 Effect.gen(function* () {
75 yield* stack.destroy();
76
77 const deployed = yield* stack.deploy(
78 Effect.gen(function* () {
79 const target = yield* FetchTargetWorker;
80 const caller = yield* FetchCallerWorker;
81 return { target, caller };
82 }),
83 );
84
85 // Dev markers: both workers serve from the local dev proxy — no cloud
86 // deploy ran.
87 expect(deployed.target.url).toMatch(/^http:\/\/localhost:\d+$/);
88 expect(deployed.caller.url).toMatch(/^http:\/\/localhost:\d+$/);
89
90 // The target answers directly.
91 expect(yield* getTextReady(`${deployed.target.url}/?name=direct`)).toBe(
92 "fetch-binding-target: hello direct",
93 );
94
95 // The roundtrip through the caller's Fetch service binding.
96 expect(yield* getTextReady(`${deployed.caller.url}/?name=alice`)).toBe(
97 "caller saw: fetch-binding-target: hello alice",
98 );
99
100 yield* stack.destroy();
101 }).pipe(logLevel),
102 {
103 tags: ["provider:cloudflare", "provider:cloudflare:worker", "local"],
104 timeout: 180_000,
105 },
106);