1import * as Cloudflare from "@/Cloudflare";
2import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
3import * as Provider from "@/Provider";
4import * as Test from "@/Test/Alchemy";
5import * as workers from "@distilled.cloud/cloudflare/workers";
6import { describe, expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import { MinimumLogLevel } from "effect/References";
9import * as Schedule from "effect/Schedule";
10const { test } = Test.make({ providers: Cloudflare.providers() });
11
12const logLevel = Effect.provideService(
13 MinimumLogLevel,
14 process.env.DEBUG ? "Debug" : "Info",
15);
16
17// Freshly-minted scoped API tokens propagate eventually-consistently across
18// Cloudflare's edge — ride out intermittent 403s on the test's own
19// out-of-band calls by retrying the typed `Forbidden` error.
20const forbiddenRetrySchedule = Schedule.exponential("500 millis");
21
22const getLiveSetting = Effect.gen(function* () {
23 const { accountId } = yield* yield* CloudflareEnvironment;
24 return yield* workers.getAccountSetting({ accountId }).pipe(
25 Effect.retry({
26 while: (e) => e._tag === "Forbidden",
27 schedule: forbiddenRetrySchedule,
28 times: 8,
29 }),
30 );
31});
32
33// `getAccountSetting` is eventually consistent: right after a PUT it can keep
34// echoing the pre-write `greenCompute` for a few seconds (more often under the
35// concurrent suite). Poll the live singleton until it reflects the value we
36// just wrote before asserting, bounded so a genuinely-stuck write fails fast.
37const waitForGreenCompute = (expected: boolean | undefined) =>
38 getLiveSetting.pipe(
39 Effect.flatMap((live) =>
40 (live.greenCompute ?? false) === (expected ?? false)
41 ? Effect.succeed(live)
42 : Effect.fail({ _tag: "GreenComputeNotApplied" as const }),
43 ),
44 Effect.retry({
45 while: (e) => e._tag === "GreenComputeNotApplied",
46 schedule: Schedule.spaced("2 seconds"),
47 times: 15,
48 }),
49 );
50
51// Both cases mutate the same account-level Workers settings singleton; run them serially so they don't corrupt each other's captured baseline under the global concurrent test config.
52describe.sequential(
53 "AccountSetting",
54 { tags: ["provider:cloudflare", "provider:cloudflare:worker", "live"] },
55 () => {
56 test.provider(
57 "flips green compute, updates in place, and restores the baseline on destroy",
58 (stack) =>
59 Effect.gen(function* () {
60 yield* stack.destroy();
61
62 // Observe the account's baseline out-of-band. The singleton always
63 // exists, so the test works relative to whatever the account has.
64 const baseline = yield* getLiveSetting;
65 const baselineGreen = baseline.greenCompute ?? false;
66 const flipped = !baselineGreen;
67
68 // Create — pin green compute to the opposite of the baseline.
69 const created = yield* stack.deploy(
70 Effect.gen(function* () {
71 return yield* Cloudflare.Workers.AccountSetting(
72 "AccountSetting",
73 {
74 greenCompute: flipped,
75 },
76 );
77 }),
78 );
79
80 expect(created.greenCompute).toEqual(flipped);
81 // The pre-management value was captured for restore-on-destroy.
82 expect(created.initialGreenCompute).toEqual(baseline.greenCompute);
83
84 // Out-of-band verify the live account state (poll through edge lag).
85 const live = yield* waitForGreenCompute(flipped);
86 expect(live.greenCompute ?? false).toEqual(flipped);
87
88 // Update in place — set it back to the baseline value. Same
89 // singleton, no replacement; the captured initial value survives.
90 const updated = yield* stack.deploy(
91 Effect.gen(function* () {
92 return yield* Cloudflare.Workers.AccountSetting(
93 "AccountSetting",
94 {
95 greenCompute: baselineGreen,
96 },
97 );
98 }),
99 );
100
101 expect(updated.greenCompute ?? false).toEqual(baselineGreen);
102 expect(updated.initialGreenCompute).toEqual(baseline.greenCompute);
103
104 const liveAfterUpdate = yield* waitForGreenCompute(baselineGreen);
105 expect(liveAfterUpdate.greenCompute ?? false).toEqual(baselineGreen);
106
107 // Destroy — restores the pre-management values (already at the
108 // baseline here, so this also exercises the idempotent no-op path).
109 yield* stack.destroy();
110
111 const restored = yield* waitForGreenCompute(baselineGreen);
112 expect(restored.greenCompute ?? false).toEqual(baselineGreen);
113 expect(restored.defaultUsageModel).toEqual(
114 baseline.defaultUsageModel,
115 );
116 }).pipe(logLevel),
117 { timeout: 120_000 },
118 );
119
120 test.provider(
121 "no-op deploy when desired settings already match the live account",
122 (stack) =>
123 Effect.gen(function* () {
124 yield* stack.destroy();
125
126 const baseline = yield* getLiveSetting;
127
128 // Deploy with the account's current values — reconcile observes no
129 // drift and skips the PUT entirely.
130 const setting = yield* stack.deploy(
131 Effect.gen(function* () {
132 return yield* Cloudflare.Workers.AccountSetting("NoopSetting", {
133 defaultUsageModel: baseline.defaultUsageModel ?? undefined,
134 greenCompute: baseline.greenCompute ?? undefined,
135 });
136 }),
137 );
138
139 expect(setting.defaultUsageModel).toEqual(
140 baseline.defaultUsageModel ?? undefined,
141 );
142 expect(setting.greenCompute).toEqual(
143 baseline.greenCompute ?? undefined,
144 );
145 expect(setting.initialDefaultUsageModel).toEqual(
146 baseline.defaultUsageModel ?? undefined,
147 );
148 expect(setting.initialGreenCompute).toEqual(
149 baseline.greenCompute ?? undefined,
150 );
151
152 // Destroy — initial values match the live state, so restore is a
153 // no-op and the account is left untouched.
154 yield* stack.destroy();
155
156 const after = yield* getLiveSetting;
157 expect(after.defaultUsageModel).toEqual(baseline.defaultUsageModel);
158 expect(after.greenCompute).toEqual(baseline.greenCompute);
159 }).pipe(logLevel),
160 { timeout: 120_000 },
161 );
162
163 // Canonical `list()` test (account-scoped singleton): there is no
164 // collection API for the Workers account settings, so `list()` reads the
165 // single account-wide object and returns it as a one-element array. The
166 // singleton always exists, so no deploy is needed to observe it.
167 test.provider(
168 "list returns the account settings singleton",
169 (stack) =>
170 Effect.gen(function* () {
171 const { accountId } = yield* yield* CloudflareEnvironment;
172
173 const provider = yield* Provider.findProvider(
174 Cloudflare.Workers.AccountSetting,
175 );
176 const all = yield* provider.list();
177
178 expect(all.length).toEqual(1);
179 expect(all[0].accountId).toEqual(accountId);
180 // The element is a full Attributes shape (same as `read` produces).
181 expect(all[0].initialGreenCompute).toEqual(all[0].greenCompute);
182 expect(all[0].initialDefaultUsageModel).toEqual(
183 all[0].defaultUsageModel,
184 );
185
186 // `stack` is unused (the singleton always exists), but keep the destroy
187 // bookend so the harness state stays clean.
188 yield* stack.destroy();
189 }).pipe(logLevel),
190 { timeout: 120_000 },
191 );
192 },
193);