From 4b1029aeaebc8d5606c51502aa9e425840281acf Mon Sep 17 00:00:00 2001 From: WhangSangGun Date: Thu, 3 Sep 2026 15:10:07 +0900 Subject: [PATCH 1/2] Test enqueueMany() wrapped message shape WorkersMessageQueue.enqueueMany() uses sendBatch() while enqueue() uses send(), so nothing previously caught the two paths drifting apart in how they wrap a message. Add tests that check the wrapped shape enqueueMany() produces, compare it against what enqueue() produces for the same input, and confirm the ordering key is left undefined when none is given. Refs https://github.com/fedify-dev/fedify/issues/878 Assisted-by: Claude Code:claude-sonnet-5 --- packages/cfworkers/src/mod.test.ts | 95 ++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/packages/cfworkers/src/mod.test.ts b/packages/cfworkers/src/mod.test.ts index 0ed720606..c6a341d4b 100644 --- a/packages/cfworkers/src/mod.test.ts +++ b/packages/cfworkers/src/mod.test.ts @@ -4,6 +4,7 @@ import { WorkersKvStore, WorkersMessageQueue, } from "./mod.ts"; +import type { Queue } from "@cloudflare/workers-types"; // Mock Temporal.Duration for testing in Cloudflare Workers environment const mockDuration = (seconds: number) => ({ @@ -352,3 +353,97 @@ describe("WorkersMessageQueue", () => { expect(second.message).toEqual({ id: "second" }); }); }); + +interface MockWrappedMessage { + readonly __fedify_ordering_key__?: string; + readonly __fedify_payload__: unknown; +} + +interface MockSendBatchOptions { + readonly delaySeconds?: number; +} + +interface MockSendOptions { + readonly contentType?: string; + readonly delaySeconds?: number; +} + +interface MockMessageSendRequest { + readonly body: MockWrappedMessage; + readonly contentType?: string; +} + +class MockQueue { + sentSingles: { + wrapped: MockWrappedMessage; + options?: MockSendOptions; + }[] = []; + sentBatches: { + batch: MockMessageSendRequest[]; + options?: MockSendBatchOptions; + }[] = []; + + send(wrapped: MockWrappedMessage, options?: MockSendOptions) { + this.sentSingles.push({ wrapped, options }); + } + + sendBatch(batch: MockMessageSendRequest[], options?: MockSendBatchOptions) { + this.sentBatches.push({ batch, options }); + } +} + +describe("WorkersMessageQueue.enqueueMany() - wrapped message shape", () => { + it("enqueueMany() - wraps each message with body{} and contentType", async () => { + const sendingMockQueue = new MockQueue(); + const queue = new WorkersMessageQueue(sendingMockQueue as unknown as Queue); + + await queue.enqueueMany(["msg-1", "msg-2"], { orderingKey: "ferer" }); + + expect(sendingMockQueue.sentBatches).toHaveLength(1); + expect(sendingMockQueue.sentBatches[0].batch).toEqual([ + { + body: { + __fedify_ordering_key__: "ferer", + __fedify_payload__: "msg-1", + }, + contentType: "json", + }, + { + body: { + __fedify_ordering_key__: "ferer", + __fedify_payload__: "msg-2", + }, + contentType: "json", + }, + ]); + }); + + it("enqueue() and enqueueMany() - produce the same wrapped shape", async () => { + const sendingMockQueue = new MockQueue(); + const queue = new WorkersMessageQueue(sendingMockQueue as unknown as Queue); + + await queue.enqueue("msg-1", { orderingKey: "ferer" }); + await queue.enqueueMany(["msg-1"], { orderingKey: "ferer" }); + + // enqueueMany() wraps each body in a request ({ body, contentType }), so + // unwrap it before comparing against enqueue()'s bare wrapped message. + const singleWrapped = sendingMockQueue.sentSingles[0].wrapped; + const batchWrapped = sendingMockQueue.sentBatches[0].batch[0].body; + + expect(batchWrapped).toEqual(singleWrapped); + }); + + it("enqueueMany() - omits ordering key when not provided", async () => { + const sendingMockQueue = new MockQueue(); + const queue = new WorkersMessageQueue(sendingMockQueue as unknown as Queue); + + await queue.enqueueMany(["msg-1"]); + + expect(sendingMockQueue.sentBatches[0].batch[0].body).toEqual( + { + __fedify_ordering_key__: undefined, + __fedify_payload__: "msg-1", + }, + ); + }); +}); From 6c9d41ecfa864b71a86669fe1c4c243eb46c6d09 Mon Sep 17 00:00:00 2001 From: WhangSangGun Date: Mon, 21 Sep 2026 15:25:42 +0900 Subject: [PATCH 2/2] Type MockQueue against the Queue contract --- packages/cfworkers/src/mod.test.ts | 59 +++++++++++++++++------------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/packages/cfworkers/src/mod.test.ts b/packages/cfworkers/src/mod.test.ts index c6a341d4b..50c63a2fa 100644 --- a/packages/cfworkers/src/mod.test.ts +++ b/packages/cfworkers/src/mod.test.ts @@ -4,7 +4,15 @@ import { WorkersKvStore, WorkersMessageQueue, } from "./mod.ts"; -import type { Queue } from "@cloudflare/workers-types"; +import type { + MessageSendRequest, + Queue, + QueueMetrics, + QueueSendBatchOptions, + QueueSendBatchResponse, + QueueSendOptions, + QueueSendResponse, +} from "@cloudflare/workers-types"; // Mock Temporal.Duration for testing in Cloudflare Workers environment const mockDuration = (seconds: number) => ({ @@ -359,43 +367,44 @@ interface MockWrappedMessage { readonly __fedify_payload__: unknown; } -interface MockSendBatchOptions { - readonly delaySeconds?: number; -} - -interface MockSendOptions { - readonly contentType?: string; - readonly delaySeconds?: number; -} - -interface MockMessageSendRequest { - readonly body: MockWrappedMessage; - readonly contentType?: string; -} - -class MockQueue { +class MockQueue implements Queue { + metrics(): Promise { + return Promise.resolve({ backlogCount: 0, backlogBytes: 0 }); + } sentSingles: { wrapped: MockWrappedMessage; - options?: MockSendOptions; + options?: QueueSendOptions; }[] = []; sentBatches: { - batch: MockMessageSendRequest[]; - options?: MockSendBatchOptions; + batch: MessageSendRequest[]; + options?: QueueSendBatchOptions; }[] = []; - send(wrapped: MockWrappedMessage, options?: MockSendOptions) { + send( + wrapped: MockWrappedMessage, + options?: QueueSendOptions, + ): Promise { this.sentSingles.push({ wrapped, options }); + return Promise.resolve({ + metadata: { metrics: { backlogCount: 0, backlogBytes: 0 } }, + }); } - sendBatch(batch: MockMessageSendRequest[], options?: MockSendBatchOptions) { - this.sentBatches.push({ batch, options }); + sendBatch( + batch: Iterable>, + options?: QueueSendBatchOptions, + ): Promise { + this.sentBatches.push({ batch: [...batch], options }); + return Promise.resolve({ + metadata: { metrics: { backlogCount: 0, backlogBytes: 0 } }, + }); } } describe("WorkersMessageQueue.enqueueMany() - wrapped message shape", () => { it("enqueueMany() - wraps each message with body{} and contentType", async () => { const sendingMockQueue = new MockQueue(); - const queue = new WorkersMessageQueue(sendingMockQueue as unknown as Queue); + const queue = new WorkersMessageQueue(sendingMockQueue); await queue.enqueueMany(["msg-1", "msg-2"], { orderingKey: "ferer" }); @@ -420,7 +429,7 @@ describe("WorkersMessageQueue.enqueueMany() - wrapped message shape", () => { it("enqueue() and enqueueMany() - produce the same wrapped shape", async () => { const sendingMockQueue = new MockQueue(); - const queue = new WorkersMessageQueue(sendingMockQueue as unknown as Queue); + const queue = new WorkersMessageQueue(sendingMockQueue); await queue.enqueue("msg-1", { orderingKey: "ferer" }); await queue.enqueueMany(["msg-1"], { orderingKey: "ferer" }); @@ -435,7 +444,7 @@ describe("WorkersMessageQueue.enqueueMany() - wrapped message shape", () => { it("enqueueMany() - omits ordering key when not provided", async () => { const sendingMockQueue = new MockQueue(); - const queue = new WorkersMessageQueue(sendingMockQueue as unknown as Queue); + const queue = new WorkersMessageQueue(sendingMockQueue); await queue.enqueueMany(["msg-1"]);