mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
Second pass over the suite, driven by the inventory in the gaps document. Nine agents wrote suites in parallel against private databases, then a tenth read all of it adversarially and five of its findings were fixed. unit + component 2076 -> 2079 (+888 over the round) api 647 -> 1015 e2e 18 -> 29 What was closed: - lib/route-access.ts, the page-level authorization layer, went from zero tests to 48. Every API route was guarded and none of the pages were. - The five media proxy routes now have a real 2xx beside every 403. The blocker was the positive control, solved by stubbing r2Client.send() and leaving lib/r2-media-proxy.ts itself real. - Every remaining server-side lib module: invitations, email verification, the upload tokens, the logger, request origin, the whole R2 and Bunny lifecycle, notifications and admin stats. - Six video-page hooks, and the chunking arithmetic extracted out of lib/client/r2-video-upload.ts as a pure module. - Five end-to-end flows: workspace members, bulk operations, the admin area, player interaction and failure recovery. Three things about the harness itself turned out to be wrong: - Two @/lib/r2 stubs in tests/setup/api.ts had the wrong return shape, so every route reaching finalizeR2VideoUpload silently took the "not a valid video" branch and no test noticed. - The auth matrix asserted only "not 2xx", which two entries satisfied without their guard existing. It now requires 401 or 403, which makes both load-bearing, and all 60 routes pass the stricter form. - Both admin API routes had no positive control anywhere: replacing their guard with an unconditional refusal left the entire suite green. Found by the adversarial review, now covered. Process: - bun run test:mutation runs StrykerJS over the authorization and validation modules. Diagnostic, not a gate, weekly in CI rather than on a push. - playwright.config.ts gains an opt-in webkit project for the player spec. - AGENTS.md now requires a batch of new tests to be reviewed by somebody who did not write them. Only two production files change, both deliberate: lib/auth.ts loses a verbatim copy of its own permission formulas, and lib/client/r2-video-upload.ts calls the extracted arithmetic. No behaviour change in either.
71 lines
2.8 KiB
TypeScript
71 lines
2.8 KiB
TypeScript
/**
|
|
* Pure arithmetic extracted from `r2-video-upload.ts`.
|
|
*
|
|
* The uploader itself is XMLHttpRequest wiring, `fetch` calls and timers, so the
|
|
* only test that can reach it is the end-to-end upload spec, and that spec only
|
|
* ever walks the happy path. The numbers below are the part of the uploader that
|
|
* is actually worth pinning down: which bytes each multipart part carries, how
|
|
* long a failed part waits before it is retried, and what percentage the UI is
|
|
* told. They live here so they can be called directly with fixed inputs.
|
|
*
|
|
* Nothing in this module touches the network, the DOM or a timer.
|
|
*/
|
|
|
|
/**
|
|
* Wait, in milliseconds, before each attempt at uploading a single multipart
|
|
* part, indexed by attempt number. Index 0 is the first try and is never waited
|
|
* on, so the schedule is really "try, then retry after 2s, 5s and 10s": four
|
|
* attempts and at most 17 seconds of backoff per part.
|
|
*/
|
|
export const PART_RETRY_DELAYS_MS = [0, 2000, 5000, 10000];
|
|
|
|
/**
|
|
* How long attempt `attempt` waits before it runs. The first attempt never
|
|
* waits, and an attempt past the end of the schedule is not one the caller
|
|
* should be making, so it waits not at all rather than for `undefined` ms.
|
|
*/
|
|
export function getRetryDelayMs(attempt: number, delays: number[] = PART_RETRY_DELAYS_MS): number {
|
|
if (attempt <= 0) return 0;
|
|
return delays[attempt] ?? 0;
|
|
}
|
|
|
|
export type PartByteRange = { start: number; end: number };
|
|
|
|
/**
|
|
* The slice of the file that a given part carries. Part numbers are 1-based
|
|
* because that is what S3 uses, and the final part is short: it stops at the end
|
|
* of the file rather than at a full part boundary.
|
|
*
|
|
* The part list comes from the server, which sized it from the same file length,
|
|
* so `partNumber` is always within range in practice. A part beyond the end of
|
|
* the file would produce `end` below `start`, which `Blob.slice` reads as an
|
|
* empty range.
|
|
*/
|
|
export function getPartByteRange(
|
|
partNumber: number,
|
|
partSizeBytes: number,
|
|
totalBytes: number
|
|
): PartByteRange {
|
|
const start = (partNumber - 1) * partSizeBytes;
|
|
const end = Math.min(start + partSizeBytes, totalBytes);
|
|
return { start, end };
|
|
}
|
|
|
|
/** Whole-percent progress for a single-request upload. */
|
|
export function getUploadProgressPercent(loadedBytes: number, totalBytes: number): number {
|
|
return Math.round((loadedBytes / totalBytes) * 100);
|
|
}
|
|
|
|
/**
|
|
* Whole-percent progress across a multipart upload, given the bytes reported so
|
|
* far for each part. Clamped at 100: parts report their own progress
|
|
* independently and a re-tried part can briefly double-count.
|
|
*/
|
|
export function getMultipartProgressPercent(
|
|
loadedBytesPerPart: number[],
|
|
totalBytes: number
|
|
): number {
|
|
const loaded = loadedBytesPerPart.reduce((sum, value) => sum + value, 0);
|
|
return Math.min(100, Math.round((loaded / totalBytes) * 100));
|
|
}
|