106 lines
4.0 KiB
TypeScript
106 lines
4.0 KiB
TypeScript
import type { PipelineStageEvent } from "@suaveplan/api-types/pipeline-events";
|
|
import { describe, expect, it } from "@suaveplan/testing/runner";
|
|
import { InMemoryPipelineEventsBus } from "./in-memory-bus.js";
|
|
|
|
function ev(
|
|
requestId: string,
|
|
stage: PipelineStageEvent["stage"],
|
|
outcome: PipelineStageEvent["outcome"],
|
|
at: number
|
|
): PipelineStageEvent {
|
|
return { requestId, stage, outcome, at };
|
|
}
|
|
|
|
describe("InMemoryPipelineEventsBus — FIFO + fan-out semantics", () => {
|
|
it("emit appends to the internal FIFO in order", { timeout: 5000 }, () => {
|
|
const bus = new InMemoryPipelineEventsBus();
|
|
bus.emit(ev("r1", "tenant", "entered", 1));
|
|
bus.emit(ev("r1", "tenant", "completed", 2));
|
|
expect(bus.events.map((e) => e.outcome)).toEqual([
|
|
"entered",
|
|
"completed",
|
|
]);
|
|
expect(bus.length).toBe(2);
|
|
});
|
|
|
|
it("subscribe receives events in emission order", { timeout: 5000 }, () => {
|
|
const bus = new InMemoryPipelineEventsBus();
|
|
const seen: string[] = [];
|
|
bus.subscribe((e) => seen.push(`${e.stage}/${e.outcome}`));
|
|
bus.emit(ev("r1", "auth", "entered", 1));
|
|
bus.emit(ev("r1", "auth", "rejected", 2));
|
|
expect(seen).toEqual(["auth/entered", "auth/rejected"]);
|
|
});
|
|
|
|
it("subscriber throws are isolated — FIFO + remaining subscribers unaffected", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const bus = new InMemoryPipelineEventsBus();
|
|
const survivor: PipelineStageEvent[] = [];
|
|
bus.subscribe(() => {
|
|
throw new Error("boom");
|
|
});
|
|
bus.subscribe((e) => survivor.push(e));
|
|
const event = ev("r1", "csrf", "completed", 1);
|
|
expect(() => bus.emit(event)).not.toThrow();
|
|
expect(survivor).toEqual([event]);
|
|
expect(bus.events).toEqual([event]);
|
|
});
|
|
|
|
it("unsubscribe is idempotent — calling twice does not double-detach", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const bus = new InMemoryPipelineEventsBus();
|
|
const seen: PipelineStageEvent[] = [];
|
|
const off = bus.subscribe((e) => seen.push(e));
|
|
expect(bus.subscriberCount).toBe(1);
|
|
off();
|
|
expect(bus.subscriberCount).toBe(0);
|
|
// Second call is a no-op.
|
|
expect(() => off()).not.toThrow();
|
|
expect(bus.subscriberCount).toBe(0);
|
|
// Re-subscribe to confirm the set still works.
|
|
const off2 = bus.subscribe((e) => seen.push(e));
|
|
bus.emit(ev("r1", "audit", "completed", 1));
|
|
expect(seen).toHaveLength(1);
|
|
off2();
|
|
});
|
|
|
|
it("drain returns + clears the FIFO atomically", { timeout: 5000 }, () => {
|
|
const bus = new InMemoryPipelineEventsBus();
|
|
bus.emit(ev("r1", "tenant", "entered", 1));
|
|
bus.emit(ev("r2", "tenant", "entered", 2));
|
|
const out = bus.drain();
|
|
expect(out).toHaveLength(2);
|
|
expect(bus.length).toBe(0);
|
|
expect(bus.events).toEqual([]);
|
|
});
|
|
|
|
it("byRequest + byStage slice the FIFO correctly", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const bus = new InMemoryPipelineEventsBus();
|
|
bus.emit(ev("r1", "tenant", "entered", 1));
|
|
bus.emit(ev("r2", "tenant", "entered", 2));
|
|
bus.emit(ev("r1", "auth", "entered", 3));
|
|
expect(bus.byRequest("r1")).toHaveLength(2);
|
|
expect(bus.byRequest("r2")).toHaveLength(1);
|
|
expect(bus.byStage("tenant")).toHaveLength(2);
|
|
expect(bus.byStage("auth")).toHaveLength(1);
|
|
expect(bus.byStage("response-validate")).toHaveLength(0);
|
|
});
|
|
|
|
it("events getter returns a fresh array snapshot each call", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const bus = new InMemoryPipelineEventsBus();
|
|
bus.emit(ev("r1", "audit", "completed", 1));
|
|
const snap1 = bus.events;
|
|
// Mutating the snapshot MUST NOT poison the internal FIFO.
|
|
(snap1 as PipelineStageEvent[]).pop();
|
|
expect(bus.length).toBe(1);
|
|
const snap2 = bus.events;
|
|
expect(snap2).toHaveLength(1);
|
|
});
|
|
});
|