1import { EdgeSessionError } from "@/Cloudflare/EdgeSession.ts";
2import {
3 isTransientBootstrapWriteError,
4 isTransientEdgeSessionError,
5} from "@/Cloudflare/StateStore/State.ts";
6import { makeHttpStateStore } from "@/State/HttpStateStore.ts";
7import type { StateStoreError } from "@/State/State.ts";
8import { describe, expect, it } from "alchemy-test";
9import * as Effect from "effect/Effect";
10import * as Fiber from "effect/Fiber";
11import * as TestClock from "effect/testing/TestClock";
12import * as Layer from "effect/Layer";
13import * as FetchHttpClient from "effect/http/FetchHttpClient";
14
15/**
16 * Predicate coverage for the retry policies added in response to
17 * production failures (Axiom `prod-traces`):
18 *
19 * - bootstrap hoist writes failing on 401 (Secrets Store token binding
20 * not propagated yet — surfaced as EMPTY-message StateStoreErrors),
21 * 404 (route propagation) and 5xx (encryption-key binding
22 * propagation).
23 * - edge-preview secret reads failing with Cloudflare HTML error pages
24 * ("Secret probe returned 400/502: <!DOCTYPE html>..."), session
25 * creation flakes and transport errors ("fetch failed").
26 *
27 * `isTransientBootstrapWriteError` inspects the safe HTTP metadata on the
28 * `StateStoreError` the real HTTP client produces, so each case drives
29 * an actual `makeHttpStateStore().set` against a stubbed fetch and
30 * feeds the resulting failure into the predicate.
31 */
32
33/** Minimal fetch signature — Bun's `typeof fetch` also demands `preconnect`. */
34type FetchStub = (
35 input: string | URL | Request,
36 init?: RequestInit,
37) => Promise<Response>;
38
39const stubHttpClient = (stub: FetchStub) =>
40 FetchHttpClient.layer.pipe(
41 Layer.provide(
42 Layer.succeed(FetchHttpClient.Fetch, stub as typeof globalThis.fetch),
43 ),
44 );
45
46/** Run a state-store write against a stubbed transport, return its failure. */
47const failingWrite = (stub: FetchStub): Effect.Effect<StateStoreError> =>
48 Effect.gen(function* () {
49 const store = yield* makeHttpStateStore({
50 url: "https://state-store.test",
51 authToken: "token",
52 id: "test-http",
53 });
54 return yield* store
55 .set({
56 stack: "s",
57 stage: "dev",
58 fqn: "stack/scope/a",
59 value: { hello: "world" } as never,
60 })
61 .pipe(Effect.flip);
62 }).pipe(Effect.provide(stubHttpClient(stub)), Effect.orDie);
63
64describe(
65 "isTransientBootstrapWriteError",
66 {
67 tags: [
68 "unit",
69 "provider:cloudflare",
70 "provider:cloudflare:statestore",
71 "local",
72 ],
73 },
74 () => {
75 it.effect(
76 "retries 401 Unauthorized (token-binding propagation) but not other 4xx",
77 () =>
78 Effect.gen(function* () {
79 const unauthorized = yield* failingWrite(
80 async () => new Response(null, { status: 401 }),
81 );
82 expect(isTransientBootstrapWriteError(unauthorized)).toBe(true);
83
84 const badRequest = yield* failingWrite(
85 async () => new Response("no", { status: 400 }),
86 );
87 expect(isTransientBootstrapWriteError(badRequest)).toBe(false);
88 }),
89 );
90
91 it.effect(
92 "retries 404 (route propagation), 5xx (binding propagation) and transport failures",
93 () =>
94 Effect.gen(function* () {
95 for (const stub of [
96 async () => new Response("not found", { status: 404 }),
97 async () => new Response("secret unavailable", { status: 500 }),
98 async () => {
99 throw new TypeError("fetch failed");
100 },
101 ]) {
102 const fiber = yield* failingWrite(stub).pipe(Effect.forkChild);
103 yield* TestClock.adjust("30 seconds");
104 const error = yield* Fiber.join(fiber);
105 expect(isTransientBootstrapWriteError(error)).toBe(true);
106 }
107 }),
108 5_000,
109 );
110
111 it("does not retry errors without HTTP failure metadata", () => {
112 expect(isTransientBootstrapWriteError({ http: undefined })).toBe(false);
113 });
114 },
115);
116
117describe(
118 "isTransientEdgeSessionError",
119 {
120 tags: [
121 "unit",
122 "provider:cloudflare",
123 "provider:cloudflare:statestore",
124 "local",
125 ],
126 },
127 () => {
128 it("retries non-200 secret-probe responses (Cloudflare HTML error pages)", () => {
129 const error = new EdgeSessionError({
130 message: 'Secret probe returned 400: <!DOCTYPE html>\n<html class="no-',
131 });
132 expect(isTransientEdgeSessionError(error)).toBe(true);
133 });
134
135 it("retries session-creation failures with transient causes", () => {
136 const error = new EdgeSessionError({
137 message: "Failed to create edge preview session",
138 cause: new TypeError("fetch failed"),
139 });
140 expect(isTransientEdgeSessionError(error)).toBe(true);
141 });
142
143 it("does not retry permanent auth causes", () => {
144 for (const tag of ["Unauthorized", "Forbidden", "InvalidRoute"]) {
145 const cause = Object.assign(new Error("denied"), { _tag: tag });
146 const error = new EdgeSessionError({
147 message: "Failed to create edge preview session",
148 cause,
149 });
150 expect(isTransientEdgeSessionError(error)).toBe(false);
151 }
152 });
153
154 it("ignores non-EdgeSessionError values", () => {
155 expect(isTransientEdgeSessionError(new Error("boom"))).toBe(false);
156 });
157 },
158);