1import { retryWorkerScriptNotFound } from "@/Cloudflare/Email/retry";
2import { describe, expect, it } from "alchemy-test";
3import * as Effect from "effect/Effect";
4import * as Fiber from "effect/Fiber";
5import * as TestClock from "effect/testing/TestClock";
6
7// Cloudflare rejects a rule whose `worker` action names a script it cannot see
8// yet with code 2016 / `WorkerScriptNotFound`. Distilled surfaces that as a
9// tagged error; only the tag matters to the retry, so a minimal stand-in keeps
10// this test free of the generated SDK.
11class WorkerScriptNotFound {
12 readonly _tag = "WorkerScriptNotFound";
13 constructor(readonly code = 2016) {}
14}
15class SomethingElse {
16 readonly _tag = "Conflict";
17}
18
19// Fails the first `failures` attempts with `error`, then succeeds.
20const flaky = <E>(failures: number, error: E) => {
21 let attempts = 0;
22 return {
23 attempts: () => attempts,
24 effect: Effect.suspend(() => {
25 attempts++;
26 return attempts <= failures ? Effect.fail(error) : Effect.succeed("ok");
27 }),
28 };
29};
30
31// The live suite (EmailRuleWorkerTarget.test.ts) cannot exercise this window —
32// the Worker provider pre-creates a stub script, so the name already resolves
33// by the time the rule is validated, and that suite passes with the retry
34// removed. These cases inject the failure instead, so they fail if the retry
35// regresses.
36//
37// Advance virtual time to exercise the production backoff without wall-clock waits.
38describe(
39 "retryWorkerScriptNotFound",
40 {
41 tags: ["unit", "provider:cloudflare", "provider:cloudflare:email", "local"],
42 },
43 () => {
44 it.effect(
45 "retries while the script is not yet visible, then succeeds",
46 () =>
47 Effect.gen(function* () {
48 const target = flaky(2, new WorkerScriptNotFound());
49
50 const fiber = yield* retryWorkerScriptNotFound(target.effect).pipe(
51 Effect.forkChild,
52 );
53 yield* TestClock.adjust("1 second");
54 const result = yield* Fiber.join(fiber);
55
56 expect(result).toBe("ok");
57 // Two not-visible-yet failures plus the success.
58 expect(target.attempts()).toBe(3);
59 }),
60 );
61
62 it.effect("does not retry an unrelated failure", () =>
63 Effect.gen(function* () {
64 const target = flaky(99, new SomethingElse());
65
66 const outcome = yield* Effect.result(
67 retryWorkerScriptNotFound(target.effect),
68 );
69
70 expect(outcome._tag).toBe("Failure");
71 // Attempted once and given up — no backoff burned on a permanent error.
72 expect(target.attempts()).toBe(1);
73 }),
74 );
75
76 it.effect(
77 "gives up after a bounded budget and re-raises the original error",
78 () =>
79 Effect.gen(function* () {
80 const error = new WorkerScriptNotFound();
81 // Never recovers: a genuinely missing Worker must still fail and say so
82 // rather than hanging.
83 const target = flaky(Number.MAX_SAFE_INTEGER, error);
84
85 const fiber = yield* Effect.result(
86 retryWorkerScriptNotFound(target.effect),
87 ).pipe(Effect.forkChild);
88 yield* TestClock.adjust("30 seconds");
89 const outcome = yield* Fiber.join(fiber);
90
91 expect(outcome._tag).toBe("Failure");
92 // The error surfaces unchanged, not wrapped in a retry-exhausted error.
93 if (outcome._tag === "Failure") {
94 expect(outcome.failure).toBe(error);
95 }
96 // `times: 8` — the initial attempt plus eight retries, and no more.
97 expect(target.attempts()).toBe(9);
98 }),
99 // Bound regressions even if virtual retries stop making progress.
100 { timeout: 5_000 },
101 );
102 },
103);