From 1357e45f554f27006f9b11b22dbbc01842f90cb9 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Fri, 8 May 2026 12:05:34 +0200 Subject: [PATCH] feat(core-testing): RecordingEventBus Adds RecordingEventBus implementing IEventBus for use in unit tests. Validates payloads via the descriptor schema, records all publish calls, and delivers events to subscribed handlers synchronously in subscription order. Co-Authored-By: Claude Sonnet 4.6 --- packages/core-testing/package.json | 2 + .../core-testing/src/instrumentation/index.ts | 1 + .../recording-event-bus.test.ts | 39 +++++++++++++++++++ .../instrumentation/recording-event-bus.ts | 26 +++++++++++++ pnpm-lock.yaml | 6 +++ 5 files changed, 74 insertions(+) create mode 100644 packages/core-testing/src/instrumentation/recording-event-bus.test.ts create mode 100644 packages/core-testing/src/instrumentation/recording-event-bus.ts diff --git a/packages/core-testing/package.json b/packages/core-testing/package.json index 87b8599..25a76b7 100644 --- a/packages/core-testing/package.json +++ b/packages/core-testing/package.json @@ -21,8 +21,10 @@ "test": "vitest run" }, "dependencies": { + "@repo/core-events": "workspace:*", "@repo/core-shared": "workspace:*", "@testing-library/jest-dom": "^6.5.0", + "zod": "^3.23.0", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.0", "@trpc/client": "^11.0.0", diff --git a/packages/core-testing/src/instrumentation/index.ts b/packages/core-testing/src/instrumentation/index.ts index cd045e9..053357c 100644 --- a/packages/core-testing/src/instrumentation/index.ts +++ b/packages/core-testing/src/instrumentation/index.ts @@ -1,3 +1,4 @@ export { RecordingTracer, type RecordedSpan } from "./recording-tracer"; export { RecordingLogger, type RecordedCapture } from "./recording-logger"; export { RecordingJobQueue } from "./recording-job-queue"; +export { RecordingEventBus } from "./recording-event-bus"; diff --git a/packages/core-testing/src/instrumentation/recording-event-bus.test.ts b/packages/core-testing/src/instrumentation/recording-event-bus.test.ts new file mode 100644 index 0000000..2325032 --- /dev/null +++ b/packages/core-testing/src/instrumentation/recording-event-bus.test.ts @@ -0,0 +1,39 @@ +import { describe, it, expect } from "vitest"; +import { z } from "zod"; +import { defineEvent } from "@repo/core-events"; +import { RecordingEventBus } from "@/instrumentation/recording-event-bus"; + +const evt = defineEvent("test.evt", z.object({ id: z.string() }).strict()); + +describe("RecordingEventBus", () => { + it("records every publish call after schema validation", async () => { + const bus = new RecordingEventBus(); + await bus.publish(evt, { id: "a" }); + await bus.publish(evt, { id: "b" }); + expect(bus.published).toEqual([ + { name: "test.evt", payload: { id: "a" } }, + { name: "test.evt", payload: { id: "b" } }, + ]); + }); + + it("rejects invalid payloads", async () => { + const bus = new RecordingEventBus(); + await expect( + bus.publish(evt, { id: 1 } as unknown as { id: string }), + ).rejects.toThrow(); + expect(bus.published).toHaveLength(0); + }); + + it("invokes registered handlers sequentially in subscription order", async () => { + const bus = new RecordingEventBus(); + const order: string[] = []; + bus.subscribe(evt, "consumer-a", async () => { + order.push("a"); + }); + bus.subscribe(evt, "consumer-b", async () => { + order.push("b"); + }); + await bus.publish(evt, { id: "x" }); + expect(order).toEqual(["a", "b"]); + }); +}); diff --git a/packages/core-testing/src/instrumentation/recording-event-bus.ts b/packages/core-testing/src/instrumentation/recording-event-bus.ts new file mode 100644 index 0000000..c4ab87f --- /dev/null +++ b/packages/core-testing/src/instrumentation/recording-event-bus.ts @@ -0,0 +1,26 @@ +import type { z } from "zod"; +import type { EventDescriptor, EventHandler, IEventBus } from "@repo/core-events"; + +export class RecordingEventBus implements IEventBus { + readonly published: { name: string; payload: unknown }[] = []; + private readonly handlers = new Map[]>(); + + async publish( + descriptor: EventDescriptor>, + payload: T, + ): Promise { + descriptor.schema.parse(payload); + this.published.push({ name: descriptor.name, payload }); + for (const h of this.handlers.get(descriptor.name) ?? []) await h(payload); + } + + subscribe( + descriptor: EventDescriptor>, + _consumerFeature: string, + handler: EventHandler, + ): void { + const arr = this.handlers.get(descriptor.name) ?? []; + arr.push(handler as EventHandler); + this.handlers.set(descriptor.name, arr); + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9cf473f..57fa3fe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -592,6 +592,9 @@ importers: packages/core-testing: dependencies: + '@repo/core-events': + specifier: workspace:* + version: link:../core-events '@repo/core-shared': specifier: workspace:* version: link:../core-shared @@ -628,6 +631,9 @@ importers: vitest: specifier: ^3.0.0 version: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.2)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.32.0)(sass@1.99.0)(terser@5.46.2)(tsx@4.21.0) + zod: + specifier: ^3.23.0 + version: 3.25.76 devDependencies: '@repo/core-eslint': specifier: workspace:*