/** * useTelemetry + useRunList — source-picker behavior. * * Tests the repointed endpoints (/__nwire/telemetry/live SSE tail + runs API) * and the live-vs-history mode switch. */ import { describe, it, expect, vi } from "vitest"; import { effectScope, nextTick, ref } from "vue"; import { useTelemetry, useRunList, type EventSourceLike, type TelemetryRunMeta, RUNS_URL, } from "../useTelemetry"; // ── Fake EventSource ────────────────────────────────────────────────────────── 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); } emit(type: string, data: unknown): void { for (const l of this.listeners[type] ?? []) { l({ data: JSON.stringify(data) }); } } close(): void { this.closed = true; } } const flush = () => new Promise((r) => setTimeout(r, 10)); // ── Helpers ─────────────────────────────────────────────────────────────────── const REC = (kind: string, n = "1") => JSON.stringify({ kind, envelope: { messageId: `m${n}`, causationId: `m${n}`, correlationId: "c1" }, }); const RUN_META: TelemetryRunMeta = { id: "2026-06-18T20-00-00-000Z-abcdef", size: 100, mtime: "2026-06-18T20:00:00.000Z", }; function makeFetch(runsResp?: TelemetryRunMeta[], runRecords?: unknown[]) { return vi.fn(async (url: string) => { if (url.startsWith(RUNS_URL + "/")) { return { ok: true, json: async () => ({ id: "2026-06-18T20-00-00-000Z-abcdef", records: runRecords ?? [] }), } as unknown as Response; } if (url === RUNS_URL || url.startsWith(RUNS_URL + "?")) { return { ok: true, json: async () => ({ runs: runsResp ?? [] }), } as unknown as Response; } return { ok: false, status: 404, json: async () => ({}) } as unknown as Response; }); } function liveTelemetryHarness(opts: Parameters[1] = {}) { const created: FakeES[] = []; const factory = (url: string) => { const es = new FakeES(url); created.push(es); return es; }; const scope = effectScope(); const api = scope.run(() => useTelemetry(undefined, { eventSourceFactory: factory, fetchFn: makeFetch(), reconnectMs: 1, ...opts, }), )!; return { api, created, scope }; } // ── Tests ───────────────────────────────────────────────────────────────────── describe("useTelemetry — live mode (default)", () => { it("opens an SSE connection to /__nwire/telemetry/live", async () => { const { api, created, scope } = liveTelemetryHarness(); await flush(); expect(created).toHaveLength(1); expect(created[0]!.lastUrl).toBe("/__nwire/telemetry/live"); expect(api.source.value).toBe("live"); scope.stop(); }); it("pushes default-event records into the ring buffer", async () => { const { api, created, scope } = liveTelemetryHarness(); await flush(); created[0]!.onmessage?.({ data: REC("action.completed") }); expect(api.records.value).toHaveLength(1); expect(api.records.value[0]!.kind).toBe("action.completed"); scope.stop(); }); it("stores the runId from the run-meta SSE event", async () => { const { api, created, scope } = liveTelemetryHarness(); await flush(); created[0]!.emit("run", RUN_META); expect(api.currentRunId.value).toBe(RUN_META.id); scope.stop(); }); it("reconnects on error", async () => { const { api, created, scope } = liveTelemetryHarness(); await flush(); created[0]!.onerror?.(); expect(created[0]!.closed).toBe(true); expect(api.status.value).toBe("reconnecting"); await flush(); expect(created.length).toBeGreaterThanOrEqual(2); scope.stop(); }); it("caps the ring buffer at limit", async () => { const { api, created, scope } = liveTelemetryHarness({ limit: 3 }); await flush(); for (let i = 0; i < 6; i++) created[0]!.onmessage?.({ data: REC("event.published", String(i)) }); expect(api.records.value).toHaveLength(3); scope.stop(); }); }); describe("useTelemetry — history mode", () => { it("fetches run records when source is set to a TelemetryRunMeta", async () => { const runRecords = [ { kind: "action.completed", envelope: { messageId: "m1", causationId: "m1", correlationId: "c1" }, }, { kind: "event.published", envelope: { messageId: "m2", causationId: "m1", correlationId: "c1" }, }, ]; const fetchFn = makeFetch([], runRecords); const created: FakeES[] = []; const scope = effectScope(); const api = scope.run(() => useTelemetry(undefined, { eventSourceFactory: (url) => { const es = new FakeES(url); created.push(es); return es; }, fetchFn, reconnectMs: 1, }), )!; await flush(); // Switch to history mode. api.source.value = RUN_META; await flush(); expect(fetchFn).toHaveBeenCalledWith( expect.stringContaining(`${RUNS_URL}/${encodeURIComponent(RUN_META.id)}`), ); expect(api.records.value).toHaveLength(2); expect(api.records.value[0]!.kind).toBe("action.completed"); expect(api.status.value).toBe("open"); expect(api.currentRunId.value).toBe(RUN_META.id); scope.stop(); }); it("switches back to live mode when source is set to 'live'", async () => { const fetchFn = makeFetch([], [{ kind: "hook.step" }]); const created: FakeES[] = []; const scope = effectScope(); const api = scope.run(() => useTelemetry(undefined, { eventSourceFactory: (url) => { const es = new FakeES(url); created.push(es); return es; }, fetchFn, reconnectMs: 1, }), )!; await flush(); const liveCount = created.length; // Switch to history. api.source.value = RUN_META; await flush(); expect(api.records.value).toHaveLength(1); // Switch back to live. api.source.value = "live"; await flush(); expect(api.records.value).toHaveLength(0); // cleared on switch expect(created.length).toBeGreaterThan(liveCount); // new SSE opened scope.stop(); }); it("sets status to closed when history fetch fails", async () => { const fetchFn = vi.fn(async () => ({ ok: false, status: 404 }) as unknown as Response); const scope = effectScope(); const api = scope.run(() => useTelemetry(undefined, { eventSourceFactory: (url) => new FakeES(url), fetchFn, reconnectMs: 1, autoConnect: false, }), )!; api.source.value = RUN_META; await nextTick(); api.connect(); await flush(); expect(api.status.value).toBe("closed"); scope.stop(); }); }); describe("useRunList", () => { it("fetches runs on mount and exposes them", async () => { const fetchFn = makeFetch([RUN_META]); const scope = effectScope(); const { runs, loading, error } = scope.run(() => useRunList(undefined, { fetchFn }))!; expect(loading.value).toBe(true); await flush(); expect(loading.value).toBe(false); expect(error.value).toBeNull(); expect(runs.value).toHaveLength(1); expect(runs.value[0]!.id).toBe(RUN_META.id); scope.stop(); }); it("sets error when fetch fails", async () => { const fetchFn = vi.fn(async () => ({ ok: false, status: 500 }) as unknown as Response); const scope = effectScope(); const { error, loading } = scope.run(() => useRunList(undefined, { fetchFn }))!; await flush(); expect(loading.value).toBe(false); expect(error.value).toMatch(/500/); scope.stop(); }); it("re-fetches when project changes", async () => { const fetchFn = makeFetch([RUN_META]); const proj = ref("proj-a"); const scope = effectScope(); scope.run(() => useRunList(proj, { fetchFn }))!; await flush(); expect(fetchFn).toHaveBeenCalledTimes(1); proj.value = "proj-b"; await nextTick(); await flush(); expect(fetchFn).toHaveBeenCalledTimes(2); scope.stop(); }); });