1import * as Cloudflare from "@/Cloudflare";
2import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
3import { generateLocalId, isLiveId } from "@/Cloudflare/LocalRuntime";
4import * as Provider from "@/Provider";
5import { State } from "@/State";
6import type { CreatedResourceState } from "@/State/ResourceState";
7import * as Test from "@/Test/Alchemy";
8import * as queues from "@distilled.cloud/cloudflare/queues";
9import { expect } from "alchemy-test";
10import * as Effect from "effect/Effect";
11import * as Exit from "effect/Exit";
12import { MinimumLogLevel } from "effect/References";
13import * as Schedule from "effect/Schedule";
14import * as pathe from "pathe";
15
16const { test } = Test.make({ providers: Cloudflare.providers() });
17
18const logLevel = Effect.provideService(
19 MinimumLogLevel,
20 process.env.DEBUG ? "Debug" : "Info",
21);
22
23const main = pathe.resolve(import.meta.dirname, "consumer-worker.ts");
24
25/**
26 * Lifecycle: create the consumer, change settings (in-place update),
27 * change script (replace), then destroy.
28 *
29 * Verifies the diff matrix end-to-end and that updateConsumer is
30 * issued every reconcile (so settings drift gets corrected even when
31 * `olds.settings` matches `news.settings`).
32 */
33test.provider(
34 "create, update settings, replace script, delete",
35 (stack) =>
36 Effect.gen(function* () {
37 const env = yield* CloudflareEnvironment;
38 const { accountId } = yield* env;
39
40 yield* stack.destroy();
41
42 const initial = yield* stack.deploy(
43 Effect.gen(function* () {
44 const queue = yield* Cloudflare.Queues.Queue("Q");
45 const workerA = yield* Cloudflare.Worker("WorkerA", {
46 main,
47 compatibility: { date: "2024-01-01" },
48 });
49 const consumer = yield* Cloudflare.Queues.Consumer("Consumer", {
50 queueId: queue.queueId,
51 scriptName: workerA.workerName,
52 settings: { batchSize: 5, maxRetries: 3 },
53 });
54 return { queue, workerA, consumer };
55 }),
56 );
57
58 expect(initial.consumer.consumerId).toBeTypeOf("string");
59 expect(initial.consumer.scriptName).toEqual(initial.workerA.workerName);
60
61 const live = yield* queues.getConsumer({
62 accountId,
63 queueId: initial.queue.queueId,
64 consumerId: initial.consumer.consumerId,
65 });
66 expect("scriptName" in live ? live.scriptName : undefined).toEqual(
67 initial.workerA.workerName,
68 );
69
70 // Settings-only change is an update, not a replace — consumerId
71 // must remain stable.
72 const updated = yield* stack.deploy(
73 Effect.gen(function* () {
74 const queue = yield* Cloudflare.Queues.Queue("Q");
75 const workerA = yield* Cloudflare.Worker("WorkerA", {
76 main,
77 compatibility: { date: "2024-01-01" },
78 });
79 const consumer = yield* Cloudflare.Queues.Consumer("Consumer", {
80 queueId: queue.queueId,
81 scriptName: workerA.workerName,
82 settings: { batchSize: 25, maxRetries: 7 },
83 });
84 return { queue, workerA, consumer };
85 }),
86 );
87
88 expect(updated.consumer.consumerId).toEqual(initial.consumer.consumerId);
89
90 const liveUpdated = yield* queues.getConsumer({
91 accountId,
92 queueId: updated.queue.queueId,
93 consumerId: updated.consumer.consumerId,
94 });
95 expect(liveUpdated.settings?.batchSize).toEqual(25);
96 expect(liveUpdated.settings?.maxRetries).toEqual(7);
97
98 // Script change is a delete-first replace: Cloudflare's
99 // updateConsumer silently ignores script_name on an existing
100 // consumer, and the platform allows only one Worker consumer
101 // per queue, so the engine must tear the old consumer down
102 // before creating the new one. WorkerA stays yielded across
103 // the deploy so it isn't garbage-collected mid-replace and
104 // race the Worker.delete with Cloudflare's queue↔script sync.
105 const replaced = yield* stack.deploy(
106 Effect.gen(function* () {
107 const queue = yield* Cloudflare.Queues.Queue("Q");
108 yield* Cloudflare.Worker("WorkerA", {
109 main,
110 compatibility: { date: "2024-01-01" },
111 });
112 const workerB = yield* Cloudflare.Worker("WorkerB", {
113 main,
114 compatibility: { date: "2024-01-01" },
115 });
116 const consumer = yield* Cloudflare.Queues.Consumer("Consumer", {
117 queueId: queue.queueId,
118 scriptName: workerB.workerName,
119 settings: { batchSize: 25, maxRetries: 7 },
120 });
121 return { queue, workerB, consumer };
122 }),
123 );
124
125 expect(replaced.consumer.consumerId).not.toEqual(
126 initial.consumer.consumerId,
127 );
128 expect(replaced.consumer.scriptName).toEqual(replaced.workerB.workerName);
129
130 const liveReplaced = yield* queues.getConsumer({
131 accountId,
132 queueId: replaced.queue.queueId,
133 consumerId: replaced.consumer.consumerId,
134 });
135 expect(
136 "scriptName" in liveReplaced ? liveReplaced.scriptName : undefined,
137 ).toEqual(replaced.workerB.workerName);
138
139 // The original consumer must be gone after the replace.
140 const oldExit = yield* Effect.exit(
141 queues.getConsumer({
142 accountId,
143 queueId: replaced.queue.queueId,
144 consumerId: initial.consumer.consumerId,
145 }),
146 );
147 expect(Exit.isFailure(oldExit)).toBe(true);
148
149 yield* stack.destroy();
150
151 // Post-destroy: the new consumer must be gone on Cloudflare too.
152 const exit = yield* Effect.exit(
153 queues.getConsumer({
154 accountId,
155 queueId: replaced.queue.queueId,
156 consumerId: replaced.consumer.consumerId,
157 }),
158 );
159 expect(Exit.isFailure(exit)).toBe(true);
160 }).pipe(logLevel),
161 {
162 tags: [
163 "provider:cloudflare",
164 "provider:cloudflare:queue",
165 "provider:cloudflare:worker",
166 "live",
167 ],
168 },
169);
170
171/**
172 * Recovery from out-of-band consumer deletion. After a manual
173 * `deleteConsumer` via the API, the reconciler must observe that
174 * the consumer is missing and recreate it instead of failing on a
175 * stale `output.consumerId` from local state.
176 *
177 * The redeploy bumps `settings` so the diff returns `update` and
178 * the engine actually invokes reconcile (a no-prop redeploy is a
179 * `noop` and skips drift detection by design — drift correction
180 * only happens when something the user-controlled changes).
181 */
182test.provider(
183 "recreates consumer after out-of-band delete",
184 (stack) =>
185 Effect.gen(function* () {
186 const env = yield* CloudflareEnvironment;
187 const { accountId } = yield* env;
188
189 yield* stack.destroy();
190
191 const initial = yield* stack.deploy(
192 Effect.gen(function* () {
193 const queue = yield* Cloudflare.Queues.Queue("Q");
194 const worker = yield* Cloudflare.Worker("Worker", {
195 main,
196 compatibility: { date: "2024-01-01" },
197 });
198 const consumer = yield* Cloudflare.Queues.Consumer("Consumer", {
199 queueId: queue.queueId,
200 scriptName: worker.workerName,
201 settings: { batchSize: 5 },
202 });
203 return { queue, worker, consumer };
204 }),
205 );
206
207 // Out-of-band delete via the SDK directly.
208 yield* queues.deleteConsumer({
209 accountId,
210 queueId: initial.queue.queueId,
211 consumerId: initial.consumer.consumerId,
212 });
213
214 const recovered = yield* stack.deploy(
215 Effect.gen(function* () {
216 const queue = yield* Cloudflare.Queues.Queue("Q");
217 const worker = yield* Cloudflare.Worker("Worker", {
218 main,
219 compatibility: { date: "2024-01-01" },
220 });
221 const consumer = yield* Cloudflare.Queues.Consumer("Consumer", {
222 queueId: queue.queueId,
223 scriptName: worker.workerName,
224 settings: { batchSize: 11 },
225 });
226 return { queue, worker, consumer };
227 }),
228 );
229
230 expect(recovered.consumer.consumerId).toBeTypeOf("string");
231 // The new consumer must be reachable on Cloudflare — the previous
232 // implementation died with "already exists but could not be found"
233 // because listConsumers was single-page and ConsumerAlreadyExists
234 // was caught by a generic `Effect.catch`. A freshly-created consumer
235 // can briefly 404 from this out-of-band read under load, so ride out
236 // the read-after-create lag before asserting.
237 const live = yield* queues
238 .getConsumer({
239 accountId,
240 queueId: recovered.queue.queueId,
241 consumerId: recovered.consumer.consumerId,
242 })
243 .pipe(
244 Effect.retry({
245 while: (e) => e._tag === "ConsumerNotFound",
246 schedule: Schedule.exponential("500 millis"),
247 times: 8,
248 }),
249 );
250 expect("scriptName" in live ? live.scriptName : undefined).toEqual(
251 recovered.worker.workerName,
252 );
253
254 yield* stack.destroy();
255 }).pipe(logLevel),
256 {
257 tags: [
258 "provider:cloudflare",
259 "provider:cloudflare:queue",
260 "provider:cloudflare:worker",
261 "live",
262 ],
263 },
264);
265
266/**
267 * State-loss adoption. Wipe the local state for the Consumer,
268 * leaving the Cloudflare consumer in place. On redeploy the engine
269 * calls `provider.read`, which now falls back to listConsumers when
270 * `output.consumerId` is missing — so the consumer is adopted instead
271 * of producing a duplicate-create attempt.
272 */
273test.provider(
274 "adopts existing consumer after local state loss",
275 (stack) =>
276 Effect.gen(function* () {
277 const { accountId } = yield* yield* CloudflareEnvironment;
278
279 yield* stack.destroy();
280
281 const initial = yield* stack.deploy(
282 Effect.gen(function* () {
283 const queue = yield* Cloudflare.Queues.Queue("Q");
284 const worker = yield* Cloudflare.Worker("Worker", {
285 main,
286 compatibility: { date: "2024-01-01" },
287 });
288 const consumer = yield* Cloudflare.Queues.Consumer("Consumer", {
289 queueId: queue.queueId,
290 scriptName: worker.workerName,
291 });
292 return { queue, worker, consumer };
293 }),
294 );
295
296 // Wipe just the Consumer entry — Queue and Worker stay so the
297 // redeploy reuses the same queueId / scriptName.
298 yield* Effect.gen(function* () {
299 const state = yield* yield* State;
300 yield* state.delete({
301 stack: stack.name,
302 stage: stack.stage,
303 fqn: "Consumer",
304 });
305 }).pipe(Effect.provide(stack.state));
306
307 const adopted = yield* stack.deploy(
308 Effect.gen(function* () {
309 const queue = yield* Cloudflare.Queues.Queue("Q");
310 const worker = yield* Cloudflare.Worker("Worker", {
311 main,
312 compatibility: { date: "2024-01-01" },
313 });
314 const consumer = yield* Cloudflare.Queues.Consumer("Consumer", {
315 queueId: queue.queueId,
316 scriptName: worker.workerName,
317 });
318 return { queue, worker, consumer };
319 }),
320 );
321
322 // Adoption: the consumerId from Cloudflare equals the one we
323 // created originally — we did not duplicate-create.
324 expect(adopted.consumer.consumerId).toEqual(initial.consumer.consumerId);
325
326 const live = yield* queues.getConsumer({
327 accountId,
328 queueId: adopted.queue.queueId,
329 consumerId: adopted.consumer.consumerId,
330 });
331 expect("scriptName" in live ? live.scriptName : undefined).toEqual(
332 adopted.worker.workerName,
333 );
334
335 yield* stack.destroy();
336 }).pipe(logLevel),
337 {
338 tags: [
339 "provider:cloudflare",
340 "provider:cloudflare:queue",
341 "provider:cloudflare:worker",
342 "live",
343 ],
344 },
345);
346
347/**
348 * Conflict: queue already has a worker consumer pointing at a
349 * *different* script. This is the regression for the user-reported
350 * "already exists but could not be found" error — the previous
351 * implementation filtered listConsumers by `news.scriptName`, so a
352 * collision with a different script left the find empty and the
353 * reconciler died with a misleading message.
354 *
355 * The new behaviour: detect the foreign worker consumer and fail with
356 * a clear, actionable error naming both the existing script and the
357 * desired one.
358 */
359test.provider(
360 "fails clearly when queue has consumer for different script",
361 (stack) =>
362 Effect.gen(function* () {
363 yield* stack.destroy();
364
365 // Phase 1: deploy worker A as the queue's consumer, then wipe just
366 // the Consumer state so the next deploy thinks it's a
367 // greenfield create.
368 const initial = yield* stack.deploy(
369 Effect.gen(function* () {
370 const queue = yield* Cloudflare.Queues.Queue("Q");
371 const workerA = yield* Cloudflare.Worker("WorkerA", {
372 main,
373 compatibility: { date: "2024-01-01" },
374 });
375 const consumer = yield* Cloudflare.Queues.Consumer("Consumer", {
376 queueId: queue.queueId,
377 scriptName: workerA.workerName,
378 });
379 return { queue, workerA, consumer };
380 }),
381 );
382
383 yield* Effect.gen(function* () {
384 const state = yield* yield* State;
385 yield* state.delete({
386 stack: stack.name,
387 stage: stack.stage,
388 fqn: "Consumer",
389 });
390 }).pipe(Effect.provide(stack.state));
391
392 // Phase 2: redeploy with a different scriptName under the same
393 // logical id. Cloudflare's queue still has worker A as consumer.
394 const exit = yield* Effect.exit(
395 stack.deploy(
396 Effect.gen(function* () {
397 const queue = yield* Cloudflare.Queues.Queue("Q");
398 const workerB = yield* Cloudflare.Worker("WorkerB", {
399 main,
400 compatibility: { date: "2024-01-01" },
401 });
402 const consumer = yield* Cloudflare.Queues.Consumer("Consumer", {
403 queueId: queue.queueId,
404 scriptName: workerB.workerName,
405 });
406 return { queue, workerB, consumer };
407 }),
408 ),
409 );
410
411 expect(Exit.isFailure(exit)).toBe(true);
412 const message = JSON.stringify(exit);
413 // The error must name both the colliding existing script and the
414 // requested one — that is the difference between "user can fix
415 // this" and "what does this mean".
416 expect(message).toContain(initial.workerA.workerName);
417 expect(message).toContain("only one worker consumer");
418
419 // Cleanup: re-introduce the Consumer entry pointing at workerA so
420 // destroy can remove the cloud consumer.
421 yield* stack.deploy(
422 Effect.gen(function* () {
423 const queue = yield* Cloudflare.Queues.Queue("Q");
424 const workerA = yield* Cloudflare.Worker("WorkerA", {
425 main,
426 compatibility: { date: "2024-01-01" },
427 });
428 yield* Cloudflare.Queues.Consumer("Consumer", {
429 queueId: queue.queueId,
430 scriptName: workerA.workerName,
431 });
432 return queue;
433 }),
434 );
435
436 yield* stack.destroy();
437 }).pipe(logLevel),
438 {
439 tags: [
440 "provider:cloudflare",
441 "provider:cloudflare:queue",
442 "provider:cloudflare:worker",
443 "live",
444 ],
445 },
446);
447
448/**
449 * Adoption of a stranded prior-generation consumer. A Worker replacement
450 * mints a new physical script name; when the consumer's state entry is
451 * lost across that replacement (the beta.64→beta.67 upgrade wedge), the
452 * queue keeps a worker consumer pointing at the *old* generation's script
453 * while the resource is configured for the new one. Both scripts carry
454 * the same `alchemy:stack/stage/id` ownership tags, which proves the
455 * stranded consumer is this resource's own wiring — the reconciler must
456 * detach it and rebuild for the new script instead of dying with the
457 * foreign-consumer error (which stays reserved for scripts of a
458 * *different* logical worker — see the previous test).
459 */
460test.provider(
461 "adopts stranded consumer from a replaced generation of the same worker",
462 (stack) =>
463 Effect.gen(function* () {
464 const { accountId } = yield* yield* CloudflareEnvironment;
465
466 yield* stack.destroy();
467
468 const initial = yield* stack.deploy(
469 Effect.gen(function* () {
470 const queue = yield* Cloudflare.Queues.Queue("Q");
471 const worker = yield* Cloudflare.Worker("Worker", {
472 main,
473 compatibility: { date: "2024-01-01" },
474 });
475 const consumer = yield* Cloudflare.Queues.Consumer("Consumer", {
476 queueId: queue.queueId,
477 scriptName: worker.workerName,
478 });
479 return { queue, worker, consumer };
480 }),
481 );
482
483 // Lose the consumer's state entry — the live consumer stays
484 // attached to the queue, pointing at the current script.
485 yield* Effect.gen(function* () {
486 const state = yield* yield* State;
487 yield* state.delete({
488 stack: stack.name,
489 stage: stack.stage,
490 fqn: "Consumer",
491 });
492 }).pipe(Effect.provide(stack.state));
493
494 // Replace the worker's physical script (explicit `name` forces a
495 // replacement) in the same deploy that recreates the consumer.
496 // Reconcile observes the stranded consumer on the old script —
497 // same stack/stage/id tags — and must rebuild instead of dying.
498 // Previews cap script names at 54 chars — trim before suffixing.
499 const replacedName = `${initial.worker.workerName.slice(0, 48)}-v2`;
500 const recovered = yield* stack.deploy(
501 Effect.gen(function* () {
502 const queue = yield* Cloudflare.Queues.Queue("Q");
503 const worker = yield* Cloudflare.Worker("Worker", {
504 main,
505 name: replacedName,
506 compatibility: { date: "2024-01-01" },
507 });
508 const consumer = yield* Cloudflare.Queues.Consumer("Consumer", {
509 queueId: queue.queueId,
510 scriptName: worker.workerName,
511 });
512 return { queue, worker, consumer };
513 }),
514 );
515
516 expect(recovered.worker.workerName).toEqual(replacedName);
517 expect(recovered.consumer.scriptName).toEqual(replacedName);
518
519 const live = yield* queues
520 .getConsumer({
521 accountId,
522 queueId: recovered.queue.queueId,
523 consumerId: recovered.consumer.consumerId,
524 })
525 .pipe(
526 Effect.retry({
527 while: (e) => e._tag === "ConsumerNotFound",
528 schedule: Schedule.exponential("500 millis"),
529 times: 8,
530 }),
531 );
532 expect("scriptName" in live ? live.scriptName : undefined).toEqual(
533 replacedName,
534 );
535
536 yield* stack.destroy();
537 }).pipe(logLevel),
538 {
539 tags: [
540 "provider:cloudflare",
541 "provider:cloudflare:queue",
542 "provider:cloudflare:worker",
543 "live",
544 ],
545 },
546);
547
548/**
549 * Suppressed delete: a consumer that only ever existed in dev (its
550 * persisted `consumerId` is a `dev:` mock id) has no Cloudflare
551 * counterpart. Destroying it must NOT issue a `deleteConsumer` against
552 * Cloudflare — the `dev:` ids are not valid and the request URL would be
553 * malformed. The live provider's `delete` short-circuits on the non-live
554 * id, so destroy succeeds and the state row is removed cleanly.
555 */
556test.provider(
557 "suppresses deletion of a dev-only consumer",
558 (stack) =>
559 Effect.gen(function* () {
560 const { accountId } = yield* yield* CloudflareEnvironment;
561
562 yield* stack.destroy();
563
564 const devQueueId = generateLocalId();
565 const devConsumerId = generateLocalId();
566
567 yield* Effect.gen(function* () {
568 const state = yield* yield* State;
569 yield* state.set({
570 stack: stack.name,
571 stage: stack.stage,
572 fqn: "Consumer",
573 value: {
574 kind: "resource",
575 status: "created",
576 resourceType: "Cloudflare.Queues.Consumer",
577 namespace: undefined,
578 fqn: "Consumer",
579 logicalId: "Consumer",
580 instanceId: "00000000000000000000000000000002",
581 providerVersion: 0,
582 bindings: [],
583 downstream: [],
584 props: {
585 queueId: devQueueId,
586 scriptName: "dev-worker",
587 },
588 attr: {
589 consumerId: devConsumerId,
590 queueId: devQueueId,
591 scriptName: "dev-worker",
592 accountId,
593 },
594 } satisfies CreatedResourceState,
595 });
596 });
597
598 // Destroy must not attempt a (malformed) live delete against the dev ids.
599 const exit = yield* Effect.exit(stack.destroy());
600 expect(Exit.isSuccess(exit)).toBe(true);
601
602 // The dev-only resource is removed from state.
603 const persisted = yield* Effect.gen(function* () {
604 const state = yield* yield* State;
605 return yield* state.get({
606 stack: stack.name,
607 stage: stack.stage,
608 fqn: "Consumer",
609 });
610 });
611 expect(persisted).toBeUndefined();
612 }).pipe(logLevel),
613 { tags: ["provider:cloudflare", "provider:cloudflare:queue", "live"] },
614);
615
616/**
617 * Promotion: a consumer whose persisted `consumerId` is a `dev:` mock id
618 * must be promoted to a real Cloudflare consumer on a live deploy.
619 *
620 * We first deploy a real queue + worker + consumer, then rewrite the
621 * persisted consumer's `consumerId` back to a `dev:` id (leaving the
622 * `queueId` live — in a real dev→live transition the upstream Queue is
623 * promoted first, so the consumer's queue ref is already a live id while
624 * its own id is still the dev one it was minted with). On redeploy the
625 * live provider's `diff` sees the `dev:` consumerId and returns `update`
626 * (not `noop`), so `reconcile` runs, re-observes the live consumer, and
627 * the persisted id is healed back to the real one.
628 */
629test.provider(
630 "promotes a dev consumer to a live consumer on deploy",
631 (stack) =>
632 Effect.gen(function* () {
633 const { accountId } = yield* yield* CloudflareEnvironment;
634
635 yield* stack.destroy();
636
637 const buildStack = Effect.gen(function* () {
638 const queue = yield* Cloudflare.Queues.Queue("Q");
639 const worker = yield* Cloudflare.Worker("Worker", {
640 main,
641 compatibility: { date: "2024-01-01" },
642 });
643 const consumer = yield* Cloudflare.Queues.Consumer("Consumer", {
644 queueId: queue.queueId,
645 scriptName: worker.workerName,
646 settings: { batchSize: 5 },
647 });
648 return { queue, worker, consumer };
649 });
650
651 const initial = yield* stack.deploy(buildStack);
652 expect(isLiveId(initial.consumer.consumerId)).toBe(true);
653
654 // Rewrite the persisted consumerId back to a dev id, simulating a
655 // consumer that was minted in `alchemy dev`.
656 const devQueueId = generateLocalId();
657 const devConsumerId = generateLocalId();
658 yield* Effect.gen(function* () {
659 const state = yield* yield* State;
660 const currentConsumer = (yield* state.get({
661 stack: stack.name,
662 stage: stack.stage,
663 fqn: "Consumer",
664 })) as CreatedResourceState;
665 yield* state.set({
666 stack: stack.name,
667 stage: stack.stage,
668 fqn: "Consumer",
669 value: {
670 ...currentConsumer,
671 attr: {
672 ...currentConsumer.attr,
673 queueId: devQueueId,
674 consumerId: devConsumerId,
675 },
676 },
677 });
678 });
679
680 const promoted = yield* stack.deploy(buildStack);
681
682 // The dev id was promoted back to the real, live consumer id.
683 expect(isLiveId(promoted.consumer.consumerId)).toBe(true);
684 expect(isLiveId(promoted.queue.queueId)).toBe(true);
685 expect(promoted.consumer.consumerId).not.toEqual(devConsumerId);
686 expect(promoted.consumer.consumerId).toEqual(initial.consumer.consumerId);
687 expect(promoted.queue.queueId).toEqual(initial.queue.queueId);
688
689 const live = yield* queues.getConsumer({
690 accountId,
691 queueId: promoted.queue.queueId,
692 consumerId: promoted.consumer.consumerId,
693 });
694 expect("scriptName" in live ? live.scriptName : undefined).toEqual(
695 promoted.worker.workerName,
696 );
697
698 yield* stack.destroy();
699 }).pipe(logLevel),
700 {
701 tags: [
702 "provider:cloudflare",
703 "provider:cloudflare:queue",
704 "provider:cloudflare:worker",
705 "live",
706 ],
707 },
708);
709
710/**
711 * Canonical `list()` test (parent fan-out): queue consumers have no
712 * account-wide enumeration API, so `list()` enumerates every queue in the
713 * account and lists each queue's worker consumer. Deploy a queue + worker +
714 * consumer, then assert the consumer is present in the exhaustively-
715 * paginated result, hydrated into the same `Attributes` shape `read` returns.
716 */
717test.provider(
718 "list enumerates the deployed consumer",
719 (stack) =>
720 Effect.gen(function* () {
721 yield* stack.destroy();
722
723 const deployed = yield* stack.deploy(
724 Effect.gen(function* () {
725 const queue = yield* Cloudflare.Queues.Queue("Q");
726 const worker = yield* Cloudflare.Worker("Worker", {
727 main,
728 compatibility: { date: "2024-01-01" },
729 });
730 const consumer = yield* Cloudflare.Queues.Consumer("Consumer", {
731 queueId: queue.queueId,
732 scriptName: worker.workerName,
733 settings: { batchSize: 7 },
734 });
735 return { queue, worker, consumer };
736 }),
737 );
738
739 const provider = yield* Provider.findProvider(Cloudflare.Queues.Consumer);
740 const all = yield* provider.list();
741
742 const found = all.find(
743 (c) => c.consumerId === deployed.consumer.consumerId,
744 );
745 expect(found).toBeDefined();
746 expect(found?.queueId).toEqual(deployed.queue.queueId);
747 expect(found?.scriptName).toEqual(deployed.worker.workerName);
748 expect(found?.accountId).toBeTypeOf("string");
749
750 yield* stack.destroy();
751 }).pipe(logLevel),
752 {
753 tags: [
754 "provider:cloudflare",
755 "provider:cloudflare:queue",
756 "provider:cloudflare:worker",
757 "live",
758 ],
759 },
760);