import { EVENTS_CODES, EventsError } from "@suaveplan/events"; import { describe, expect, it } from "@suaveplan/testing/runner"; import type { SubscriptionEntry } from "./event-bus-service.types.js"; import { buildEmitOptions, nextSubscriptionId, projectSubscription, recordPublishMetric, stableHash, toErrorReport, } from "./helpers.js"; describe("nextSubscriptionId", () => { it("returns a unique id on every call", { timeout: 5000 }, () => { const a = nextSubscriptionId(); const b = nextSubscriptionId(); expect(a).not.toBe(b); expect(a.startsWith("sub_")).toBe(true); }); }); describe("stableHash", () => { it("returns deterministic JSON for plain objects", { timeout: 5000, }, () => { expect(stableHash({ a: 1, b: 2 })).toBe(JSON.stringify({ a: 1, b: 2 })); }); it("returns empty string for undefined", { timeout: 5000 }, () => { expect(stableHash(undefined)).toBe(""); }); it("falls back to String(payload) on JSON failure", { timeout: 5000, }, () => { const circular: Record = {}; circular.self = circular; expect(stableHash(circular)).toBe(String(circular)); }); }); describe("toErrorReport", () => { it("preserves EventsError code", { timeout: 5000 }, () => { const err = new EventsError(EVENTS_CODES["LISTENER.THREW"], "boom"); const report = toErrorReport(err); expect(report.code).toBe("EVENTS.LISTENER.THREW"); expect(report.message).toBe("boom"); }); it("uses LISTENER_FAILED envelope for plain Error", { timeout: 5000, }, () => { const report = toErrorReport(new Error("oops")); expect(report.code).toBe("EVENT_BUS.LISTENER_FAILED"); expect(report.message).toBe("oops"); }); it("stringifies non-Error throwables", { timeout: 5000 }, () => { const report = toErrorReport("literal"); expect(report.code).toBe("EVENT_BUS.LISTENER_FAILED"); expect(report.message).toBe("literal"); }); }); describe("projectSubscription", () => { const base: SubscriptionEntry = { id: "s", topic: "t", scopedTopic: "t", listener: () => undefined, detach: () => undefined, createdAt: 0, once: false, idempotent: false, }; it("includes tenantId when present", { timeout: 5000 }, () => { const view = projectSubscription({ ...base, tenantId: "ta" }); expect(view.tenantId).toBe("ta"); }); it("omits tenantId when absent", { timeout: 5000 }, () => { const view = projectSubscription(base); expect("tenantId" in view).toBe(false); }); }); describe("buildEmitOptions", () => { it("returns an empty object when nothing is supplied", { timeout: 5000, }, () => { expect(buildEmitOptions({})).toEqual({}); }); it("forwards signal/timeoutMs/traceId", { timeout: 5000 }, () => { const ac = new AbortController(); const out = buildEmitOptions({ signal: ac.signal, timeoutMs: 1234, traceId: "trace-1", }); expect(out?.signal).toBe(ac.signal); expect(out?.timeoutMs).toBe(1234); expect(out?.traceId).toBe("trace-1"); }); it("falls back from traceId to idempotencyKey", { timeout: 5000 }, () => { const out = buildEmitOptions({ idempotencyKey: "k1" }); expect(out?.traceId).toBe("k1"); }); }); describe("recordPublishMetric", () => { it("does not throw for either outcome", { timeout: 5000 }, () => { expect(() => recordPublishMetric("order.created", "success", 1) ).not.toThrow(); expect(() => recordPublishMetric("order.created", "error", 1) ).not.toThrow(); }); });