import type { TenantId } from "@suaveplan/api-types/tenant"; import type { BrokerEnvelope } from "@suaveplan/api-ws/broker"; import { createInMemoryWsBroker, type InMemoryWsBroker, } from "../broker/broker.js"; import type { InMemoryWsBrokerOptions } from "../options/options.js"; /** * Result returned by {@link createInMemoryBrokerHarness}. * * The harness ships everything a test needs to drive the broker: * * - `broker` — the broker instance under test. * - `received` — a chronological array of every envelope delivered * to the auto-subscribed observer, useful for `expect(...).toEqual`. * - `observerSubscription` — disposal handle for the observer. * - `publishOne(payload, opts?)` — convenience for publishing a * single envelope on the harness's default topic + tenant. * - `dispose()` — tears down the observer and disposes the broker * (idempotent). */ export interface InMemoryBrokerHarness { readonly broker: InMemoryWsBroker; readonly received: ReadonlyArray>; readonly observerSubscription: { readonly topic: string }; publishOne( payload: Payload, opts?: { readonly topic?: string; readonly tenant?: TenantId } ): Promise; dispose(): void; } /** * Options accepted by {@link createInMemoryBrokerHarness}. The defaults * cover the 90% test path; supply `topic` / `tenant` only when the * scenario needs to vary them. */ export interface CreateInMemoryBrokerHarnessOptions { readonly broker?: InMemoryWsBrokerOptions; readonly topic?: string; readonly tenant?: TenantId; readonly extraTopics?: ReadonlyArray; readonly initialPayload?: Payload; } const DEFAULT_TOPIC = "test.events"; const DEFAULT_TENANT = "tenant-test" as unknown as TenantId; /** * Build a ready-to-use broker harness for tests. The harness wires up * a single observer subscription on `topic` so callers can `publish` * and assert on `received` without any boilerplate. * * The harness is intentionally additive: it does NOT replace the * broker's public surface — `harness.broker` is the real * {@link InMemoryWsBroker} returned by * {@link createInMemoryWsBroker}, so any test can drop down to direct * subscribe/publish/dispose when the convenience helpers don't fit. */ export function createInMemoryBrokerHarness( options: CreateInMemoryBrokerHarnessOptions = {} ): InMemoryBrokerHarness { const topic = options.topic ?? DEFAULT_TOPIC; const tenant = options.tenant ?? DEFAULT_TENANT; const broker = createInMemoryWsBroker(options.broker); const received: BrokerEnvelope[] = []; const observerSubscription = broker.subscribe(topic, (envelope) => { received.push(envelope); }); // Pre-warm extra topics by registering a no-op subscriber so // subscriberCount reports them. Useful for tests that probe the // registry shape before publishing. const extraHandles = (options.extraTopics ?? []).map((extra) => broker.subscribe(extra, () => { /* probe subscriber */ }) ); let disposed = false; return { broker, received, observerSubscription: { topic: observerSubscription.topic }, async publishOne( payload: Payload, opts?: { readonly topic?: string; readonly tenant?: TenantId } ): Promise { const envelope: BrokerEnvelope = { topic: opts?.topic ?? topic, tenant: opts?.tenant ?? tenant, payload, }; await broker.publish(envelope.topic, envelope); }, dispose(): void { if (disposed) return; disposed = true; try { observerSubscription.unsubscribe(); } catch { /* already torn down by broker.dispose */ } for (const handle of extraHandles) { try { handle.unsubscribe(); } catch { /* same */ } } broker.dispose(); }, }; }