Files
siftlode/frontend/src/lib/diagnostics.session.dom.test.ts
T
peter 5ba5679239 test: tests that assumed an idle machine wait for what they assert, or carry a budget for their work
Under a busy machine (the gate's lanes side by side, 12 busy cores) these went red at random: a press landed while the button was still busy, an answer was settled before it was asked for, a page grew past one wait's budget, a lazy chunk was transformed inside a 1 s wait, focus was taken a frame after the press. They now wait for the condition they depend on, preload the chunks they open, and the tests that do heavy work (whole-repo scans, the type-aware linter, module resets) carry an explicit budget naming that work. The whole suite four times under 12 busy-loop processes: red 4/4 before, 0/4 after.
2026-10-17 01:08:15 +02:00

169 lines
6.6 KiB
TypeScript

// @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<typeof import("./api/core")>("./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");
});
});