Files
fusion/packages/dashboard/app/utils/__tests__/resumeInstrumentation.test.ts
Fusion (runfusion.ai) 1913cb59fe feat(FN-5389): add resume event instrumentation with SSE, hooks, and diagno
FN-5389 adds dashboard resume event instrumentation: a `resumeInstrumentation` utility captures SSE resume signals, wired through `useChat`, `useChatRooms`, and `useTasks` hooks, with remount markers in `Board` and `ChatView`; diagnostics routes expose resume events for observability, documented in

Fusion-Task-Id: FN-5389
2026-05-20 19:58:49 -07:00

125 lines
3.8 KiB
TypeScript

/**
* @vitest-environment jsdom
*/
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
clearResumeEvents,
getResumeEvents,
recordResumeEvent,
setResumeInstrumentationEnabled,
type ResumeTrigger,
} from "../resumeInstrumentation";
const triggers: ResumeTrigger[] = [
"visibility",
"pageshow",
"sse-error",
"sse-reconnect",
"sse-open",
"remount",
"route-active",
"route-inactive",
"project-context-change",
];
describe("resumeInstrumentation", () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-05-20T12:00:00.000Z"));
clearResumeEvents();
setResumeInstrumentationEnabled(true);
});
afterEach(() => {
vi.useRealTimers();
});
it("records all trigger values", () => {
for (const trigger of triggers) {
recordResumeEvent({ view: "test", trigger, replayAttempted: false });
}
expect(getResumeEvents().map((event) => event.trigger)).toEqual(triggers);
});
it("computes gapMs with fake timers", () => {
const first = recordResumeEvent({ view: "gap", trigger: "visibility", replayAttempted: false });
expect(first.gapMs).toBeUndefined();
vi.advanceTimersByTime(250);
const second = recordResumeEvent({ view: "gap", trigger: "visibility", replayAttempted: false });
expect(second.gapMs).toBe(250);
});
it("isolates per-view gaps while falling back to global baseline", () => {
recordResumeEvent({ view: "a", trigger: "remount", replayAttempted: false, now: 1000 });
const other = recordResumeEvent({ view: "b", trigger: "remount", replayAttempted: false, now: 1500 });
const againA = recordResumeEvent({ view: "a", trigger: "route-active", replayAttempted: false, now: 1800 });
expect(other.gapMs).toBe(500);
expect(againA.gapMs).toBe(800);
});
it("caps ring at 500", () => {
for (let i = 0; i < 520; i += 1) {
recordResumeEvent({
view: "cap",
trigger: "visibility",
replayAttempted: false,
detail: { i },
});
}
const events = getResumeEvents();
expect(events).toHaveLength(500);
expect(events[0]?.detail).toEqual({ i: 20 });
expect(events[499]?.detail).toEqual({ i: 519 });
});
it("is a no-op for buffering and posting when disabled", async () => {
const fetchSpy = vi.fn().mockResolvedValue({ ok: true });
vi.stubGlobal("fetch", fetchSpy);
setResumeInstrumentationEnabled(false);
const event = recordResumeEvent({ view: "disabled", trigger: "visibility", replayAttempted: false });
expect(event.ts).toBeDefined();
expect(getResumeEvents()).toHaveLength(0);
await vi.runAllTimersAsync();
expect(fetchSpy).not.toHaveBeenCalled();
});
it("posts batched events with payload cap of 25", async () => {
const fetchSpy = vi.fn().mockResolvedValue({ ok: true });
vi.stubGlobal("fetch", fetchSpy);
for (let i = 0; i < 30; i += 1) {
recordResumeEvent({
view: "batch",
trigger: "sse-open",
replayAttempted: false,
detail: { i },
});
}
await vi.runAllTimersAsync();
expect(fetchSpy).toHaveBeenCalledTimes(2);
const firstBody = JSON.parse(String(fetchSpy.mock.calls[0]?.[1]?.body));
const secondBody = JSON.parse(String(fetchSpy.mock.calls[1]?.[1]?.body));
expect(firstBody.events).toHaveLength(25);
expect(secondBody.events).toHaveLength(5);
expect(fetchSpy.mock.calls[0]?.[0]).toBe("/api/diagnostics/resume-events");
});
it("swallows network failures", async () => {
const fetchSpy = vi.fn().mockRejectedValue(new Error("offline"));
vi.stubGlobal("fetch", fetchSpy);
recordResumeEvent({ view: "network", trigger: "sse-error", replayAttempted: false });
await vi.runAllTimersAsync();
expect(fetchSpy).toHaveBeenCalledTimes(1);
});
});