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.
251 lines
9.8 KiB
TypeScript
251 lines
9.8 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
||
|
||
// format.ts boots i18n on import (localStorage); the formatters here take the language as an argument.
|
||
vi.mock("../i18n", () => ({ default: { t: (k: string) => k } }));
|
||
import {
|
||
buildStatRows,
|
||
createFragMeter,
|
||
fmtBitrate,
|
||
fmtBufferSecs,
|
||
fmtClock,
|
||
fmtCodec,
|
||
fmtFrames,
|
||
fmtLevel,
|
||
fmtMode,
|
||
fmtRes,
|
||
type PlexStatsInput,
|
||
} from "./plexStats";
|
||
|
||
const DASH = "—";
|
||
|
||
describe("plexStats formatters", () => {
|
||
it("fmtBitrate scales kbps/Mbps and dashes zero/undefined", () => {
|
||
expect(fmtBitrate(undefined, "en")).toBe(DASH);
|
||
expect(fmtBitrate(0, "en")).toBe(DASH);
|
||
expect(fmtBitrate(820_000, "en")).toBe("820 kbps");
|
||
expect(fmtBitrate(4_200_000, "en")).toBe("4.2 Mbps");
|
||
expect(fmtBitrate(1_000_000, "en")).toBe("1.0 Mbps");
|
||
expect(fmtBitrate(4_200_000, "hu")).toBe("4,2 Mbps"); // the UI language's decimal mark
|
||
});
|
||
|
||
it("fmtBufferSecs shows one decimal, dashes negatives/non-finite", () => {
|
||
expect(fmtBufferSecs(12.34, "en")).toBe("12.3 sec");
|
||
expect(fmtBufferSecs(0, "en")).toBe("0.0 sec");
|
||
expect(fmtBufferSecs(12.34, "hu")).toBe("12,3 mp");
|
||
expect(fmtBufferSecs(-1, "en")).toBe(DASH);
|
||
expect(fmtBufferSecs(Infinity, "en")).toBe(DASH);
|
||
});
|
||
|
||
it("fmtFrames shows ratio + drop %, dashes when nothing decoded", () => {
|
||
expect(fmtFrames(0, 0, "en")).toBe(DASH);
|
||
expect(fmtFrames(0, 1000, "en")).toBe("0 / 1,000 (0.00%)");
|
||
expect(fmtFrames(5, 1000, "en")).toBe("5 / 1,000 (0.50%)");
|
||
expect(fmtFrames(30, 1000, "en")).toBe("30 / 1,000 (3.0%)");
|
||
expect(fmtFrames(30, 1000, "hu")).toBe("30 / 1000 (3,0%)");
|
||
});
|
||
|
||
it("fmtRes dashes an undecoded 0×0 frame", () => {
|
||
expect(fmtRes(1920, 1080)).toBe("1920×1080");
|
||
expect(fmtRes(0, 0)).toBe(DASH);
|
||
expect(fmtRes(1920, 0)).toBe(DASH);
|
||
});
|
||
|
||
it("fmtClock renders m:ss and dashes bad input", () => {
|
||
expect(fmtClock(0)).toBe("0:00");
|
||
expect(fmtClock(9)).toBe("0:09");
|
||
expect(fmtClock(75)).toBe("1:15");
|
||
expect(fmtClock(3661)).toBe("61:01");
|
||
expect(fmtClock(-1)).toBe(DASH);
|
||
});
|
||
|
||
it("fmtLevel is 1-based with resolution, handles auto and empty", () => {
|
||
expect(fmtLevel(null)).toBe(DASH);
|
||
expect(fmtLevel({ index: 1, total: 4, width: 1280, height: 720 })).toBe("2 / 4 · 1280×720");
|
||
expect(fmtLevel({ index: -1, total: 3 })).toBe("auto / 3");
|
||
expect(fmtLevel({ index: 0, total: 0 })).toBe(DASH);
|
||
});
|
||
|
||
it("fmtCodec strips profile suffixes and dashes when absent", () => {
|
||
expect(fmtCodec("avc1.640028", "mp4a.40.2")).toBe("avc1 / mp4a");
|
||
expect(fmtCodec("hvc1.1.6.L120", undefined)).toBe("hvc1");
|
||
expect(fmtCodec(undefined, undefined)).toBe(DASH);
|
||
});
|
||
});
|
||
|
||
describe("buildStatRows", () => {
|
||
const base: PlexStatsInput = {
|
||
lang: "en",
|
||
playable: "remux",
|
||
transport: "hls",
|
||
videoWidth: 1280,
|
||
videoHeight: 720,
|
||
droppedFrames: 2,
|
||
totalFrames: 500,
|
||
bufferAheadS: 8.2,
|
||
hlsLevel: { index: 0, total: 1, width: 1280, height: 720, bitrate: 3_000_000 },
|
||
sessionStartS: 65,
|
||
abs: 130,
|
||
duration: 5214,
|
||
};
|
||
|
||
it("includes the HLS-only rows (codec/bitrate/level) for hls transport", () => {
|
||
const keys = buildStatRows(base).map((r) => r.key);
|
||
expect(keys).toEqual([
|
||
"mode",
|
||
"transport",
|
||
"res",
|
||
"codec",
|
||
"bitrate",
|
||
"level",
|
||
"audio",
|
||
"subtitle",
|
||
"frames",
|
||
"buffer",
|
||
"offset",
|
||
"position",
|
||
]);
|
||
});
|
||
|
||
it("drops the HLS-only rows for direct play", () => {
|
||
const keys = buildStatRows({ ...base, transport: "direct", hlsLevel: null }).map((r) => r.key);
|
||
expect(keys).not.toContain("codec");
|
||
expect(keys).not.toContain("bitrate");
|
||
expect(keys).not.toContain("level");
|
||
expect(keys).toContain("res");
|
||
});
|
||
|
||
it("surfaces the classification as the mode value and formats position", () => {
|
||
const rows = buildStatRows({ ...base, playable: "transcode" });
|
||
expect(rows.find((r) => r.key === "mode")?.value).toBe("transcode");
|
||
expect(rows.find((r) => r.key === "offset")?.value).toBe("1:05");
|
||
expect(rows.find((r) => r.key === "position")?.value).toBe("2:10 / 86:54");
|
||
});
|
||
|
||
it("appends the transcode encoder to the mode row when known", () => {
|
||
const gpu = buildStatRows({ ...base, playable: "transcode", hwEncoder: true });
|
||
expect(gpu.find((r) => r.key === "mode")?.value).toBe("transcode · GPU");
|
||
const cpu = buildStatRows({ ...base, playable: "transcode", hwEncoder: false });
|
||
expect(cpu.find((r) => r.key === "mode")?.value).toBe("transcode · CPU");
|
||
});
|
||
|
||
it("tags the mode row with HDR→SDR for a HW tonemap session", () => {
|
||
const rows = buildStatRows({ ...base, playable: "transcode", hwEncoder: true, tonemap: true });
|
||
expect(rows.find((r) => r.key === "mode")?.value).toBe("transcode · GPU · HDR→SDR");
|
||
});
|
||
|
||
it("shows an empty classification honestly as a dash", () => {
|
||
expect(buildStatRows({ ...base, playable: "" }).find((r) => r.key === "mode")?.value).toBe(
|
||
DASH,
|
||
);
|
||
});
|
||
});
|
||
|
||
describe("fmtMode", () => {
|
||
it("appends the encoder only for a transcode with a known encoder", () => {
|
||
expect(fmtMode("transcode", true)).toBe("transcode · GPU");
|
||
expect(fmtMode("transcode", false)).toBe("transcode · CPU");
|
||
expect(fmtMode("transcode", null)).toBe("transcode"); // encoder unknown → bare mode
|
||
expect(fmtMode("transcode")).toBe("transcode");
|
||
expect(fmtMode("remux", true)).toBe("remux"); // not a transcode → no encoder suffix
|
||
expect(fmtMode("")).toBe(DASH);
|
||
// The HDR→SDR tag rides on the GPU encoder token for a tonemap session.
|
||
expect(fmtMode("transcode", true, true)).toBe("transcode · GPU · HDR→SDR");
|
||
expect(fmtMode("transcode", true, false)).toBe("transcode · GPU");
|
||
expect(fmtMode("transcode", null, true)).toBe("transcode"); // no encoder → no tag either
|
||
});
|
||
});
|
||
|
||
// --- item 12 of the 2026-09 bug hunt ------------------------------------------------------------
|
||
//
|
||
// The panel reported "Bitrate: 211 kbps" on EVERY remux, which the viewer rightly refused to
|
||
// believe. It was reading the master playlist's BANDWIDTH, and for a remux ffmpeg computes that from
|
||
// the only stream whose rate it knows — the `-b:a 192k` AAC track — plus 10%: 192000 × 1.1 = 211200,
|
||
// a constant that has nothing to do with the video. And the panel never said which audio or subtitle
|
||
// track was playing, though the player can switch both.
|
||
|
||
describe("measured bitrate", () => {
|
||
it("reports bytes per second of MEDIA, not of wall clock", () => {
|
||
const m = createFragMeter();
|
||
// A 6 s remux segment of 6 MB is 8 Mbps, however fast it happened to download.
|
||
m.add({ type: "main", bytes: 6_000_000, durationS: 6 });
|
||
expect(m.bitrate()).toBe(8_000_000);
|
||
});
|
||
|
||
it("adds the audio rendition's rate instead of averaging it away", () => {
|
||
// Multi-audio sessions ship audio as its own rendition, so both fragment types cover the SAME
|
||
// media seconds. Pooling bytes over pooled durations would report half the real bitrate.
|
||
const m = createFragMeter();
|
||
m.add({ type: "main", bytes: 6_000_000, durationS: 6 });
|
||
m.add({ type: "audio", bytes: 150_000, durationS: 6 });
|
||
expect(m.bitrate()).toBe(8_000_000 + 200_000);
|
||
});
|
||
|
||
it("has nothing to say before the first fragment, and after a reset", () => {
|
||
const m = createFragMeter();
|
||
expect(m.bitrate()).toBeUndefined(); // an honest dash — never the manifest's stand-in number
|
||
m.add({ type: "main", bytes: 6_000_000, durationS: 6 });
|
||
m.reset();
|
||
expect(m.bitrate()).toBeUndefined();
|
||
});
|
||
|
||
it("ignores init segments and anything with no media duration", () => {
|
||
const m = createFragMeter();
|
||
m.add({ type: "main", bytes: 900, durationS: 0 });
|
||
m.add({ type: "main", bytes: 0, durationS: 6 });
|
||
expect(m.bitrate()).toBeUndefined();
|
||
});
|
||
|
||
it("follows the stream: old fragments fall out of the rolling window", () => {
|
||
const m = createFragMeter(2);
|
||
m.add({ type: "main", bytes: 60_000_000, durationS: 6 }); // a dense scene
|
||
m.add({ type: "main", bytes: 3_000_000, durationS: 6 });
|
||
m.add({ type: "main", bytes: 3_000_000, durationS: 6 });
|
||
expect(m.bitrate()).toBe(4_000_000); // the 80 Mbps sample is gone, not still weighing it down
|
||
});
|
||
});
|
||
|
||
describe("buildStatRows — what is actually playing", () => {
|
||
const hls = (over: Partial<PlexStatsInput> = {}): PlexStatsInput => ({
|
||
lang: "en",
|
||
playable: "remux",
|
||
transport: "hls",
|
||
videoWidth: 1920,
|
||
videoHeight: 1080,
|
||
droppedFrames: 6,
|
||
totalFrames: 1458,
|
||
bufferAheadS: 12,
|
||
hlsLevel: { index: 0, total: 1, bitrate: 211_200 },
|
||
sessionStartS: 0,
|
||
abs: 10,
|
||
duration: 8580,
|
||
...over,
|
||
});
|
||
const value = (rows: { key: string; value: string }[], key: string) =>
|
||
rows.find((r) => r.key === key)?.value;
|
||
|
||
it("shows the MEASURED bitrate and never the manifest's 211 kbps stand-in", () => {
|
||
const rows = buildStatRows(hls({ measuredBps: 7_900_000 }));
|
||
expect(value(rows, "bitrate")).toBe("7.9 Mbps");
|
||
});
|
||
|
||
it("dashes the bitrate until something has been measured", () => {
|
||
// The level still carries its 211200 BANDWIDTH here — using it would be the old bug.
|
||
expect(value(buildStatRows(hls()), "bitrate")).toBe(DASH);
|
||
});
|
||
|
||
it("names the selected audio and subtitle track", () => {
|
||
const rows = buildStatRows(hls({ audioLabel: "Magyar AC3 5.1", subLabel: "Magyar" }));
|
||
expect(value(rows, "audio")).toBe("Magyar AC3 5.1");
|
||
expect(value(rows, "subtitle")).toBe("Magyar");
|
||
});
|
||
|
||
it("keeps the track rows for direct play too, where there is no HLS level at all", () => {
|
||
const rows = buildStatRows(
|
||
hls({ transport: "direct", hlsLevel: null, audioLabel: "English", subLabel: "Off" }),
|
||
);
|
||
expect(rows.some((r) => r.key === "bitrate")).toBe(false); // HLS-only, as before
|
||
expect(value(rows, "audio")).toBe("English");
|
||
expect(value(rows, "subtitle")).toBe("Off");
|
||
});
|
||
});
|