Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 113303d18c87ba7012a8bd897abed3b76a336572
parent ca4fd1ff99a0f8ab8951c2b76d175ceb63cd8881
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Fri, 25 Sep 2026 11:09:49 -0400

e2e: pacing.spec — a deferred video is skipped; a live 429 defers and moves on

T1 seeds download.videoDeferrals.a1 and proves the runner picks only a2,
idles "deferred", and /operations/download draws the "Rate-limit cooldown"
region with "Deferred videos" listing alpha/a1. T2 drives a real 429
through the fake's new `dl429` sentinel (managed single-URL download only;
the prefetch still succeeds, the two-spawn shape): youtube backs off
(fails 1), dl429vid1 is deferred ~6 h, and after the real ~60 s cooldown
the next pick is a2.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
Meditor/e2e/fixtures/bin/fake-ytdlp.mjs | 11+++++++++++
Aeditor/e2e/pacing.spec.ts | 223+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 234 insertions(+), 0 deletions(-)

diff --git a/editor/e2e/fixtures/bin/fake-ytdlp.mjs b/editor/e2e/fixtures/bin/fake-ytdlp.mjs @@ -444,6 +444,17 @@ async function modeYoutubeSingleUrlManaged(url) { `youtube-single:${url} cookies=${cookieArg()}\n`, ); if (cookieGateBlocked(url)) failCookieGate(url); + // `dl429`: the 2026-09-24 YouTube shape — the metadata prefetch succeeded + // (the prefetch branch has no such sentinel), then the real download's + // SUBTITLE fetch answers 429. Per video, not IP-wide: availability.ts + // classifies it rate_limit, so the runner backs youtube off AND defers this + // one video (pacing.spec.ts). + if (url.toLowerCase().includes("dl429")) { + process.stderr.write( + `ERROR: [youtube] ${url}: Unable to download video subtitles for 'en': HTTP Error 429: Too Many Requests\n`, + ); + process.exit(1); + } process.stdout.write(`[fake-ytdlp] managed single-URL ${id}\n`); // URL sentinels for no-subs-fallback tests. The sentinels live in the diff --git a/editor/e2e/pacing.spec.ts b/editor/e2e/pacing.spec.ts @@ -0,0 +1,223 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import { test, expect } from "@playwright/test"; +import { readJson, resetData, resolvePath, writeSettings } from "./helpers"; +import { baseUrl } from "./baseUrl"; + +// YouTube lane pacing (release 7): a rate-limited video is DEFERRED for 6 h as +// well as backing the platform off, so after the cooldown the auto-download +// runner moves on to the NEXT video instead of re-picking the same one. The +// pure decision is unit-tested (common/jobs/unitOutcome.test.ts); this proves +// the runner's pick honours a persisted deferral, the lane says why it idles, +// the page shows it, and a live 429 (the fake's `dl429` sentinel) defers. + +type Leaf = { id: string; match: { type: string; value?: string } }; +type Group = { id: string; mode: string; children: (Group | Leaf)[] }; + +const ONE_WORKER = [ + { + id: "w1", + name: "W1", + kind: "local", + enabled: true, + priority: 0, + appId: "whisper-cpp", + config: {}, + }, +]; + +const STATE_FILE = "test-transcripts/.auto-queue/state.json"; + +// Copied from auto-queue.spec.ts: a YouTube channel with undownloaded videos. +async function makeDownloadChannel(slug: string, ids: string[]) { + const root = resolvePath(`test-transcripts/channels/${slug}`); + await mkdir(`${root}/data`, { recursive: true }); + await writeFile( + `${root}/config.json`, + JSON.stringify({ + handling: "youtube", + name: slug, + url: `https://www.youtube.com/@${slug}/videos`, + }), + ); + await writeFile( + `${root}/playlist`, + ids.map((id) => `https://www.youtube.com/watch?v=${id}`).join("\n") + "\n", + ); + await writeFile( + `${root}/snapshot.json`, + JSON.stringify({ + generatedAt: "2026-06-01T00:00:00.000Z", + totals: { videos: ids.length, transcribed: 0, downloaded: 0 }, + buckets: { + noTranscript: [], + downloadedNoTranscript: [], + untranscoded: [], + multipleAudioFormats: [], + transcribedWithAudio: [], + untranscribable: [], + noMetadata: [], + failedListed: [], + missingFromArchive: [], + duplicateDirs: [], + partialDownloads: [], + corruptSource: [], + nonStandardVtt: [], + skippedByFilter: [], + }, + undownloadedIds: ids, + }), + ); +} + +async function setup(ids: string[]) { + await resetData(null); + await makeDownloadChannel("alpha", ids); + const root: Group = { + id: "root", + mode: "strict", + children: [{ id: "leaf-alpha", match: { type: "channel", value: "alpha" } }], + }; + await writeSettings({ + adminTitle: "Test Admin", + maxTranscriptPageBytes: 8388608, + sleepBetweenDownloadsSeconds: 0, + minFreeDiskGB: 0, + workers: ONE_WORKER, + autoQueue: { + transcription: {}, + download: { enabled: true, maxWorkers: null, root }, + }, + }); +} + +type KindStatus = { + runner: { running: boolean; idleReason: string | null }; + picks: { videoId: string }[]; + deferred: { videoId: string; channelSlug: string; untilMs: number }[]; +}; +type Status = { download: KindStatus }; + +async function getStatus( + request: import("@playwright/test").APIRequestContext, +): Promise<Status> { + const res = await request.get(`${baseUrl}/api/auto-queue/status`); + expect(res.ok()).toBeTruthy(); + return res.json(); +} + +// Chronological pick order (the status log is newest-first). +function pickOrder(status: Status): string[] { + return status.download.picks.map((p) => p.videoId).reverse(); +} + +async function control( + request: import("@playwright/test").APIRequestContext, + action: "start" | "stop", +) { + const res = await request.post(`${baseUrl}/api/auto-queue/control`, { + data: { kind: "download", action }, + }); + if (action === "start") expect(res.ok()).toBeTruthy(); +} + +type PersistedDownload = { + platformBackoff: Record<string, { until: number; fails: number }>; + videoDeferrals?: Record<string, { until: number; channelSlug: string }>; +}; + +async function persistedDownload(): Promise<PersistedDownload | null> { + try { + return (await readJson<{ download: PersistedDownload }>(STATE_FILE)) + .download; + } catch { + return null; + } +} + +test.afterEach(async ({ request }) => { + await control(request, "stop"); +}); + +test("a deferred video is skipped by auto-download, and the lane says so", async ({ + page, + request, +}) => { + await setup(["a1", "a2"]); + // Seed a live deferral on a1 — as if its download had just answered 429. + await mkdir(resolvePath("test-transcripts/.auto-queue"), { recursive: true }); + await writeFile( + resolvePath(STATE_FILE), + JSON.stringify({ + transcription: { runtime: {}, picks: [], platformBackoff: {} }, + download: { + runtime: {}, + picks: [], + platformBackoff: {}, + videoDeferrals: { + a1: { until: Date.now() + 60 * 60_000, channelSlug: "alpha" }, + }, + }, + }), + ); + + await control(request, "start"); + + await expect + .poll(async () => (await getStatus(request)).download.runner.idleReason, { + timeout: 60_000, + }) + .toBe("deferred"); + const status = await getStatus(request); + // a1 is listed first, and was never picked. + expect(pickOrder(status)).toEqual(["a2"]); + expect(status.download.deferred.map((d) => d.videoId)).toEqual(["a1"]); + + await page.goto("/operations/download"); + const region = page.getByRole("region", { name: "Rate-limit cooldown" }); + await expect(region).toBeVisible({ timeout: 20_000 }); + const list = region.getByRole("list", { name: "Deferred videos" }); + await expect(list).toContainText("alpha/a1"); + // Nothing is cooling, so the platform list is not drawn at all. + await expect( + region.getByRole("list", { name: "Platforms in cooldown" }), + ).toHaveCount(0); + await expect(page.getByText(/rate-limited recently and is deferred/)).toBeVisible(); +}); + +test("a live 429 backs youtube off, defers the video, and the next pick is the next video", async ({ + request, +}) => { + // A REAL cooldown: base 60 s ±10 %. The whole point is what happens when it + // lapses — the runner must move on to a2, not re-pick dl429vid1. + test.setTimeout(150_000); + await setup(["dl429vid1", "a2"]); + + await control(request, "start"); + + await expect + .poll( + async () => { + const d = await persistedDownload(); + return { + deferred: Boolean(d?.videoDeferrals?.dl429vid1), + fails: d?.platformBackoff.youtube?.fails ?? 0, + }; + }, + { timeout: 30_000 }, + ) + .toEqual({ deferred: true, fails: 1 }); + const d = await persistedDownload(); + expect(d?.videoDeferrals?.dl429vid1?.channelSlug).toBe("alpha"); + // Deferred for ~6 h, not for the cooldown's minute. + expect(d!.videoDeferrals!.dl429vid1.until - Date.now()).toBeGreaterThan( + 5 * 60 * 60_000, + ); + + await expect + .poll(async () => pickOrder(await getStatus(request)), { + timeout: 100_000, + intervals: [2_000], + }) + .toContain("a2"); + expect(pickOrder(await getStatus(request))).toEqual(["dl429vid1", "a2"]); +});