Archilyzer · Source

archilyzer

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

commit e6310a8c1230604409f5b2df88151203e4e360dd
parent e4bc59181ffa629a0c7c6d4fb36383af208a3398
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon, 28 Sep 2026 12:46:59 -0400

channels: a Diagnostics retry keeps its card and its log when it empties the bucket

Both Diagnostics grids dropped an emptied bucket's card — `populated` for the
channel-health cards, `listed` for the availability cards — and a retry
empties its own bucket while it streams (the per-video snapshot regen and
the end-of-run refresh), so the card took RetryBucketControl, its
StreamActionLog and the run's log with it. The same hole O3 closed on the
Download stage (cookies-mode.spec:241).

DiagnosticsStage and AvailabilitySummary now keep a `ran` set (bucket keys
whose Retry ran on this page, told by RetryBucketControl's onRun); a ran
card stays, its button disabled at (0). AvailabilitySummary's hook sits
above its `total === 0` early return, which also yields to a ran card. A
reload drops an empty card as before. Labels and test ids unchanged.

e2e: diagnostics-retry-log.spec.ts (2) — a Missing metadata retry and a
Needs auth retry whose bucket empties mid-run; each waits for the
refreshed EMPTY list, which renders only inside a card that survived.

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

Diffstat:
Meditor/app/channels/[slug]/components/stages/DiagnosticsStage.tsx | 35+++++++++++++++++++++++++++++++----
Aeditor/e2e/diagnostics-retry-log.spec.ts | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 190 insertions(+), 4 deletions(-)

diff --git a/editor/app/channels/[slug]/components/stages/DiagnosticsStage.tsx b/editor/app/channels/[slug]/components/stages/DiagnosticsStage.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useCallback, useState } from "react"; import { StreamActionLog } from "yt-dlp-transcript-common/components/StreamActionLog"; import { AVAILABILITY_VALUES, @@ -153,7 +153,15 @@ export function DiagnosticsStage({ ariaLabel: "chat only pending", }, ]; - const populated = buckets.filter((b) => b.ids.length > 0); + // A RETRY EMPTIES ITS OWN BUCKET, and its card must outlive that (release 13 + // slice W1; plans/FACTS.md, "A run log lives in the panel's React state"). + // The run refreshes the page after every video it fetches, and a card whose + // bucket had gone to 0 was filtered out here — RetryBucketControl, its + // StreamActionLog and the log with it. `ran` holds the buckets whose Retry + // ran on this page; such a card stays, its button disabled at (0). A reload + // drops it as before. + const [ran, markRan] = useRanBuckets(); + const populated = buckets.filter((b) => b.ids.length > 0 || ran.has(b.ariaLabel)); const total = populated.reduce((acc, b) => acc + b.ids.length, 0); return ( <div className="flex flex-col gap-6"> @@ -265,6 +273,7 @@ export function DiagnosticsStage({ actionLabel={b.ariaLabel} defaultQueueKey={downloadDefaultQueueKey} existingQueues={existingQueues} + onRun={() => markRan(b.ariaLabel)} /> ) : null} </div> @@ -372,6 +381,18 @@ function VerifyResultBucket({ ); } +// The bucket keys whose Retry has run on this page (see DiagnosticsStage and +// AvailabilitySummary). Client state only: a reload starts empty. +function useRanBuckets(): [ReadonlySet<string>, (key: string) => void] { + const [ran, setRan] = useState<ReadonlySet<string>>(() => new Set()); + const markRan = useCallback( + (key: string) => + setRan((prev) => (prev.has(key) ? prev : new Set([...prev, key]))), + [], + ); + return [ran, markRan]; +} + function Heading({ title, desc }: { title: string; desc: string }) { return ( <div> @@ -672,12 +693,17 @@ function AvailabilitySummary({ existingQueues: string[]; downloadDefaultQueueKey: string; }) { + // Above the early return (hooks run on every render). A needs-auth or error + // retry does not empty its own bucket — only a probe rewrites + // availability.json — but a check landing while it streams does, and the + // card went with its log exactly as the channel-health cards did. + const [ran, markRan] = useRanBuckets(); const total = AVAILABILITY_VALUES.reduce( (acc, v) => acc + availability.byStatus[v].length, 0, ) + availability.unchecked.length; - if (total === 0) { + if (total === 0 && ran.size === 0) { return ( <p aria-label="availability summary empty" @@ -692,7 +718,7 @@ function AvailabilitySummary({ n: availability.byStatus[v].length, })).filter((x) => x.n > 0); const listed = AVAILABILITY_LIST_STATUSES.filter( - (v) => availability.byStatus[v].length > 0, + (v) => availability.byStatus[v].length > 0 || ran.has(v), ); return ( <div @@ -744,6 +770,7 @@ function AvailabilitySummary({ actionLabel={AVAILABILITY_LABELS[v].toLowerCase()} defaultQueueKey={downloadDefaultQueueKey} existingQueues={existingQueues} + onRun={() => markRan(v)} /> )} </div> diff --git a/editor/e2e/diagnostics-retry-log.spec.ts b/editor/e2e/diagnostics-retry-log.spec.ts @@ -0,0 +1,159 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import { dirname } from "node:path"; +import { test, expect } from "@playwright/test"; +import { + channelStage, + generateReport, + readJson, + resetData, + resolvePath, +} from "./helpers"; + +// A DIAGNOSTICS RETRY KEEPS ITS LOG WHEN ITS BUCKET EMPTIES (release 13 slice +// W1) — the Diagnostics twin of cookies-mode.spec's "defer" test (release 11 +// slice O3), which closed the same hole on the Download stage. +// +// A retry empties its own bucket, and the run refreshes the page after every +// video it fetches (recordTaskDone → the snapshot regen → the pulse → +// AutoRefresh), then once more when it ends. Both Diagnostics grids used to +// filter an emptied bucket's card out — `populated` for the channel-health +// cards, `listed` for the availability cards — and the card took the +// RetryBucketControl, its StreamActionLog and the run's log with it. Each test +// below waits for the refreshed, EMPTY list, which renders only inside a card +// that survived the refresh: on the old code that wait is what fails, every +// time, rather than the log check racing the refresh. + +const CHANNEL = "availability-test"; +const FIXTURE = "availability-baseline"; +const ROOT = `test-transcripts/channels/${CHANNEL}`; + +type Snapshot = { + buckets?: { noMetadata?: string[] }; + availability?: { byStatus?: Record<string, string[]> }; +}; + +async function writePlaylist(ids: string[]): Promise<void> { + // retry-bucket reads the channel playlist to learn each id's URL. + await writeFile( + resolvePath(`${ROOT}/playlist`), + ids.map((id) => `https://www.youtube.com/watch?v=${id}\n`).join(""), + ); +} + +async function writeAvailability(id: string, availability: string) { + const path = resolvePath(`${ROOT}/data/${id}/availability.json`); + await mkdir(dirname(path), { recursive: true }); + await writeFile( + path, + JSON.stringify({ + checkedAt: new Date().toISOString(), + availability, + webpageUrl: `https://www.youtube.com/watch?v=${id}`, + }), + ); +} + +async function waitForSnapshot( + read: (snap: Snapshot) => number | undefined, + size: number, +): Promise<void> { + await expect + .poll( + async () => { + const snap = await readJson<Snapshot>(`${ROOT}/snapshot.json`).catch( + () => null, + ); + return snap ? (read(snap) ?? -1) : -1; + }, + { timeout: 15_000 }, + ) + .toBe(size); +} + +test("channel health: a Retry that empties Missing metadata keeps its card and its log", async ({ + page, +}) => { + test.setTimeout(90_000); + await resetData(FIXTURE); + // A video directory with nothing in it: no metadata.info.json, so it is in + // the retryable "Missing metadata.info.json" bucket. The retry's download + // writes the metadata, which is what empties the bucket. + const ID = "vidnometa1"; + await mkdir(resolvePath(`${ROOT}/data/${ID}`), { recursive: true }); + await writePlaylist([ID]); + + await generateReport(page, CHANNEL); + await waitForSnapshot((s) => s.buckets?.noMetadata?.length, 1); + await page.goto(channelStage(CHANNEL, "diagnostics")); + + const card = page.getByLabel("retry missing metadata bucket"); + await card.getByRole("button", { name: /^Retry \(1\)$/ }).click(); + const log = page.getByLabel("Retry missing metadata output"); + await expect(log).toContainText("Retry allowlist: kept 1 of 1", { + timeout: 30_000, + }); + await expect(log).toContainText("Managed download complete", { + timeout: 30_000, + }); + + // The bucket empties once the snapshot regenerates… + await waitForSnapshot((s) => s.buckets?.noMetadata?.length, 0); + // …and the page refreshes onto the EMPTY list, inside the card that must + // have survived that refresh, with the run's log still in it. + await expect(page.getByLabel("missing metadata empty")).toBeVisible({ + timeout: 20_000, + }); + await expect( + page.getByRole("heading", { name: "Missing metadata.info.json (0)" }), + ).toBeVisible(); + await expect(log).toContainText("Managed download complete"); + await expect(card.getByRole("button", { name: /^Retry \(0\)$/ })).toBeDisabled(); + // "All clear." is what the grid used to fall back to once its only card went. + await expect(page.getByLabel("channel health empty")).toHaveCount(0); + + // A fresh page has nothing to show for an empty bucket. + await page.reload(); + await expect(page.getByLabel("channel health empty")).toBeVisible(); + await expect(page.getByLabel("retry missing metadata bucket")).toHaveCount(0); +}); + +test("availability: a Needs auth card whose bucket empties during its Retry keeps its log", async ({ + page, +}) => { + test.setTimeout(90_000); + await resetData(FIXTURE); + const ID = "vidneedsauth1"; + await writeAvailability(ID, "needs_auth"); + await writePlaylist([ID]); + + await generateReport(page, CHANNEL); + await waitForSnapshot((s) => s.availability?.byStatus?.needs_auth?.length, 1); + await page.goto(channelStage(CHANNEL, "diagnostics")); + const card = page.getByLabel("retry needs auth bucket"); + await expect(card.getByRole("button", { name: /^Retry \(1\)$/ })).toBeVisible(); + + // A retry never rewrites availability.json — a download records its + // availability history only, with the top level untouched — so what empties + // an availability bucket is a PROBE. Land one now, as a check running beside + // the retry would: the page still shows the snapshot it rendered from, and + // the regeneration the retry's own download triggers is the first to read it. + await writeAvailability(ID, "public"); + + await card.getByRole("button", { name: /^Retry \(1\)$/ }).click(); + const log = page.getByLabel("Retry needs auth output"); + await expect(log).toContainText("Managed download complete", { + timeout: 30_000, + }); + + await waitForSnapshot((s) => s.availability?.byStatus?.needs_auth?.length, 0); + await expect(page.getByLabel("availability needs_auth empty")).toBeVisible({ + timeout: 20_000, + }); + await expect(page.getByRole("heading", { name: "Needs auth (0)" })).toBeVisible(); + await expect(log).toContainText("Managed download complete"); + await expect(card.getByRole("button", { name: /^Retry \(0\)$/ })).toBeDisabled(); + + await page.reload(); + await expect(page.getByLabel("availability public count")).toBeVisible(); + await expect(page.getByLabel("retry needs auth bucket")).toHaveCount(0); +});