1import * as Cloudflare from "@/Cloudflare";
2import * as Test from "@/Test/Alchemy";
3import { expect } from "alchemy-test";
4import * as Data from "effect/Data";
5import * as Effect from "effect/Effect";
6import * as Schedule from "effect/Schedule";
7import * as HttpClient from "effect/http/HttpClient";
8import Stack from "./fixtures/stack.ts";
9
10// The fixture stack provisions its own Flagship app via the FlagshipApp
11// resource and binds it to both workers. Each fixture worker evaluates a
12// flag and returns the result; with no matching flag configured, Flagship
13// falls back to the provided defaults, so the assertions below hold whether
14// or not the app has flags defined.
15const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
16 providers: Cloudflare.providers(),
17});
18
19class WorkerNotReady extends Data.TaggedError("WorkerNotReady")<{
20 status: number;
21 body: string;
22}> {}
23
24// A fresh workers.dev URL needs a few seconds of edge propagation before it
25// serves real traffic. During that window the request can fail in several
26// ways: the DNS name doesn't resolve yet (transport `RequestError`), the
27// edge returns Cloudflare's placeholder page (4xx/5xx), or a 200 briefly
28// carries a non-JSON body (parse error). Retry through ALL of these — the
29// same defensiveness the Workers/*.test.ts suites use — until the worker
30// answers with a real 200 JSON body. `WorkerNotReady` keeps the status/body
31// on the final surfaced error so a genuine failure isn't masked.
32const getJson = (url: string) =>
33 HttpClient.get(url).pipe(
34 Effect.flatMap((res) =>
35 res.status === 200
36 ? res.json
37 : res.text.pipe(
38 Effect.flatMap((body) =>
39 Effect.fail(new WorkerNotReady({ status: res.status, body })),
40 ),
41 ),
42 ),
43 Effect.retry({
44 schedule: Schedule.max([
45 Schedule.min([
46 Schedule.exponential("500 millis"),
47 Schedule.spaced("5 seconds"),
48 ]),
49 Schedule.recurs(30),
50 ]),
51 }),
52 );
53
54const stack = beforeAll(deploy(Stack));
55afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack));
56
57test(
58 "async worker evaluates a boolean flag via env Flagship binding",
59 Effect.gen(function* () {
60 const { asyncWorkerUrl } = yield* stack;
61 const result = yield* getJson(`${asyncWorkerUrl}/bool`);
62 expect(result).toMatchObject({ mode: "async", enabled: false });
63 }),
64 {
65 tags: [
66 "provider:cloudflare",
67 "provider:cloudflare:flagship",
68 "provider:cloudflare:worker",
69 "live",
70 ],
71 timeout: 240_000,
72 },
73);
74
75test(
76 "async worker returns fallback details for a nonexistent flag",
77 Effect.gen(function* () {
78 const { asyncWorkerUrl } = yield* stack;
79 const result = (yield* getJson(`${asyncWorkerUrl}/details`)) as {
80 mode: string;
81 details: { flagKey: string; value: string };
82 };
83 expect(result.mode).toBe("async");
84 expect(result.details.flagKey).toBe("nonexistent-flag");
85 expect(result.details.value).toBe("fallback");
86 }),
87 {
88 tags: [
89 "provider:cloudflare",
90 "provider:cloudflare:flagship",
91 "provider:cloudflare:worker",
92 "live",
93 ],
94 timeout: 240_000,
95 },
96);
97
98test(
99 "effect worker evaluates a boolean flag via FlagshipApp.bind",
100 Effect.gen(function* () {
101 const { effectWorkerUrl } = yield* stack;
102 const result = yield* getJson(`${effectWorkerUrl}/bool`);
103 expect(result).toMatchObject({ mode: "effect", enabled: false });
104 }),
105 {
106 tags: [
107 "provider:cloudflare",
108 "provider:cloudflare:flagship",
109 "provider:cloudflare:worker",
110 "live",
111 ],
112 timeout: 240_000,
113 },
114);
115
116test(
117 "effect worker returns fallback details for a nonexistent flag",
118 Effect.gen(function* () {
119 const { effectWorkerUrl } = yield* stack;
120 const result = (yield* getJson(`${effectWorkerUrl}/details`)) as {
121 mode: string;
122 details: { flagKey: string; value: string };
123 };
124 expect(result.mode).toBe("effect");
125 expect(result.details.flagKey).toBe("nonexistent-flag");
126 expect(result.details.value).toBe("fallback");
127 }),
128 {
129 tags: [
130 "provider:cloudflare",
131 "provider:cloudflare:flagship",
132 "provider:cloudflare:worker",
133 "live",
134 ],
135 timeout: 240_000,
136 },
137);