// @vitest-environment jsdom // // A debug session applied to the tab. It raises the level for its own run (starting it was the // consent), never lowers a raised one, tags every event it keeps, and its last seconds reach the // server before Stop. Its own file: `lib/diagnostics` and `lib/log` keep module-level state, so every // test imports fresh copies of both, as diagnostics.dom.test does. import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const sent: { event: string; debug_session_id?: string }[][] = []; vi.mock("./api", async () => { const core = await vi.importActual("./api/core"); return { HttpError: core.HttpError, DIAGNOSTICS_EVENTS_PATH: "/api/diagnostics/events", api: { sendLogEvents: (events: { event: string }[]) => { sent.push(events); return Promise.resolve({ ok: true, stored: events.length }); }, sendLogEventsOnUnload: () => {}, }, }; }); const BASE = { level: "INFO" as const, until: null, retention_days: 7, session_minutes: 60 }; const raised = (level: "DEBUG" | "TRACE") => ({ ...BASE, level, until: new Date(Date.now() + 24 * 3_600_000).toISOString(), }); const recording = (level: "DEBUG" | "TRACE", minutes = 60, over = {}) => ({ id: "0123456789abcdef", level, started_at: new Date().toISOString(), until: new Date(Date.now() + minutes * 60_000).toISOString(), ended_at: null, ended_reason: null, issue_url: null, pending_files: [], images_pending: false, sending: false, screenshots: 0, draft: null, ...over, }); // Warm the transform cache once (see verifyEntry.dom.test.tsx): the first cold import costs ~0.8 s // alone and outran the 5 s case timeout under a full coverage run. Transformed code outlives // vi.resetModules, so each case still gets fresh module instances, only faster. beforeAll(async () => { await import("./diagnostics"); await import("./log"); }, 60_000); let current: typeof import("./diagnostics") | null = null; // Warm or not, each case still loads and evaluates the modules it imports afresh after its module // reset: tens of ms idle, but the same reload put sibling suites past 5 s when the full suite's // workers share a busy machine. const CASE_BUDGET = 30_000; beforeEach(() => { vi.useFakeTimers(); vi.resetModules(); sent.length = 0; }); afterEach(() => { current?.applyDiagnostics(BASE, null, () => {}); current = null; vi.useRealTimers(); }); async function fresh() { const diagnostics = await import("./diagnostics"); current = diagnostics; return { ...diagnostics, ...(await import("./log")) }; } const flat = () => sent.flat(); describe("a debug session applied to the tab", { timeout: CASE_BUDGET }, () => { it("raises the base level to its own, and tags and streams what it keeps", async () => { const d = await fresh(); d.applyDiagnostics(BASE, recording("DEBUG"), () => {}); expect(d.enabled("DEBUG")).toBe(true); expect(d.currentDebugSession()).toBe("0123456789abcdef"); d.log.debug("ui.click", "click"); expect(d.events().slice(-1)[0]?.debug_session_id).toBe("0123456789abcdef"); await vi.advanceTimersByTimeAsync(5_000); const click = flat().find((e) => e.event === "ui.click"); expect(click?.debug_session_id).toBe("0123456789abcdef"); }); it("never lowers a level the user raised further", async () => { const d = await fresh(); d.applyDiagnostics(raised("TRACE"), recording("DEBUG"), () => {}); expect(d.enabled("TRACE")).toBe(true); expect(d.currentDebugSession()).toBe("0123456789abcdef"); }); it("an ended session records nothing and tags nothing", async () => { const d = await fresh(); d.applyDiagnostics( BASE, recording("DEBUG", 60, { ended_at: new Date().toISOString() }), () => {}, ); expect(d.currentDebugSession()).toBeNull(); expect(d.enabled("DEBUG")).toBe(false); }); it("at its cap it lapses back to the user's own level, untagged", async () => { const d = await fresh(); const lapsed = vi.fn(); d.applyDiagnostics(raised("DEBUG"), recording("TRACE", 1), lapsed); expect(d.enabled("TRACE")).toBe(true); await vi.advanceTimersByTimeAsync(60_000); expect(d.currentDebugSession()).toBeNull(); // The user's own Részletes is still in force — the session's end is not the consent's. expect(d.enabled("DEBUG")).toBe(true); expect(d.enabled("TRACE")).toBe(false); expect(lapsed).toHaveBeenCalled(); }); it("at its cap the tag goes even when the user's own level outlasts the session", async () => { // Own Teljes for 24 h, a Részletes session for 1 min: the level never changes, so only the // session's own end can take its tag off the events that follow. const d = await fresh(); d.applyDiagnostics(raised("TRACE"), recording("DEBUG", 1), () => {}); expect(d.currentDebugSession()).toBe("0123456789abcdef"); await vi.advanceTimersByTimeAsync(60_000); expect(d.currentDebugSession()).toBeNull(); expect(d.enabled("TRACE")).toBe(true); d.log.debug("ui.after", "after"); expect(d.events().slice(-1)[0]?.debug_session_id).toBeUndefined(); }); it("starting a recording on a raised level drops nothing already waiting", async () => { const d = await fresh(); d.applyDiagnostics(raised("DEBUG"), null, () => {}); d.log.debug("ui.before", "before"); d.applyDiagnostics(raised("DEBUG"), recording("TRACE"), () => {}); d.log.debug("ui.after", "after"); await vi.advanceTimersByTimeAsync(5_000); const codes = flat().map((e) => e.event); expect(codes).toEqual(expect.arrayContaining(["ui.before", "ui.after"])); }); it("its last seconds are sent before its cap, while the server still tags them", async () => { const d = await fresh(); d.applyDiagnostics(BASE, recording("DEBUG", 1), () => {}); await vi.advanceTimersByTimeAsync(57_000); // the regular flushes ran; the next is past the cap d.log.info("debug.step", "the last step"); await vi.advanceTimersByTimeAsync(2_000); // 59 s: the cap is still a second away const step = flat().find((e) => e.event === "debug.step"); expect(step?.debug_session_id).toBe("0123456789abcdef"); }); it("flushNow sends what is waiting at once — before Stop closes the session", async () => { const d = await fresh(); d.applyDiagnostics(BASE, recording("DEBUG"), () => {}); d.log.info("debug.step", "the last step"); expect(flat().some((e) => e.event === "debug.step")).toBe(false); await d.flushNow(); const step = flat().find((e) => e.event === "debug.step"); expect(step?.debug_session_id).toBe("0123456789abcdef"); }); });