test: a size and complexity ratchet; coverage reported on both sides
The "class size" item from the quality checklist, measured: - A shipped file over 800 lines may not grow past its line count today (13 frontend, 11 backend). No other file may cross 800. - A function over the complexity limit may not rise, and no new one may appear. The limit is eslint `complexity` 20 on the frontend (64 functions over it today) and ruff C901 10 on the backend (32). - Each is a per-file ceiling. Shrinking is free; an entry back under the limit must be deleted, so the lists only shrink. C901 is measured in a test rather than switched on in ruff.toml: ruff has no per-function baseline, and a noqa would let a function grow without limit. Coverage (owner decision D4) is printed with no threshold: `vitest --coverage` (v8) gives 80% of lines, and the pytest lane ends with the total for app/, 78%. The bar stays "every regression test seen red". .coverage is ignored by git and kept out of the image's build context.
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
complexityOffenders,
|
||||
lineCounts,
|
||||
MAX_COMPLEXITY,
|
||||
MAX_LINES,
|
||||
shippedFiles,
|
||||
} from "./sourceSize.testkit";
|
||||
|
||||
// The size / complexity RATCHET for the shipped frontend (E32, owner 2026-10-03 — "class size" from
|
||||
// the quality checklist, measured rather than argued). Two limits, each with today's offenders as a
|
||||
// CEILING:
|
||||
// • a file over 800 lines — the listed ones may not grow past their line count; any other
|
||||
// file may not cross the limit;
|
||||
// • a function over complexity 20 (eslint `complexity`, cyclomatic) — per file, the
|
||||
// offenders' values highest-first may not rise, and no new offender may appear.
|
||||
// Shrinking is free; a file or function that drops UNDER the limit must be taken off its list (red
|
||||
// until it is), so the lists only get shorter. Raising a ceiling is a decision for the owner, with
|
||||
// the reason in docs/gates.md. JSX branches count toward complexity, so a component scores high by
|
||||
// construction — the limit catches the outliers, it does not grade style.
|
||||
|
||||
const BIG_FILES: Record<string, number> = {
|
||||
"components/AdminUsers.tsx": 993,
|
||||
"components/Channels.tsx": 904,
|
||||
"components/Feed.tsx": 934,
|
||||
"components/Home.tsx": 1136,
|
||||
"components/IssueComposer.tsx": 861,
|
||||
"components/PlayerModal.tsx": 1142,
|
||||
"components/Playlists.tsx": 1069,
|
||||
"components/PlexPlayer.tsx": 1984,
|
||||
"components/PlexPlaylistView.tsx": 830,
|
||||
"components/plexViews.tsx": 941,
|
||||
"components/SettingsPanel.tsx": 885,
|
||||
"components/Welcome.tsx": 944,
|
||||
"lib/plexPlayerHooks.ts": 922,
|
||||
};
|
||||
|
||||
const COMPLEX: Record<string, number[]> = {
|
||||
"App.tsx": [28],
|
||||
"components/AccountPanel.tsx": [29],
|
||||
"components/AdminLogs.tsx": [35],
|
||||
"components/auditColumns.tsx": [24],
|
||||
"components/ChannelPage.tsx": [91],
|
||||
"components/Channels.tsx": [53],
|
||||
"components/ConfigPanel.tsx": [28, 24],
|
||||
"components/DownloadCenter.tsx": [36],
|
||||
"components/downloadRow.tsx": [25],
|
||||
"components/Feed.tsx": [64],
|
||||
"components/Home.tsx": [34, 32, 21],
|
||||
"components/IssueComposer.tsx": [34],
|
||||
"components/KeyGate.tsx": [22],
|
||||
"components/NotificationsPanel.tsx": [28],
|
||||
"components/OnboardingWizard.tsx": [23],
|
||||
"components/PersonalViewsModal.tsx": [24],
|
||||
"components/PlayerModal.tsx": [99],
|
||||
"components/Playlists.tsx": [66],
|
||||
"components/PlexBrowse.tsx": [45],
|
||||
"components/plexCards.tsx": [58],
|
||||
"components/PlexCollectionView.tsx": [41],
|
||||
"components/PlexDiscover.tsx": [21],
|
||||
"components/PlexInfo.tsx": [65],
|
||||
"components/PlexPlayer.tsx": [103, 22],
|
||||
"components/PlexPlaylistView.tsx": [22],
|
||||
"components/PlexSidebar.tsx": [32],
|
||||
"components/PlexStatsOverlay.tsx": [29],
|
||||
"components/plexViews.tsx": [50, 30, 21],
|
||||
"components/Scheduler.tsx": [21, 21],
|
||||
"components/SetupWizard.tsx": [21],
|
||||
"components/SyncStatus.tsx": [22],
|
||||
"components/ui/AnchoredPopover.tsx": [22],
|
||||
"components/ui/anchoredPopoverHooks.ts": [30],
|
||||
"components/ui/Combobox.tsx": [22],
|
||||
"components/ui/DataTable.tsx": [42],
|
||||
"components/ui/PanelGroup.tsx": [23],
|
||||
"components/ui/SidePanel.tsx": [45],
|
||||
"components/ui/SortControl.tsx": [23],
|
||||
"components/usePlaylistActions.tsx": [25],
|
||||
"components/usePlexActiveFilters.ts": [29],
|
||||
"components/useVideoActions.tsx": [25],
|
||||
"components/VideoCard.tsx": [39],
|
||||
"components/VideoEditor.tsx": [50],
|
||||
"components/WatchPage.tsx": [30],
|
||||
"components/Welcome.tsx": [39, 24],
|
||||
"lib/api/core.ts": [48, 22],
|
||||
"lib/api/plex.ts": [26],
|
||||
"lib/diagnostics.ts": [22],
|
||||
"lib/keymap.ts": [28],
|
||||
"lib/logHooks.ts": [21],
|
||||
"lib/plexDetail.ts": [24],
|
||||
"lib/plexPlayerHooks.ts": [30],
|
||||
"lib/urlState.ts": [26, 24],
|
||||
"lib/useActiveFilters.ts": [28],
|
||||
};
|
||||
|
||||
describe("file size", () => {
|
||||
const lines = lineCounts(shippedFiles());
|
||||
|
||||
it(`no file crosses ${MAX_LINES} lines, and a listed one does not grow`, () => {
|
||||
const over = Object.entries(lines)
|
||||
.filter(([f, n]) => n > (BIG_FILES[f] ?? MAX_LINES))
|
||||
.map(([f, n]) => `${f}: ${n} lines (ceiling ${BIG_FILES[f] ?? MAX_LINES})`);
|
||||
expect(over).toEqual([]);
|
||||
});
|
||||
|
||||
it("lists no file that is back under the limit (delete it)", () => {
|
||||
const back = Object.keys(BIG_FILES).filter((f) => (lines[f] ?? 0) <= MAX_LINES);
|
||||
expect(back).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("function complexity", async () => {
|
||||
const actual = await complexityOffenders();
|
||||
|
||||
it(`no function crosses ${MAX_COMPLEXITY}, and a listed one does not rise`, () => {
|
||||
const worse: string[] = [];
|
||||
for (const [file, values] of Object.entries(actual)) {
|
||||
const ceiling = COMPLEX[file] ?? [];
|
||||
values.forEach((v, i) => {
|
||||
if (v > (ceiling[i] ?? MAX_COMPLEXITY))
|
||||
worse.push(`${file}: #${i + 1} is ${v} (ceiling ${ceiling[i] ?? MAX_COMPLEXITY})`);
|
||||
});
|
||||
}
|
||||
expect(worse).toEqual([]);
|
||||
});
|
||||
|
||||
it("lists no offender that is back under the limit (shorten the entry)", () => {
|
||||
const stale = Object.entries(COMPLEX)
|
||||
.filter(([file, values]) => (actual[file]?.length ?? 0) < values.length)
|
||||
.map(
|
||||
([file, values]) => `${file}: listed ${values.length}, now ${actual[file]?.length ?? 0}`,
|
||||
);
|
||||
expect(stale).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user