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
125 lines
3.8 KiB
TypeScript
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);
|
|
});
|
|
});
|