/** * useTelemetry — live-mode behavior (SSE tail of /__nwire/telemetry/live). * History mode is covered in useTelemetryRuns.test.ts. */ import { describe, it, expect, vi } from "vitest"; import { effectScope } from "vue"; import { useTelemetry, type EventSourceLike } from "../useTelemetry"; class FakeES implements EventSourceLike { onmessage: ((ev: { data: string }) => void) | null = null; onerror: ((ev?: unknown) => void) | null = null; onopen: ((ev?: unknown) => void) | null = null; closed = false; listeners: Record void>> = {}; lastUrl: string; constructor(url: string) { this.lastUrl = url; } addEventListener(type: string, listener: (ev: { data: string }) => void): void { (this.listeners[type] ??= []).push(listener); } close() { this.closed = true; } } const flush = () => new Promise((r) => setTimeout(r, 5)); const rec = (kind: string, messageId = "m1", correlationId = "c1") => JSON.stringify({ kind, envelope: { messageId, causationId: messageId, correlationId } }); function harness(opts: Parameters[1] = {}) { const created: FakeES[] = []; const factory = (url: string) => { const es = new FakeES(url); created.push(es); return es; }; const fetchFn = vi.fn( async () => ({ ok: true, json: async () => ({ runs: [] }) }) as unknown as Response, ); const scope = effectScope(); const api = scope.run(() => useTelemetry(() => "proj", { eventSourceFactory: factory, fetchFn, reconnectMs: 1, ...opts }), )!; return { api, created, fetchFn, scope }; } describe("useTelemetry", () => { it("opens an SSE connection to /__nwire/telemetry/live and streams records", async () => { const { api, created } = harness({ limit: 10 }); await flush(); // Live mode opens an SSE — no fetch for backfill (server does the backfill over SSE). expect(created).toHaveLength(1); expect(created[0]!.lastUrl).toBe("/__nwire/telemetry/live"); created[0]!.onopen?.(); expect(api.status.value).toBe("open"); created[0]!.onmessage?.({ data: rec("event.published", "m2") }); created[0]!.onmessage?.({ data: rec("listener.fired", "m3") }); expect(api.records.value.map((r) => r.kind)).toEqual(["event.published", "listener.fired"]); expect(api.recent("event.published")).toHaveLength(1); }); it("caps the ring buffer at `limit`", async () => { const { api, created } = harness({ limit: 2 }); await flush(); for (let i = 0; i < 5; i++) created[0]!.onmessage?.({ data: rec("event.published", `m${i}`) }); expect(api.records.value).toHaveLength(2); }); it("reconnects on error (closes the dead source, opens a fresh one)", async () => { const { api, created } = harness(); await flush(); expect(created).toHaveLength(1); created[0]!.onerror?.(); expect(created[0]!.closed).toBe(true); expect(api.status.value).toBe("reconnecting"); await flush(); expect(created.length).toBeGreaterThanOrEqual(2); // a new source was opened }); it("disconnect() closes the source and stops", async () => { const { api, created } = harness(); await flush(); api.disconnect(); expect(created[0]!.closed).toBe(true); expect(api.status.value).toBe("closed"); }); it("derives the correlation tree from buffered records", async () => { const { api, created } = harness(); await flush(); created[0]!.onmessage?.({ data: rec("action.dispatched", "m1") }); created[0]!.onmessage?.({ data: JSON.stringify({ kind: "event.published", envelope: { messageId: "m2", causationId: "m1", correlationId: "c1" }, }), }); expect(api.tree.value).toHaveLength(1); expect(api.tree.value[0]!.children).toHaveLength(1); }); });