| 1 | import * as workers from "@distilled.cloud/cloudflare/workers"; |
| 2 | import { expect } from "alchemy-test"; |
| 3 | import * as Data from "effect/Data"; |
| 4 | import * as Effect from "effect/Effect"; |
| 5 | import * 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 | |
| 14 | class 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 | */ |
| 24 | export 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 | */ |
| 54 | export 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 | */ |
| 80 | export 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 | */ |
| 96 | export 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 | */ |
| 112 | export 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 | */ |
| 128 | export 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 | }); |