fix(generators): event buses deliver the zod-parsed payload

InMemoryEventBus.publish and PayloadJobsEventBus.publish called
descriptor.schema.parse(payload) but discarded the result, so handlers
and enqueued jobs received the raw input — zod defaults, catches,
transforms and strips never applied. Both bus templates now fan out the
parsed value; regression tests added to both test templates and the
events snapshot hashes regenerated (template-tree sha over
hbs-stripped paths — only the four touched files differ).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 16:30:53 +02:00
parent 6fd746d3bd
commit 8476712620
5 changed files with 38 additions and 10 deletions

View File

@@ -29,11 +29,11 @@
},
{
"path": "src/in-memory-event-bus.test.ts",
"sha256": "56f704de153f7676bf243d6350f4bebe28cb9ffec1eb3afef8f5f2bb08561c21"
"sha256": "d75a7694cbf08f6afdc0b5ffe80c37e2c61e00b94f61231200e1c26becb9f26f"
},
{
"path": "src/in-memory-event-bus.ts",
"sha256": "d7de709049bd864bdcd8f02316d981cf0407efbc5b6f13fd2ab86094ecaddb8a"
"sha256": "c629e0733ab23ef808be7cba04ccd4d31792846dc81d02c3722a210a47aa5035"
},
{
"path": "src/index.ts",
@@ -41,11 +41,11 @@
},
{
"path": "src/payload-jobs-event-bus.test.ts",
"sha256": "cda493839ed9af7d48a4c240392933af7167805a94ebaf30efd64f8a374b7441"
"sha256": "dc091425978d4e02881ccdcb2a46b716273d848ebdd87f72131abdf419a40161"
},
{
"path": "src/payload-jobs-event-bus.ts",
"sha256": "5fbeb611af7f4ba4302828de6a0262a74d9b4bb6a11cbbb8ed40712aae731ded"
"sha256": "2aae98fcc2b3d4c77d7b0f80e1aedb56bbce737e296b9681b663fd22380ae3ab"
},
{
"path": "src/symbols.ts",

View File

@@ -46,4 +46,19 @@ describe("InMemoryEventBus", () => {
const bus = new InMemoryEventBus();
await expect(bus.publish(evt, { id: "x" })).resolves.toBeUndefined();
});
it("delivers the zod-parsed payload, not the raw input", async () => {
const evtNormalized = defineEvent(
"test.normalized",
z.object({ id: z.string(), mode: z.string().catch("auto") }),
);
const bus = new InMemoryEventBus();
const handler = vi.fn();
bus.subscribe(evtNormalized, "consumer", handler);
await bus.publish(evtNormalized, {
id: "x",
mode: 42 as unknown as string,
});
expect(handler).toHaveBeenCalledWith({ id: "x", mode: "auto" });
});
});

View File

@@ -16,12 +16,10 @@ export class InMemoryEventBus implements IEventBus {
descriptor: EventDescriptor<string, z.ZodType<T>>,
payload: T,
): Promise<void> {
descriptor.schema.parse(payload);
const parsed = descriptor.schema.parse(payload);
const subscribers = this.handlers.get(descriptor.name) ?? [];
if (subscribers.length === 0) return;
const settled = await Promise.allSettled(
subscribers.map((h) => h(payload)),
);
const settled = await Promise.allSettled(subscribers.map((h) => h(parsed)));
if (this.options.failFast) {
const failure = settled.find((s) => s.status === "rejected");
// Only the first rejection is rethrown. Other failures are intentionally

View File

@@ -42,6 +42,21 @@ describe("PayloadJobsEventBus", () => {
expect(queue.enqueued[0]!.input).toEqual({ userId: "u1" });
});
it("enqueues the zod-parsed payload, not the raw input", async () => {
const evtNormalized = defineEvent(
"test.normalized",
z.object({ userId: z.string(), mode: z.string().catch("auto") }),
);
const queue = recordingQueue();
const bus = new PayloadJobsEventBus(queue);
bus.subscribe(evtNormalized, "marketing-pages", vi.fn());
await bus.publish(evtNormalized, {
userId: "u1",
mode: 42 as unknown as string,
});
expect(queue.enqueued[0]!.input).toEqual({ userId: "u1", mode: "auto" });
});
it("enqueues nothing when no subscribers are registered", async () => {
const queue = recordingQueue();
const bus = new PayloadJobsEventBus(queue);

View File

@@ -19,13 +19,13 @@ export class PayloadJobsEventBus implements IEventBus {
descriptor: EventDescriptor<string, z.ZodType<T>>,
payload: T,
): Promise<void> {
descriptor.schema.parse(payload);
const parsed = descriptor.schema.parse(payload);
const consumers = this.subscribers.get(descriptor.name) ?? [];
await Promise.all(
consumers.map((consumerFeature) =>
this.queue.enqueue(
`__events.${descriptor.name}.${consumerFeature}`,
payload,
parsed,
),
),
);