import { describe, expect, it } from "@suaveplan/testing/runner"; import { AUDIO_FX_CODES, type AudioFxError, } from "../error-codes/error-codes.js"; import { DefaultBusRegistry } from "./bus-registry.js"; function frame(samples: number[], channels = 1): Float32Array[] { const out: Float32Array[] = []; for (let ch = 0; ch < channels; ch++) { out.push(Float32Array.from(samples)); } return out; } describe("DefaultBusRegistry: publish + read round-trip", () => { it("returns undefined for an unknown bus", { timeout: 5_000 }, () => { const r = new DefaultBusRegistry(); expect(r.get("missing")).toBeUndefined(); expect(r.list()).toEqual([]); }); it("publishes a named bus reachable via get + list", { timeout: 5_000, }, () => { const r = new DefaultBusRegistry(); const pub = r.publish("kick", 2); expect(pub.bus.name).toBe("kick"); expect(pub.bus.channels).toBe(2); expect(r.get("kick")).toBe(pub.bus); expect(r.list()).toEqual(["kick"]); }); it("round-trips a buffer from write to read", { timeout: 5_000, }, () => { const r = new DefaultBusRegistry(); const pub = r.publish("kick", 1); pub.write(frame([0.1, 0.2, 0.3])); const scratch = [new Float32Array(3)]; r.get("kick")!.read(scratch); expect(Array.from(scratch[0]!)).toEqual([ expect.closeTo(0.1, 6), expect.closeTo(0.2, 6), expect.closeTo(0.3, 6), ]); }); it("reads silence before any write", { timeout: 5_000 }, () => { const r = new DefaultBusRegistry(); r.publish("kick", 1); const scratch = [new Float32Array(4)]; r.get("kick")!.read(scratch); expect(Array.from(scratch[0]!)).toEqual([0, 0, 0, 0]); }); it("pads with silence when scratch is larger than the written frame", { timeout: 5_000, }, () => { const r = new DefaultBusRegistry(); const pub = r.publish("kick", 1); pub.write(frame([1, 1])); const scratch = [new Float32Array(4)]; r.get("kick")!.read(scratch); expect(Array.from(scratch[0]!)).toEqual([1, 1, 0, 0]); }); it("reuses the accumulator when consecutive writes are the same size", { timeout: 5_000, }, () => { const r = new DefaultBusRegistry(); const pub = r.publish("kick", 1); pub.write(frame([1, 2, 3])); pub.write(frame([4, 5, 6])); // same size — accumulator reused const scratch = [new Float32Array(3)]; r.get("kick")!.read(scratch); expect(Array.from(scratch[0]!)).toEqual([4, 5, 6]); }); }); describe("DefaultBusRegistry: channel-count enforcement", () => { it("throws BUS_CHANNEL_MISMATCH when read scratch length differs", { timeout: 5_000, }, () => { const r = new DefaultBusRegistry(); r.publish("kick", 2); try { r.get("kick")!.read([new Float32Array(4)]); } catch (error) { expect((error as AudioFxError).code).toBe( AUDIO_FX_CODES.BUS_CHANNEL_MISMATCH ); expect((error as AudioFxError).context).toEqual({ bus: "kick", expected: 2, actual: 1, }); return; } throw new Error("expected BUS_CHANNEL_MISMATCH"); }); it("throws BUS_CHANNEL_MISMATCH when write frame length differs", { timeout: 5_000, }, () => { const r = new DefaultBusRegistry(); const pub = r.publish("kick", 1); try { pub.write(frame([1, 1], 2)); } catch (error) { expect((error as AudioFxError).code).toBe( AUDIO_FX_CODES.BUS_CHANNEL_MISMATCH ); return; } throw new Error("expected BUS_CHANNEL_MISMATCH"); }); }); describe("DefaultBusRegistry: close semantics", () => { it("close removes the bus from get/list and reads as silence", { timeout: 5_000, }, () => { const r = new DefaultBusRegistry(); const pub = r.publish("kick", 1); const bus = pub.bus; pub.write(frame([1, 1])); pub.close(); expect(r.get("kick")).toBeUndefined(); expect(r.list()).toEqual([]); // The local bus handle is still callable but reads silence. const scratch = [new Float32Array(2)]; bus.read(scratch); expect(Array.from(scratch[0]!)).toEqual([0, 0]); }); it("close is idempotent", { timeout: 5_000 }, () => { const r = new DefaultBusRegistry(); const pub = r.publish("kick", 1); pub.close(); // second close must be a no-op expect(() => pub.close()).not.toThrow(); }); it("write on a closed publisher is a no-op", { timeout: 5_000 }, () => { const r = new DefaultBusRegistry(); const pub = r.publish("kick", 1); pub.close(); expect(() => pub.write(frame([1]))).not.toThrow(); }); it("republishing the same name closes the previous publisher", { timeout: 5_000, }, () => { const r = new DefaultBusRegistry(); const first = r.publish("kick", 1); const events: Array = []; r.subscribe("kick", (b) => events.push(b)); const second = r.publish("kick", 2); expect(r.get("kick")).toBe(second.bus); expect(first.bus.channels).toBe(1); // subscribers see: close of first (undefined), then second (bus) expect(events.length).toBe(2); expect(events[0]).toBeUndefined(); expect(events[1]).toBe(second.bus); }); }); describe("DefaultBusRegistry: subscribe", () => { it("fires the listener on publish and on close", { timeout: 5_000, }, () => { const r = new DefaultBusRegistry(); const events: Array = []; const unsub = r.subscribe("kick", (b) => events.push(b)); const pub = r.publish("kick", 1); pub.close(); unsub(); expect(events.length).toBe(2); expect(events[0]).toBe(pub.bus); expect(events[1]).toBeUndefined(); }); it("multiple listeners on the same name all fire", { timeout: 5_000, }, () => { const r = new DefaultBusRegistry(); let countA = 0; let countB = 0; r.subscribe("kick", () => countA++); r.subscribe("kick", () => countB++); r.publish("kick", 1); expect(countA).toBe(1); expect(countB).toBe(1); }); it("unsubscribe removes only that listener; cleans up empty buckets", { timeout: 5_000, }, () => { const r = new DefaultBusRegistry(); let countA = 0; let countB = 0; const unsubA = r.subscribe("kick", () => countA++); r.subscribe("kick", () => countB++); unsubA(); r.publish("kick", 1); expect(countA).toBe(0); expect(countB).toBe(1); }); it("subscribe before any publish is harmless", { timeout: 5_000, }, () => { const r = new DefaultBusRegistry(); const events: Array = []; r.subscribe("kick", (b) => events.push(b)); expect(events.length).toBe(0); r.publish("kick", 1); expect(events.length).toBe(1); }); });