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.
321 lines
13 KiB
TypeScript
321 lines
13 KiB
TypeScript
// @vitest-environment jsdom
|
|
//
|
|
// The consented level applied to the tab, and the stream it opens. The module keeps its state
|
|
// at module level (what is applied, the queue), so every test imports a fresh copy — and the log
|
|
// from that same copy, or it would be reading a different ring.
|
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const sent: unknown[][] = [];
|
|
const unloaded: unknown[][] = [];
|
|
let answer: () => Promise<unknown> = () => Promise.resolve({ ok: true, stored: 0 });
|
|
|
|
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: unknown[]) => {
|
|
sent.push(events);
|
|
return answer();
|
|
},
|
|
sendLogEventsOnUnload: (events: unknown[]) => void unloaded.push(events),
|
|
},
|
|
};
|
|
});
|
|
|
|
// 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");
|
|
await import("./api");
|
|
}, 60_000);
|
|
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
vi.resetModules();
|
|
sent.length = 0;
|
|
unloaded.length = 0;
|
|
answer = () => Promise.resolve({ ok: true, stored: 0 });
|
|
});
|
|
let current: typeof import("./diagnostics") | null = null;
|
|
afterEach(() => {
|
|
// A module copy left streaming would still hear `pagehide` in the next test.
|
|
current?.applyDiagnostics(
|
|
{ level: "INFO", until: null, retention_days: 7, session_minutes: 60 },
|
|
null,
|
|
() => {},
|
|
);
|
|
current = null;
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
async function fresh() {
|
|
const diagnostics = await import("./diagnostics");
|
|
current = diagnostics;
|
|
const logMod = await import("./log");
|
|
const apiMod = await import("./api");
|
|
return { ...diagnostics, ...logMod, HttpError: apiMod.HttpError };
|
|
}
|
|
|
|
const raised = (level: "DEBUG" | "TRACE", hours = 24) => ({
|
|
level,
|
|
until: new Date(Date.now() + hours * 3_600_000).toISOString(),
|
|
retention_days: 7,
|
|
session_minutes: 60,
|
|
});
|
|
const codes = (batch: unknown[] | undefined) =>
|
|
(batch ?? []).map((e) => (e as { event: string }).event);
|
|
|
|
describe("diagnostics level", () => {
|
|
it("the base level at boot sets INFO, streams nothing and logs nothing", async () => {
|
|
const d = await fresh();
|
|
d.applyDiagnostics(
|
|
{ level: "INFO", until: null, retention_days: 7, session_minutes: 60 },
|
|
null,
|
|
() => {},
|
|
);
|
|
d.log.info("nav.view", "x");
|
|
await vi.advanceTimersByTimeAsync(60_000);
|
|
expect(sent).toEqual([]);
|
|
expect(d.enabled("DEBUG")).toBe(false);
|
|
expect(codes(d.events())).toEqual(["nav.view"]);
|
|
});
|
|
|
|
it("a raised level streams only what happens after it, without its own request lines", async () => {
|
|
const d = await fresh();
|
|
d.log.info("before.consent", "not sent");
|
|
d.applyDiagnostics(raised("DEBUG"), null, () => {});
|
|
expect(d.enabled("DEBUG")).toBe(true);
|
|
d.log.debug("ui.click", "click");
|
|
d.log.request("INFO", "POST", { path: "/api/diagnostics/events", status: 200 }, "abcdef0123");
|
|
d.log.request("INFO", "GET", { path: "/api/feed", status: 200 }, "abcdef0124");
|
|
await vi.advanceTimersByTimeAsync(5_000);
|
|
expect(sent).toHaveLength(1);
|
|
expect(codes(sent[0])).toEqual(["diagnostics.level", "ui.click", "http.request"]);
|
|
expect(JSON.stringify(sent[0])).not.toContain("/api/diagnostics/events");
|
|
});
|
|
|
|
it("sends in batches of at most 100, and a full batch goes without waiting", async () => {
|
|
const d = await fresh();
|
|
d.applyDiagnostics(raised("DEBUG"), null, () => {});
|
|
for (let i = 0; i < 250; i++) d.log.debug("ui.click", `click ${i}`);
|
|
await vi.advanceTimersByTimeAsync(0);
|
|
expect(sent.map((b) => b.length)).toEqual([100]);
|
|
await vi.advanceTimersByTimeAsync(5_000);
|
|
await vi.advanceTimersByTimeAsync(5_000);
|
|
expect(sent.every((b) => b.length <= 100)).toBe(true);
|
|
expect(sent.flat()).toHaveLength(251); // the 250 clicks + the line that opened the stream
|
|
});
|
|
|
|
it("stops at once when the level is lowered, and drops what was not sent yet", async () => {
|
|
const d = await fresh();
|
|
d.applyDiagnostics(raised("TRACE"), null, () => {});
|
|
d.log.trace("search.typed", "lofi");
|
|
d.applyDiagnostics(
|
|
{ level: "INFO", until: null, retention_days: 7, session_minutes: 60 },
|
|
null,
|
|
() => {},
|
|
);
|
|
d.log.info("nav.view", "after");
|
|
await vi.advanceTimersByTimeAsync(60_000);
|
|
expect(sent).toEqual([]);
|
|
expect(d.enabled("TRACE")).toBe(false);
|
|
});
|
|
|
|
it("lapses on its own timer and tells the app to refetch", async () => {
|
|
const d = await fresh();
|
|
const lapsed = vi.fn();
|
|
d.applyDiagnostics(raised("DEBUG", 1), null, lapsed);
|
|
await vi.advanceTimersByTimeAsync(3_600_000 + 1);
|
|
expect(lapsed).toHaveBeenCalledOnce();
|
|
expect(d.enabled("DEBUG")).toBe(false);
|
|
sent.length = 0;
|
|
d.log.info("nav.view", "after the lapse");
|
|
await vi.advanceTimersByTimeAsync(10_000);
|
|
expect(sent).toEqual([]);
|
|
});
|
|
|
|
it("an already-expired level is the base level", async () => {
|
|
const d = await fresh();
|
|
d.applyDiagnostics(raised("DEBUG", -1), null, () => {});
|
|
expect(d.enabled("DEBUG")).toBe(false);
|
|
});
|
|
|
|
it("the server saying it is off stops the stream; being told to slow down keeps the batch", async () => {
|
|
const d = await fresh();
|
|
const lapsed = vi.fn();
|
|
d.applyDiagnostics(raised("DEBUG"), null, lapsed);
|
|
answer = () => Promise.reject(new d.HttpError(429, "slow down"));
|
|
d.log.debug("ui.click", "kept for later");
|
|
await vi.advanceTimersByTimeAsync(5_000);
|
|
answer = () => Promise.reject(new d.HttpError(409, "off"));
|
|
await vi.advanceTimersByTimeAsync(15_000);
|
|
expect(codes(sent[1])).toEqual(codes(sent[0])); // the same batch, retried after the back-off
|
|
expect(lapsed).toHaveBeenCalledOnce();
|
|
expect(d.enabled("DEBUG")).toBe(false);
|
|
});
|
|
|
|
it("after a 429 nothing is sent until the back-off is over, however full the queue", async () => {
|
|
const d = await fresh();
|
|
d.applyDiagnostics(raised("DEBUG"), null, () => {});
|
|
answer = () => Promise.reject(new d.HttpError(429, "slow down"));
|
|
for (let i = 0; i < 99; i++) d.log.debug("ui.click", `click ${i}`);
|
|
await vi.advanceTimersByTimeAsync(0);
|
|
expect(sent).toHaveLength(1); // the full batch went at once, and was refused
|
|
answer = () => Promise.resolve({ ok: true, stored: 0 });
|
|
for (let i = 0; i < 50; i++) d.log.debug("ui.click", `more ${i}`);
|
|
await vi.advanceTimersByTimeAsync(14_000);
|
|
expect(sent).toHaveLength(1); // one POST per new event is exactly the storm to avoid
|
|
await vi.advanceTimersByTimeAsync(1_000);
|
|
expect(codes(sent[1])).toEqual(codes(sent[0]));
|
|
});
|
|
|
|
it("past the queue cap the oldest events go, a refused batch included", async () => {
|
|
const d = await fresh();
|
|
d.applyDiagnostics(raised("DEBUG"), null, () => {});
|
|
answer = () => Promise.reject(new d.HttpError(429, "slow down"));
|
|
for (let i = 0; i < 99; i++) d.log.debug("ui.click", `click ${i}`);
|
|
await vi.advanceTimersByTimeAsync(0);
|
|
answer = () => Promise.resolve({ ok: true, stored: 0 });
|
|
for (let i = 99; i < 1_100; i++) d.log.debug("ui.click", `click ${i}`);
|
|
await vi.advanceTimersByTimeAsync(60_000);
|
|
const msgs = sent
|
|
.slice(1)
|
|
.flat()
|
|
.map((e) => (e as { msg: string }).msg);
|
|
expect(msgs).toHaveLength(1_000);
|
|
expect(msgs[0]).toBe("click 100");
|
|
expect(msgs[msgs.length - 1]).toBe("click 1099");
|
|
});
|
|
|
|
it("a batch refused after the level was lowered is not sent under a later consent", async () => {
|
|
const d = await fresh();
|
|
let refuse: (err: unknown) => void = () => {};
|
|
answer = () => new Promise((_, reject) => (refuse = reject));
|
|
d.applyDiagnostics(raised("DEBUG"), null, () => {});
|
|
d.log.debug("ui.click", "under the first consent");
|
|
await vi.advanceTimersByTimeAsync(5_000); // in flight
|
|
d.applyDiagnostics(
|
|
{ level: "INFO", until: null, retention_days: 7, session_minutes: 60 },
|
|
null,
|
|
() => {},
|
|
);
|
|
refuse(new d.HttpError(429, "slow down"));
|
|
await vi.advanceTimersByTimeAsync(0);
|
|
answer = () => Promise.resolve({ ok: true, stored: 0 });
|
|
d.applyDiagnostics(raised("TRACE"), null, () => {});
|
|
await vi.advanceTimersByTimeAsync(60_000);
|
|
const later = sent
|
|
.slice(1)
|
|
.flat()
|
|
.map((e) => (e as { msg: string }).msg);
|
|
expect(later).not.toContain("under the first consent");
|
|
expect(later).toContain("diagnostics level raised");
|
|
});
|
|
|
|
it("a batch refused after a re-raise is still not sent: it was taken under the old consent", async () => {
|
|
// The test above settles the refusal while the stream is STOPPED, so `unsubscribe` is what
|
|
// blocks the requeue there. Here it settles after the new consent is already streaming — the
|
|
// ordering only the `session` counter can catch, and the one that would leak events across a
|
|
// withdrawal if it did not.
|
|
const d = await fresh();
|
|
let refuse: (err: unknown) => void = () => {};
|
|
answer = () => new Promise((_, reject) => (refuse = reject));
|
|
d.applyDiagnostics(raised("DEBUG"), null, () => {});
|
|
d.log.debug("ui.click", "under the first consent");
|
|
await vi.advanceTimersByTimeAsync(5_000); // in flight
|
|
d.applyDiagnostics(
|
|
{ level: "INFO", until: null, retention_days: 7, session_minutes: 60 },
|
|
null,
|
|
() => {},
|
|
);
|
|
answer = () => Promise.resolve({ ok: true, stored: 0 });
|
|
d.applyDiagnostics(raised("TRACE"), null, () => {});
|
|
refuse(new d.HttpError(429, "slow down")); // only now does the old consent's batch come back
|
|
await vi.advanceTimersByTimeAsync(60_000);
|
|
const later = sent
|
|
.slice(1)
|
|
.flat()
|
|
.map((e) => (e as { msg: string }).msg);
|
|
expect(later).not.toContain("under the first consent");
|
|
expect(later).toContain("diagnostics level raised");
|
|
});
|
|
|
|
it("a 409 for a batch taken under the old consent does not lapse the new one", async () => {
|
|
const d = await fresh();
|
|
const lapsed = vi.fn();
|
|
let refuse: (err: unknown) => void = () => {};
|
|
answer = () => new Promise((_, reject) => (refuse = reject));
|
|
d.applyDiagnostics(raised("DEBUG"), null, lapsed);
|
|
d.log.debug("ui.click", "under the first consent");
|
|
await vi.advanceTimersByTimeAsync(5_000); // in flight
|
|
d.applyDiagnostics(
|
|
{ level: "INFO", until: null, retention_days: 7, session_minutes: 60 },
|
|
null,
|
|
lapsed,
|
|
);
|
|
answer = () => Promise.resolve({ ok: true, stored: 0 });
|
|
d.applyDiagnostics(raised("TRACE"), null, lapsed);
|
|
refuse(new d.HttpError(409, "off"));
|
|
await vi.advanceTimersByTimeAsync(60_000);
|
|
expect(lapsed).not.toHaveBeenCalled();
|
|
expect(d.enabled("TRACE")).toBe(true);
|
|
});
|
|
|
|
it("a 409 answering a batch the user themselves cancelled is not called a lapse", async () => {
|
|
// "Alap" is the user revoking their own consent; the server answers the still-flying batch with
|
|
// the same 409 a real expiry gets. Their own log must not tell them the consent ran out.
|
|
const d = await fresh();
|
|
const lapsed = vi.fn();
|
|
let refuse: (err: unknown) => void = () => {};
|
|
answer = () => new Promise((_, reject) => (refuse = reject));
|
|
d.applyDiagnostics(raised("DEBUG"), null, lapsed);
|
|
d.log.debug("ui.click", "under the consent");
|
|
await vi.advanceTimersByTimeAsync(5_000); // in flight
|
|
d.applyDiagnostics(
|
|
{ level: "INFO", until: null, retention_days: 7, session_minutes: 60 },
|
|
null,
|
|
lapsed,
|
|
);
|
|
refuse(new d.HttpError(409, "off"));
|
|
await vi.advanceTimersByTimeAsync(0);
|
|
expect(lapsed).not.toHaveBeenCalled();
|
|
expect(d.events().map((e) => e.msg)).not.toContain("diagnostics level lapsed");
|
|
});
|
|
|
|
it("a refused batch that comes back to a full queue is what goes, not the events since", async () => {
|
|
// The cap test above fills the queue AFTER the refusal landed, so the trim it measures is the
|
|
// one on the subscribe path. This one has the queue already at the cap when the batch comes
|
|
// back — the only state where the trim in the 429 handler decides anything.
|
|
const d = await fresh();
|
|
let refuse: (err: unknown) => void = () => {};
|
|
answer = () => new Promise((_, reject) => (refuse = reject));
|
|
d.applyDiagnostics(raised("DEBUG"), null, () => {});
|
|
for (let i = 0; i < 99; i++) d.log.debug("ui.click", `old ${i}`);
|
|
await vi.advanceTimersByTimeAsync(0); // the 100 oldest are in flight, the queue is empty
|
|
for (let i = 0; i < 1_000; i++) d.log.debug("ui.click", `new ${i}`);
|
|
answer = () => Promise.resolve({ ok: true, stored: 0 });
|
|
refuse(new d.HttpError(429, "slow down")); // 1_000 + 100 back = 1_100, over the cap
|
|
await vi.advanceTimersByTimeAsync(60_000);
|
|
const msgs = sent
|
|
.slice(1)
|
|
.flat()
|
|
.map((e) => (e as { msg: string }).msg);
|
|
expect(msgs).toHaveLength(1_000);
|
|
expect(msgs).not.toContain("old 0");
|
|
expect([msgs[0], msgs[msgs.length - 1]]).toEqual(["new 0", "new 999"]);
|
|
});
|
|
|
|
it("a closing tab sends what is still waiting with a keepalive request", async () => {
|
|
const d = await fresh();
|
|
d.applyDiagnostics(raised("DEBUG"), null, () => {});
|
|
d.log.debug("ui.click", "last click");
|
|
window.dispatchEvent(new Event("pagehide"));
|
|
expect(codes(unloaded[0])).toEqual(["diagnostics.level", "ui.click"]);
|
|
expect(sent).toEqual([]);
|
|
});
|
|
});
|