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.
104 lines
4.1 KiB
TypeScript
104 lines
4.1 KiB
TypeScript
// @vitest-environment jsdom
|
|
//
|
|
// A split export that fails part-way. Every clip is its own job,
|
|
// committed as it goes, so a failure at clip 2 of 2 (the job cap) left clip 1 queued while the editor
|
|
// reported a plain failure and kept both selected — and trying again queued clip 1 a second time. It
|
|
// now says how many made it (also when `req` has already shown the refusal in its dialog), deselects
|
|
// them, holds the cut list still while it queues, and a retry goes on with the same "(i/N)" series.
|
|
import { describe, it, expect, vi, beforeAll, afterEach } from "vitest";
|
|
import { render, screen, fireEvent, waitFor, cleanup, act } from "@testing-library/react";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
|
|
vi.mock("react-i18next", async (importOriginal) => ({
|
|
...(await importOriginal<typeof import("react-i18next")>()),
|
|
useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en" } }),
|
|
}));
|
|
const { notify } = vi.hoisted(() => ({ notify: vi.fn() }));
|
|
vi.mock("../lib/notifications", async (importOriginal) => ({
|
|
...(await importOriginal<typeof import("../lib/notifications")>()),
|
|
notify,
|
|
}));
|
|
|
|
import VideoEditor from "./VideoEditor";
|
|
import { api, HttpError, type DownloadJob } from "../lib/api";
|
|
import { markReported } from "../lib/api/core";
|
|
|
|
beforeAll(() => {
|
|
class Stub {
|
|
observe() {}
|
|
unobserve() {}
|
|
disconnect() {}
|
|
takeRecords() {
|
|
return [];
|
|
}
|
|
}
|
|
globalThis.ResizeObserver ??= Stub as unknown as typeof ResizeObserver;
|
|
globalThis.IntersectionObserver ??= Stub as unknown as typeof IntersectionObserver;
|
|
});
|
|
afterEach(() => {
|
|
cleanup();
|
|
vi.restoreAllMocks();
|
|
notify.mockReset();
|
|
});
|
|
|
|
const job = {
|
|
id: 7,
|
|
display_name: "Talk",
|
|
asset: { duration_s: 100, width: 1920, height: 1080, title: "Talk" },
|
|
} as unknown as DownloadJob;
|
|
|
|
describe("VideoEditor — a split export that fails part-way", () => {
|
|
it("says how many clips made it, and a retry queues only the rest", async () => {
|
|
vi.spyOn(api, "downloadStoryboard").mockResolvedValue({ available: false } as never);
|
|
let refuse: (e: unknown) => void = () => {};
|
|
const enqueue = vi
|
|
.spyOn(api, "enqueueEdit")
|
|
.mockResolvedValueOnce({} as never)
|
|
.mockImplementationOnce(
|
|
() =>
|
|
new Promise((_, reject) => {
|
|
refuse = reject;
|
|
}) as never,
|
|
)
|
|
.mockResolvedValue({} as never);
|
|
render(
|
|
<QueryClientProvider client={new QueryClient()}>
|
|
<VideoEditor job={job} onClose={() => {}} />
|
|
</QueryClientProvider>,
|
|
);
|
|
|
|
// The playhead is where the video element says it is.
|
|
const video = document.querySelector("video")!;
|
|
const playhead = (s: number) => {
|
|
Object.defineProperty(video, "currentTime", { value: s, configurable: true, writable: true });
|
|
fireEvent.timeUpdate(video);
|
|
};
|
|
playhead(50);
|
|
fireEvent.click(screen.getByText("editor.splitHere"));
|
|
|
|
fireEvent.click(screen.getByText("editor.createN"));
|
|
await waitFor(() => expect(enqueue).toHaveBeenCalledTimes(2));
|
|
// While the clips are queued the cut list holds still: a split now would be a range never queued.
|
|
playhead(75);
|
|
fireEvent.click(screen.getByText("editor.splitHere"));
|
|
|
|
// The job cap, as `req` delivers it: already shown in the error dialog, and marked reported.
|
|
const refused = new HttpError(422, "errors.generic", { code: "quota" });
|
|
markReported(refused);
|
|
await act(async () => refuse(refused));
|
|
await waitFor(() =>
|
|
expect(notify).toHaveBeenCalledWith(
|
|
expect.objectContaining({ level: "error", title: "editor.partlyCreated" }),
|
|
),
|
|
);
|
|
expect(enqueue.mock.calls[0]![0].display_name).toBe("Talk (1/2)");
|
|
|
|
// The queued first clip is deselected: one clip left to create, and only it is sent again — as
|
|
// the second of the same two.
|
|
fireEvent.click(screen.getByText("editor.create"));
|
|
await waitFor(() => expect(enqueue).toHaveBeenCalledTimes(3));
|
|
expect(enqueue.mock.calls[2]![0].edit_spec.trim).toEqual({ start_s: 50, end_s: 100 });
|
|
expect(enqueue.mock.calls[2]![0].display_name).toBe("Talk (2/2)");
|
|
});
|
|
});
|