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 { MinimumLogLevel } from "effect/References";
7import * as Schedule from "effect/Schedule";
8import * as HttpClient from "effect/http/HttpClient";
9import type { HttpClientResponse } from "effect/http/HttpClientResponse";
10import Stack from "./fixtures/stack.ts";
11import {
12 LITERAL_SECRET_VALUE,
13 NUMBER_VAR_VALUE,
14 OBJECT_VAR_VALUE,
15 STRING_VAR_VALUE,
16} from "./fixtures/worker.ts";
17/**
18 * `Config.Redacted("CONFIG_SECRET")` resolves against the active
19 * `ConfigProvider` at deploy time. The default provider reads from
20 * `process.env`, so populate it before `beforeAll(deploy(Stack))`
21 * compiles the stack.
22 */
23const CONFIG_SECRET_VALUE = (process.env.CONFIG_SECRET =
24 "sk-from-config-source-xyz");
25
26const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
27 providers: Cloudflare.providers(),
28});
29
30const logLevel = Effect.provideService(
31 MinimumLogLevel,
32 process.env.DEBUG ? "Debug" : "Info",
33);
34
35const stack = beforeAll(deploy(Stack));
36afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack));
37
38/**
39 * Cloudflare's edge takes a few seconds to start serving a fresh
40 * workers.dev URL — initial requests can return Cloudflare's "There is
41 * nothing here yet" 404 page or a 5xx while the script propagates.
42 * Retry until we get a 2xx (or a 4xx that isn't 404 — those are real
43 * client errors and should fail loud).
44 */
45class WorkerNotReady extends Data.TaggedError("WorkerNotReady")<{
46 status: number;
47}> {}
48
49const fetchWhenReady = (url: string) =>
50 Effect.gen(function* () {
51 const client = yield* HttpClient.HttpClient;
52 return yield* client.get(url).pipe(
53 Effect.flatMap((res: HttpClientResponse) =>
54 res.status === 200
55 ? Effect.succeed(res)
56 : Effect.fail(new WorkerNotReady({ status: res.status })),
57 ),
58 Effect.retry({
59 while: (e): e is WorkerNotReady =>
60 e instanceof WorkerNotReady && (e.status === 404 || e.status >= 500),
61 schedule: Schedule.max([
62 Schedule.exponential("500 millis"),
63 Schedule.recurs(20),
64 ]),
65 }),
66 );
67 });
68
69test(
70 "Config.Redacted with literal default round-trips to runtime as Redacted<string>",
71 Effect.gen(function* () {
72 const { url } = yield* stack;
73 expect(url).toBeTypeOf("string");
74
75 const res = yield* fetchWhenReady(`${url}/secret/literal`);
76 expect(res.status).toBe(200);
77
78 const body = (yield* res.json) as { isRedacted: boolean; value: string };
79 expect(body).toEqual({
80 isRedacted: true,
81 value: LITERAL_SECRET_VALUE,
82 });
83 }).pipe(logLevel),
84 {
85 tags: ["provider:cloudflare", "provider:cloudflare:worker", "live"],
86 timeout: 180_000,
87 },
88);
89
90test(
91 "Config.Redacted resolved from env deploys as a secret_text and round-trips",
92 Effect.gen(function* () {
93 const { url } = yield* stack;
94
95 const res = yield* fetchWhenReady(`${url}/secret/config`);
96 expect(res.status).toBe(200);
97
98 const body = (yield* res.json) as { isRedacted: boolean; value: string };
99 expect(body).toEqual({
100 isRedacted: true,
101 value: CONFIG_SECRET_VALUE,
102 });
103 }).pipe(logLevel),
104 {
105 tags: ["provider:cloudflare", "provider:cloudflare:worker", "live"],
106 timeout: 180_000,
107 },
108);
109
110test(
111 "Config.String round-trips to runtime as a string",
112 Effect.gen(function* () {
113 const { url } = yield* stack;
114
115 const res = yield* fetchWhenReady(`${url}/var/string`);
116 expect(res.status).toBe(200);
117
118 const body = (yield* res.json) as { type: string; value: unknown };
119 expect(body).toEqual({ type: "string", value: STRING_VAR_VALUE });
120 }).pipe(logLevel),
121 {
122 tags: ["provider:cloudflare", "provider:cloudflare:worker", "live"],
123 timeout: 180_000,
124 },
125);
126
127test(
128 "Config.Number round-trips to runtime preserving the number type",
129 Effect.gen(function* () {
130 const { url } = yield* stack;
131
132 const res = yield* fetchWhenReady(`${url}/var/number`);
133 expect(res.status).toBe(200);
134
135 const body = (yield* res.json) as { type: string; value: unknown };
136 expect(body).toEqual({ type: "number", value: NUMBER_VAR_VALUE });
137 }).pipe(logLevel),
138 {
139 tags: ["provider:cloudflare", "provider:cloudflare:worker", "live"],
140 timeout: 180_000,
141 },
142);
143
144test(
145 "Config.String with object default round-trips to runtime preserving nested shape",
146 Effect.gen(function* () {
147 const { url } = yield* stack;
148
149 const res = yield* fetchWhenReady(`${url}/var/object`);
150 expect(res.status).toBe(200);
151
152 const body = (yield* res.json) as { type: string; value: unknown };
153 expect(body).toEqual({ type: "object", value: OBJECT_VAR_VALUE });
154 }).pipe(logLevel),
155 {
156 tags: ["provider:cloudflare", "provider:cloudflare:worker", "live"],
157 timeout: 180_000,
158 },
159);