1import {
2 encodeDurableObjectTags,
3 getDurableObjectTagMap,
4 normalizeStateDomains,
5 resolveWorkerDomain,
6 resolveWorkerDomainZone,
7 resolveWorkersDev,
8 shouldRecreateWorkerDomainAttachment,
9 shouldObserveWorkerCrons,
10 shouldObserveWorkerDomains,
11 shouldObserveWorkerRoutes,
12 stateCustomDomains,
13 stateWorkerDomain,
14} from "@/Cloudflare/Workers/WorkerProvider";
15import { describe, expect, test } from "alchemy-test";
16import * as Effect from "effect/Effect";
17import * as Result from "effect/Result";
18
19describe(
20 "WorkerProvider",
21 {
22 tags: [
23 "unit",
24 "provider:cloudflare",
25 "provider:cloudflare:worker",
26 "local",
27 ],
28 },
29 () => {
30 describe("normalizeStateDomains", () => {
31 // Worker state has gone through three generations: <= beta.44 stored each
32 // custom domain as a `{ id, hostname, zoneId }` object; beta.45 – beta.57
33 // stored `https://<hostname>` URL strings (with the workers.dev URL mixed
34 // in); the current format stores bare hostnames aligned with `allUrls`.
35 // The diff path reads all three without throwing (#546).
36 test("coerces legacy domain objects to hostnames", () => {
37 expect(
38 normalizeStateDomains([
39 { id: "abc", hostname: "metrics.example.com", zoneId: "z1" },
40 ]),
41 ).toEqual(["metrics.example.com"]);
42 });
43
44 test("coerces legacy https:// URL strings to hostnames", () => {
45 expect(
46 normalizeStateDomains([
47 "https://app.example.com",
48 "https://my-worker.acct.workers.dev",
49 ]),
50 ).toEqual(["app.example.com", "my-worker.acct.workers.dev"]);
51 });
52
53 test("leaves current-format hostnames untouched", () => {
54 expect(normalizeStateDomains(["app.example.com", "localhost"])).toEqual(
55 ["app.example.com", "localhost"],
56 );
57 });
58
59 test("drops entries that fit no state generation", () => {
60 expect(
61 normalizeStateDomains([
62 "https://keep.example.com",
63 { id: "no-hostname" },
64 { hostname: 123 },
65 null,
66 42,
67 "",
68 ]),
69 ).toEqual(["keep.example.com"]);
70 });
71
72 test("returns an empty array for undefined state", () => {
73 expect(normalizeStateDomains(undefined)).toEqual([]);
74 });
75 });
76
77 describe("stateCustomDomains", () => {
78 test("excludes workers.dev, preview, and local-dev entries", () => {
79 expect(
80 stateCustomDomains([
81 "my-worker.acct.workers.dev",
82 "0a1b2c3d-my-worker.acct.workers.dev",
83 "localhost",
84 "192.168.0.12",
85 "app.example.com",
86 ]),
87 ).toEqual(["app.example.com"]);
88 });
89
90 test("reads legacy URL-string state", () => {
91 expect(
92 stateCustomDomains([
93 "https://app.example.com",
94 "https://my-worker.acct.workers.dev",
95 ]),
96 ).toEqual(["app.example.com"]);
97 });
98 });
99
100 describe("resolveWorkerDomain", () => {
101 const resolve = (domain: Parameters<typeof resolveWorkerDomain>[0]) =>
102 Effect.runSync(resolveWorkerDomain(domain));
103
104 test("string shorthand resolves to { name }", () => {
105 expect(resolve("app.example.com")).toEqual({
106 name: "app.example.com",
107 aliases: [],
108 redirects: [],
109 });
110 });
111
112 test("previews: true is preserved", () => {
113 expect(resolve({ name: "app.example.com", previews: true })).toEqual({
114 name: "app.example.com",
115 aliases: [],
116 redirects: [],
117 previews: true,
118 });
119 });
120
121 test("object form dedupes and punycodes hostnames", () => {
122 expect(
123 resolve({
124 name: "📦.example.com",
125 aliases: ["www.example.com", "www.example.com"],
126 redirects: ["old.example.com"],
127 }),
128 ).toEqual({
129 name: "xn--cu8h.example.com",
130 aliases: ["www.example.com"],
131 redirects: ["old.example.com"],
132 });
133 });
134
135 test("undefined and null resolve to no domain", () => {
136 expect(resolve(undefined)).toBeUndefined();
137 expect(resolve(null)).toBeUndefined();
138 });
139
140 // Pre-redesign props stored `domain: string[]` — persisted `olds` can
141 // still hand that shape to read's classification. `domains[0]` was the
142 // primary hostname back then, so it maps to `name`; the rest to
143 // aliases. A legacy empty array was the explicit detach-all.
144 test("legacy string[] maps to name + aliases", () => {
145 expect(resolve(["app.example.com", "www.example.com"])).toEqual({
146 name: "app.example.com",
147 aliases: ["www.example.com"],
148 redirects: [],
149 });
150 expect(resolve([])).toBeUndefined();
151 });
152
153 test("a hostname in more than one role is a typed error", () => {
154 const result = Effect.runSync(
155 Effect.result(
156 resolveWorkerDomain({
157 name: "app.example.com",
158 aliases: ["app.example.com"],
159 }),
160 ),
161 );
162 expect(Result.isFailure(result)).toBe(true);
163 if (Result.isFailure(result)) {
164 expect(result.failure._tag).toEqual("WorkerDomainConfigError");
165 }
166 });
167
168 test("carries zoneId / zone / zoneName pins", () => {
169 const zoneId = "0123456789abcdef0123456789abcdef";
170 expect(
171 resolve({
172 name: "app.example.com",
173 zoneId,
174 }),
175 ).toEqual({
176 name: "app.example.com",
177 aliases: [],
178 redirects: [],
179 zone: zoneId,
180 });
181 expect(
182 resolve({
183 name: "app.example.com",
184 zoneName: "example.com",
185 }),
186 ).toEqual({
187 name: "app.example.com",
188 aliases: [],
189 redirects: [],
190 zone: "example.com",
191 });
192 expect(
193 resolveWorkerDomainZone({
194 zoneId,
195 zoneName: "ignored.com",
196 zone: "also-ignored.com",
197 }),
198 ).toEqual(zoneId);
199 });
200
201 test("recreates an existing attachment only for a changed explicit zone", () => {
202 expect(
203 shouldRecreateWorkerDomainAttachment("live-zone", undefined),
204 ).toBe(false);
205 expect(
206 shouldRecreateWorkerDomainAttachment("live-zone", "live-zone"),
207 ).toBe(false);
208 expect(
209 shouldRecreateWorkerDomainAttachment("live-zone", "desired-zone"),
210 ).toBe(true);
211 });
212 });
213
214 describe("stateWorkerDomain", () => {
215 test("passes the current-format domain attribute through", () => {
216 expect(
217 stateWorkerDomain({
218 domain: {
219 name: "app.example.com",
220 aliases: ["www.example.com"],
221 redirects: ["old.example.com"],
222 },
223 }),
224 ).toEqual({
225 name: "app.example.com",
226 aliases: ["www.example.com"],
227 redirects: ["old.example.com"],
228 });
229 expect(
230 stateWorkerDomain({
231 domain: {
232 name: "app.example.com",
233 aliases: [],
234 redirects: [],
235 zoneId: "0123456789abcdef0123456789abcdef",
236 },
237 }),
238 ).toEqual({
239 name: "app.example.com",
240 aliases: [],
241 redirects: [],
242 zone: "0123456789abcdef0123456789abcdef",
243 });
244 expect(
245 stateWorkerDomain({
246 domain: {
247 name: "app.example.com",
248 aliases: [],
249 redirects: [],
250 zoneId: 42,
251 zoneName: "example.com",
252 },
253 }),
254 ).toEqual({
255 name: "app.example.com",
256 aliases: [],
257 redirects: [],
258 zone: "example.com",
259 });
260 });
261
262 test("derives name + aliases from legacy URL-string domains state", () => {
263 expect(
264 stateWorkerDomain({
265 domains: [
266 "https://app.example.com",
267 "https://www.example.com",
268 "https://my-worker.acct.workers.dev",
269 ],
270 }),
271 ).toEqual({
272 name: "app.example.com",
273 aliases: ["www.example.com"],
274 redirects: [],
275 });
276 });
277
278 test("derives from <= beta.44 domain objects", () => {
279 expect(
280 stateWorkerDomain({
281 domains: [{ id: "abc", hostname: "app.example.com", zoneId: "z" }],
282 }),
283 ).toEqual({
284 name: "app.example.com",
285 aliases: [],
286 redirects: [],
287 });
288 });
289
290 test("workers.dev-only and empty state resolve to no domain", () => {
291 expect(
292 stateWorkerDomain({
293 domains: ["https://my-worker.acct.workers.dev"],
294 }),
295 ).toBeUndefined();
296 expect(stateWorkerDomain({})).toBeUndefined();
297 expect(stateWorkerDomain(undefined)).toBeUndefined();
298 });
299 });
300
301 describe("resolveWorkersDev", () => {
302 test("defaults to the full workers.dev behavior", () => {
303 expect(resolveWorkersDev(undefined)).toEqual({
304 enabled: true,
305 previewsEnabled: true,
306 });
307 expect(resolveWorkersDev(true)).toEqual({
308 enabled: true,
309 previewsEnabled: true,
310 });
311 });
312
313 test("false disables both toggles", () => {
314 expect(resolveWorkersDev(false)).toEqual({
315 enabled: false,
316 previewsEnabled: false,
317 });
318 });
319
320 test("object form fills unset toggles with true", () => {
321 expect(resolveWorkersDev({})).toEqual({
322 enabled: true,
323 previewsEnabled: true,
324 });
325 expect(resolveWorkersDev({ enabled: false })).toEqual({
326 enabled: false,
327 previewsEnabled: true,
328 });
329 expect(resolveWorkersDev({ previewsEnabled: false })).toEqual({
330 enabled: true,
331 previewsEnabled: false,
332 });
333 expect(
334 resolveWorkersDev({ enabled: false, previewsEnabled: true }),
335 ).toEqual({
336 enabled: false,
337 previewsEnabled: true,
338 });
339 });
340 });
341
342 // Cloudflare allows at most 10 tags per worker and 1024 bytes per tag, so
343 // the DO logical-id→class mapping is packed into `alchemy:dos:` tags
344 // instead of one `alchemy:do:` tag per binding (#811).
345 describe("durable object tags", () => {
346 test("packs all mappings into a single tag", () => {
347 expect(
348 encodeDurableObjectTags([
349 { logicalId: "Counter", className: "Counter" },
350 { logicalId: "Meter", className: "MeterV2" },
351 ]),
352 ).toEqual(["alchemy:dos:Counter;Meter=MeterV2"]);
353 });
354
355 test("elides the class name when it equals the logical id", () => {
356 expect(
357 encodeDurableObjectTags([{ logicalId: "A", className: "A" }]),
358 ).toEqual(["alchemy:dos:A"]);
359 });
360
361 test("output is deterministic regardless of input order", () => {
362 const forward = encodeDurableObjectTags([
363 { logicalId: "A", className: "A1" },
364 { logicalId: "B", className: "B1" },
365 ]);
366 const reverse = encodeDurableObjectTags([
367 { logicalId: "B", className: "B1" },
368 { logicalId: "A", className: "A1" },
369 ]);
370 expect(forward).toEqual(reverse);
371 });
372
373 test("round-trips through the parser", () => {
374 const mappings = Array.from({ length: 25 }, (_, i) => ({
375 logicalId: `binding-${i}`,
376 className: `ClassName${i}`,
377 }));
378 expect(
379 getDurableObjectTagMap(encodeDurableObjectTags(mappings)),
380 ).toEqual(
381 Object.fromEntries(
382 mappings.map(({ logicalId, className }) => [logicalId, className]),
383 ),
384 );
385 });
386
387 test("escapes separators and Cloudflare-forbidden characters", () => {
388 const mappings = [
389 { logicalId: "a;b", className: "C1" },
390 { logicalId: "a=b", className: "C2" },
391 { logicalId: "a,b&c", className: "C3" },
392 { logicalId: "a:b", className: "C4" },
393 ];
394 const tags = encodeDurableObjectTags(mappings);
395 for (const tag of tags) {
396 expect(tag).not.toContain(",");
397 expect(tag).not.toContain("&");
398 }
399 expect(getDurableObjectTagMap(tags)).toEqual({
400 "a;b": "C1",
401 "a=b": "C2",
402 "a,b&c": "C3",
403 "a:b": "C4",
404 });
405 });
406
407 test("splits into multiple tags at the 1024-byte limit", () => {
408 const mappings = Array.from({ length: 100 }, (_, i) => ({
409 logicalId: `some-durable-object-binding-${i}`,
410 className: `SomeDurableObjectClassName${i}`,
411 }));
412 const tags = encodeDurableObjectTags(mappings);
413 expect(tags.length).toBeGreaterThan(1);
414 for (const tag of tags) {
415 expect(tag.length).toBeLessThanOrEqual(1024);
416 expect(tag.startsWith("alchemy:dos:")).toBe(true);
417 }
418 expect(getDurableObjectTagMap(tags)).toEqual(
419 Object.fromEntries(
420 mappings.map(({ logicalId, className }) => [logicalId, className]),
421 ),
422 );
423 });
424
425 test("unicode identifiers stay within the byte limit", () => {
426 const mappings = Array.from({ length: 40 }, (_, i) => ({
427 logicalId: `对象-${i}`,
428 className: `Class_${i}`,
429 }));
430 const tags = encodeDurableObjectTags(mappings);
431 const encoder = new TextEncoder();
432 for (const tag of tags) {
433 expect(encoder.encode(tag).length).toBeLessThanOrEqual(1024);
434 }
435 expect(getDurableObjectTagMap(tags)).toEqual(
436 Object.fromEntries(
437 mappings.map(({ logicalId, className }) => [logicalId, className]),
438 ),
439 );
440 });
441
442 test("parses legacy per-DO alchemy:do: tags", () => {
443 expect(
444 getDurableObjectTagMap([
445 "alchemy:stack:app",
446 "alchemy:do:Counter:CounterV2",
447 "alchemy:do:Meter:Meter",
448 "user-tag",
449 ]),
450 ).toEqual({ Counter: "CounterV2", Meter: "Meter" });
451 });
452
453 test("packed entries win over legacy entries for the same logical id", () => {
454 expect(
455 getDurableObjectTagMap([
456 "alchemy:do:Counter:OldClass",
457 "alchemy:dos:Counter=NewClass",
458 ]),
459 ).toEqual({ Counter: "NewClass" });
460 });
461
462 test("returns an empty map when no DO tags are present", () => {
463 expect(getDurableObjectTagMap(["alchemy:stack:app", "user"])).toEqual(
464 {},
465 );
466 });
467 });
468
469 // Worker read used to always fan out listDomains + account-wide route
470 // discovery + getScriptSchedule, even for a plain workers.dev Worker. That
471 // stampeded GET /accounts/{id}/workers/subdomain neighbors into 429/code 971
472 // (#926). These helpers gate those observations on whether Alchemy manages
473 // the surface.
474 describe("shouldObserveWorkerDomains", () => {
475 test("skips when neither props nor state manage custom domains", () => {
476 expect(
477 shouldObserveWorkerDomains(
478 {},
479 {
480 domains: ["https://my-worker.acct.workers.dev"],
481 },
482 ),
483 ).toBe(false);
484 expect(shouldObserveWorkerDomains(undefined, undefined)).toBe(false);
485 });
486
487 test("observes when domain prop is present, including null", () => {
488 expect(shouldObserveWorkerDomains({ domain: null }, undefined)).toBe(
489 true,
490 );
491 expect(
492 shouldObserveWorkerDomains({ domain: "app.example.com" }, undefined),
493 ).toBe(true);
494 });
495
496 test("observes when prior state has non-workers.dev domains", () => {
497 expect(
498 shouldObserveWorkerDomains(
499 {},
500 {
501 domains: [
502 "https://app.example.com",
503 "https://my-worker.acct.workers.dev",
504 ],
505 },
506 ),
507 ).toBe(true);
508 });
509 });
510
511 describe("shouldObserveWorkerRoutes", () => {
512 test("skips when neither props nor state manage routes", () => {
513 expect(shouldObserveWorkerRoutes({}, { routes: [] })).toBe(false);
514 expect(shouldObserveWorkerRoutes(undefined, undefined)).toBe(false);
515 });
516
517 test("observes when routes prop is present, including empty array", () => {
518 expect(shouldObserveWorkerRoutes({ routes: [] }, undefined)).toBe(true);
519 expect(
520 shouldObserveWorkerRoutes(
521 { routes: [{ pattern: "example.com/*" }] },
522 undefined,
523 ),
524 ).toBe(true);
525 });
526
527 test("observes when prior state has routes", () => {
528 expect(
529 shouldObserveWorkerRoutes(
530 {},
531 {
532 routes: [{ id: "r1", pattern: "example.com/*", zoneId: "z1" }],
533 },
534 ),
535 ).toBe(true);
536 });
537 });
538
539 describe("shouldObserveWorkerCrons", () => {
540 test("skips when neither props nor state manage crons", () => {
541 expect(shouldObserveWorkerCrons({}, { crons: [] })).toBe(false);
542 expect(shouldObserveWorkerCrons(undefined, undefined)).toBe(false);
543 });
544
545 test("observes when crons prop is present, including empty array", () => {
546 expect(shouldObserveWorkerCrons({ crons: [] }, undefined)).toBe(true);
547 expect(
548 shouldObserveWorkerCrons({ crons: ["0 * * * *"] }, undefined),
549 ).toBe(true);
550 });
551
552 test("observes when prior state has crons (e.g. Effect-native cron())", () => {
553 expect(shouldObserveWorkerCrons({}, { crons: ["0 * * * *"] })).toBe(
554 true,
555 );
556 });
557 });
558 },
559);