1import * as workers from "@distilled.cloud/cloudflare/workers";
2import { expect } from "alchemy-test";
3import * as Data from "effect/Data";
4import * as Effect from "effect/Effect";
5import * as Schedule from "effect/Schedule";
6
7/**
8 * Test helpers for asserting against Cloudflare Worker state via the
9 * official workers SDK. These wrap the common "exists / get tags / wait
10 * for delete" patterns used across `Worker.test.ts` and the website
11 * tests so callers don't reimplement the retry plumbing each time.
12 */
13
14class WorkerStillExists extends Data.TaggedError("WorkerStillExists") {}
15
16/**
17 * Poll Cloudflare's `getScriptSubdomain` until it reports `expected`,
18 * then assert. Cloudflare's subdomain toggle is eventually consistent
19 * with respect to a freshly-completed `putScript` /
20 * `createScriptSubdomain` call, so a same-tick read may still return
21 * the previous value for a second or two. Retries indefinitely with a
22 * fixed cadence; the surrounding test's own timeout bounds the wait.
23 */
24export const expectWorkersDevSubdomain = Effect.fn(function* (
25 workerName: string,
26 accountId: string,
27 expected: boolean,
28) {
29 const final = yield* workers
30 .getScriptSubdomain({ accountId, scriptName: workerName })
31 .pipe(
32 Effect.flatMap((s) =>
33 s.enabled === expected
34 ? Effect.succeed(s)
35 : Effect.fail(
36 `subdomain not yet ${expected ? "enabled" : "disabled"}`,
37 ),
38 ),
39 Effect.tapError(Effect.logInfo),
40 Effect.retry({ schedule: Schedule.spaced("500 millis") }),
41 );
42 expect(final.enabled).toBe(expected);
43});
44
45/**
46 * Poll Cloudflare's `getScriptSubdomain` until both `enabled` and
47 * `previewsEnabled` match the expected values, then assert. Like
48 * `expectWorkersDevSubdomain`, the subdomain toggle is eventually
49 * consistent with respect to a freshly-completed `putScript` /
50 * `createScriptSubdomain` call, so a same-tick read may still return the
51 * previous value for a second or two. Retries indefinitely with a fixed
52 * cadence; the surrounding test's own timeout bounds the wait.
53 */
54export const expectWorkersDevPreviews = Effect.fn(function* (
55 workerName: string,
56 accountId: string,
57 expected: { enabled: boolean; previewsEnabled: boolean },
58) {
59 const final = yield* workers
60 .getScriptSubdomain({ accountId, scriptName: workerName })
61 .pipe(
62 Effect.flatMap((s) =>
63 s.enabled === expected.enabled &&
64 s.previewsEnabled === expected.previewsEnabled
65 ? Effect.succeed(s)
66 : Effect.fail(
67 `subdomain not yet { enabled: ${expected.enabled}, previewsEnabled: ${expected.previewsEnabled} }`,
68 ),
69 ),
70 Effect.tapError(Effect.logInfo),
71 Effect.retry({ schedule: Schedule.spaced("500 millis") }),
72 );
73 expect(final).toEqual(expected);
74});
75
76/**
77 * Look up a worker by name. Returns `undefined` if no script with that
78 * name exists in the account.
79 */
80export const findWorker = Effect.fn(function* (
81 workerName: string,
82 accountId: string,
83) {
84 const matches = yield* workers.searchScript({
85 accountId,
86 name: workerName,
87 });
88 return matches.find((worker) => worker.scriptName === workerName);
89});
90
91/**
92 * Read the tags currently attached to a worker's live script settings.
93 * Useful for asserting `alchemy:stack:*` / `alchemy:stage:*` ownership
94 * tags without round-tripping through `searchScript`.
95 */
96export const getWorkerTags = Effect.fn(function* (
97 workerName: string,
98 accountId: string,
99) {
100 const settings = yield* workers.getScriptScriptAndVersionSetting({
101 accountId,
102 scriptName: workerName,
103 });
104 return settings.tags ?? [];
105});
106
107/**
108 * Asserts that a worker exists on Cloudflare. Use as a deploy
109 * checkpoint that doesn't take a propagation dependency on the
110 * workers.dev subdomain.
111 */
112export const expectWorkerExists = Effect.fn(function* (
113 workerName: string,
114 accountId: string,
115) {
116 const settings = yield* workers.getScriptScriptAndVersionSetting({
117 accountId,
118 scriptName: workerName,
119 });
120 expect(settings).toBeDefined();
121});
122
123/**
124 * Block until Cloudflare reports the worker is gone. Uses exponential
125 * backoff; safe to call after `stack.destroy()` to ensure subsequent
126 * tests in the same file don't observe the about-to-be-deleted worker.
127 */
128export const waitForWorkerToBeDeleted = Effect.fn(function* (
129 workerName: string,
130 accountId: string,
131) {
132 yield* workers.getScript({ accountId, scriptName: workerName }).pipe(
133 Effect.flatMap(() => Effect.fail(new WorkerStillExists())),
134 Effect.retry({
135 while: (e): e is WorkerStillExists => e instanceof WorkerStillExists,
136 schedule: Schedule.max([Schedule.exponential(100), Schedule.recurs(20)]),
137 }),
138 // Deletion propagates in stages: getScript can briefly report the
139 // worker as existing-but-empty ("has no versions") before the 10007
140 // not-found lands. Both mean the worker is gone for our purposes.
141 Effect.catchTag(
142 ["WorkerNotFound", "WorkerHasNoVersions"],
143 () => Effect.void,
144 ),
145 );
146});