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.
127 lines
4.2 KiB
TypeScript
127 lines
4.2 KiB
TypeScript
// @vitest-environment jsdom
|
|
//
|
|
// The Scheduler page: Run all and Pause stay in the status strip; the destructive Purge
|
|
// discovery left it for the strip's "…", which holds the page's whole list and asks before purging.
|
|
import { describe, it, expect, vi, afterEach, beforeEach } from "vitest";
|
|
import { render, screen, fireEvent, cleanup, act, waitFor } from "@testing-library/react";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
|
|
const h = vi.hoisted(() => ({ confirm: vi.fn(async () => true) }));
|
|
vi.mock("react-i18next", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("react-i18next")>();
|
|
return { ...actual, useTranslation: () => ({ i18n: { language: "en" }, t: (k: string) => k }) };
|
|
});
|
|
vi.mock("./confirmContext", () => ({ useConfirm: () => h.confirm }));
|
|
|
|
import Scheduler from "./Scheduler";
|
|
import { api, type SchedulerStatus } from "../lib/api";
|
|
|
|
const status = (over: Partial<SchedulerStatus> = {}): SchedulerStatus => ({
|
|
running_here: true,
|
|
enabled: true,
|
|
paused: false,
|
|
jobs: [
|
|
{
|
|
id: "sync",
|
|
interval_minutes: 30,
|
|
next_run: null,
|
|
running: false,
|
|
status: "ok",
|
|
last_started: null,
|
|
last_finished: null,
|
|
last_result: null,
|
|
result: null,
|
|
last_error: null,
|
|
progress: null,
|
|
},
|
|
],
|
|
queue: {
|
|
channels_recent_pending: 0,
|
|
channels_deep_pending: 0,
|
|
deep_eta_seconds: 0,
|
|
videos_pending_enrich: 0,
|
|
videos_pending_shorts: 0,
|
|
videos_live_refresh: 0,
|
|
},
|
|
quota: { used_today: 0, remaining_today: 100, daily_budget: 100, backfill_reserve: 0 },
|
|
maintenance: { revalidate_batch: 5000, revalidate_batch_default: 5000, min: 1000, max: 50000 },
|
|
...over,
|
|
});
|
|
|
|
let qc: QueryClient;
|
|
let current: SchedulerStatus;
|
|
|
|
async function mount(initial = status()) {
|
|
current = initial;
|
|
qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
render(
|
|
<QueryClientProvider client={qc}>
|
|
<Scheduler />
|
|
</QueryClientProvider>,
|
|
);
|
|
await screen.findByText("scheduler.maintenance.batchLabel");
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.spyOn(api, "schedulerStatus").mockImplementation(() => Promise.resolve(current));
|
|
});
|
|
afterEach(() => {
|
|
cleanup();
|
|
vi.restoreAllMocks();
|
|
h.confirm.mockClear();
|
|
});
|
|
|
|
const menuRows = () =>
|
|
screen.getAllByRole("menuitem", { hidden: true }).map((b) => b.textContent ?? "");
|
|
async function openMenu() {
|
|
act(() => {
|
|
fireEvent.click(screen.getByRole("button", { name: "scheduler.menu" }));
|
|
});
|
|
await waitFor(() =>
|
|
expect(screen.getAllByRole("menuitem", { hidden: true }).length).toBeGreaterThan(0),
|
|
);
|
|
}
|
|
const choose = (label: string) =>
|
|
fireEvent.click(
|
|
screen.getAllByRole("menuitem", { hidden: true }).find((b) => b.textContent === label)!,
|
|
);
|
|
|
|
describe("Scheduler · the status strip's '…'", () => {
|
|
it("keeps Run all and Pause in sight and Purge discovery only in the menu", async () => {
|
|
await mount();
|
|
expect(screen.getByRole("button", { name: /scheduler\.runAll$/ })).toBeTruthy();
|
|
expect(screen.getByRole("button", { name: /scheduler\.pause$/ })).toBeTruthy();
|
|
expect(screen.queryByRole("button", { name: /scheduler\.purgeDiscovery/ })).toBeNull();
|
|
await openMenu();
|
|
expect(menuRows()).toEqual(["scheduler.runAll", "scheduler.pause", "scheduler.purgeDiscovery"]);
|
|
});
|
|
|
|
it("purges only after the confirm says yes", async () => {
|
|
const purge = vi.spyOn(api, "purgeDiscovery").mockResolvedValue({
|
|
search_videos_deleted: 1,
|
|
videos_deleted: 0,
|
|
channels_deleted: 0,
|
|
} as never);
|
|
await mount();
|
|
h.confirm.mockResolvedValueOnce(false);
|
|
await openMenu();
|
|
choose("scheduler.purgeDiscovery");
|
|
await waitFor(() => expect(h.confirm).toHaveBeenCalledTimes(1));
|
|
expect(h.confirm).toHaveBeenCalledWith(expect.objectContaining({ danger: true }));
|
|
expect(purge).not.toHaveBeenCalled();
|
|
await openMenu();
|
|
choose("scheduler.purgeDiscovery");
|
|
await waitFor(() => expect(purge).toHaveBeenCalledTimes(1));
|
|
});
|
|
|
|
it("says why Run all is off while the scheduler is paused", async () => {
|
|
await mount(status({ paused: true }));
|
|
await openMenu();
|
|
expect(menuRows()).toEqual([
|
|
"scheduler.runAllscheduler.runAllPausedHint",
|
|
"scheduler.resume",
|
|
"scheduler.purgeDiscovery",
|
|
]);
|
|
});
|
|
});
|