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

@@ -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,
),
),
);