The code cited its plan instead of saying why: `E28 review J4`, `(R6 S3b)`, `S1 justified…`,
`deep-review round 5`. That plan lives in the private ops repo, so for every reader of this one the
citation was noise, and a sentence whose subject was the plan step ("round 2 showed…") hid its
lesson behind an id. Each one is gone and the "why" kept: a bare citation is deleted, a sentence
carrying a lesson is reworded around it ("a later attempt showed…").
Measured (`scripts/audit_metrics.py`, product code): 652 lines in 270 files carried an epic code by
the definition the next commit holds (507 / 241 by the old, two-digit-only one) -> 0. Across all
code, tests, scripts and configs: ~1100 epic-code lines plus ~320 sprint/round ids.
Behaviour unchanged, checked mechanically: every changed .py file has the same AST once docstrings
are blanked; css/sh/toml/yml/json files are identical once comments are stripped; the only
non-comment changes in TS/JS are test titles, one description string in a contract table, and five
ESLint messages, whose text is all the gates snapshot diff holds. Not touched: Markdown (docs and
the changelog keep their history), Alembic migrations, TV episode notation (`S2 · E5`), DESIGN.md
ids (`C1`, `B4`, `S3h`), tracker ids (`SEC-16`, `DX-17`).
One stale claim fixed on the way: test_hls_reaper's docstring said the player checkpoints "every
10 s whether or not it is playing", which plex/stream.py calls wrong; it now says what the code does.
164 lines
6.3 KiB
TypeScript
164 lines
6.3 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;
|
|
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", () => {
|
|
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");
|
|
});
|
|
});
|