commit cb5a826548507442c42eff6d75fae8883187ec58
parent fb17b672efcab5d1dd3e7b247f83ce172fa42e7b
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Sat, 26 Sep 2026 03:57:20 -0400
common: the pre-clean gate never judges a suspect its check did not probe, blocked or not (review fix)
94a9aa62 left unverified only the suspects a BLOCKED check did not reach.
An unblocked check also skips a suspect with no `webpage_url` in its
metadata (nothing to probe it by), and that suspect was then judged on its
old availability.json — an old `public` cleared it for an irreversible
delete. Every tier C suspect missing from `probedIds` is now `unverified`,
whatever the reason. The review's read-only scan found no such video on the
live corpus (79,700 dirs, 1,186 clean candidates), so nothing changes today;
such a video is never cleaned until it has a `webpage_url`.
verifyBeforeClean.test.ts +1: an unblocked check with a no-URL suspect
carrying a stale `public` probes only the other suspect, leaves the no-URL
one unverified and excluded, and keeps the probed public suspect and the
listed video cleanable. It fails with the old blocked-only condition; the
blocked and unblocked cases still pass.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
2 files changed, 65 insertions(+), 15 deletions(-)
diff --git a/common/controller/verifyBeforeClean.test.ts b/common/controller/verifyBeforeClean.test.ts
@@ -219,12 +219,20 @@ async function withFakeSource(
}
}
-async function seedProbeable(paths: Paths, id: string): Promise<void> {
+async function seedProbeable(
+ paths: Paths,
+ id: string,
+ opts: { noUrl?: boolean } = {},
+): Promise<void> {
const dir = path.join(paths.channelsDir, "ch", "data", id);
await mkdir(dir, { recursive: true });
await writeFile(
path.join(dir, "metadata.info.json"),
- JSON.stringify({ id, webpage_url: `https://www.youtube.com/watch?v=${id}` }),
+ JSON.stringify(
+ opts.noUrl
+ ? { id }
+ : { id, webpage_url: `https://www.youtube.com/watch?v=${id}` },
+ ),
);
// A STALE verdict from long ago: exactly what must not clear a delete.
await writeFile(
@@ -307,3 +315,42 @@ test("an unblocked confirmation is judged exactly as before", async () => {
},
);
});
+
+test("an unblocked confirmation that could not probe a suspect (no webpage_url) leaves it unverified, not judged on its old record", async () => {
+ await withFakeSource(
+ [
+ 'case "$last" in',
+ ` *suspect0001*) echo '{"id":"suspect0001","availability":"public"}';;`,
+ ` *) echo "ERROR: [youtube] x: Video unavailable" >&2; exit 1;;`,
+ "esac",
+ ].join("\n"),
+ async (paths, probes) => {
+ await seedConfig(paths, "ch", {
+ handling: "youtube",
+ url: "https://www.youtube.com/@ch/videos",
+ });
+ await seedProbeable(paths, "listed00001");
+ await seedProbeable(paths, "suspect0001");
+ // Not listed, and nothing to probe it by — but its old record says
+ // `public`, which would clear it for delete if it were read.
+ await seedProbeable(paths, "nourl000001", { noUrl: true });
+
+ const verdicts = await verifyBeforeClean({
+ channelSlug: "ch",
+ paths,
+ candidateIds: ["listed00001", "suspect0001", "nourl000001"],
+ onLog: () => {},
+ });
+
+ // The check was not blocked: it probed the one suspect it could.
+ assert.deepEqual(await probes(), ["https://www.youtube.com/watch?v=suspect0001"]);
+ assert.deepEqual([...verdicts.unverified], ["nourl000001"]);
+ const excluded = excludedIds(verdicts);
+ assert.ok(excluded.has("nourl000001"));
+ // The probed public suspect and the listed video stay cleanable.
+ assert.equal(excluded.has("suspect0001"), false);
+ assert.equal(excluded.has("listed00001"), false);
+ await assert.rejects(readFile(paths.autoQueueStateFile, "utf8"));
+ },
+ );
+});
diff --git a/common/controller/verifyBeforeClean.ts b/common/controller/verifyBeforeClean.ts
@@ -183,20 +183,23 @@ export async function verifyBeforeClean({
return verdicts;
}
- // A STOPPED CHECK JUDGES NOTHING IT DID NOT PROBE (release 10, L2 review).
- // The confirmation stops at the first rate-limited probe, and every suspect
- // after it is left with whatever `availability.json` it already had — which
- // can be months old and say `public`. Read below, that stale record would
- // clear the video for an irreversible delete. So when the check was
- // blocked, every suspect it did not probe is `unverified`: skipped, no
- // marker, retried on the next sweep. An unblocked check is judged exactly
- // as before.
- const unprobed = new Set<string>();
- if (check.blocked) {
- const probed = new Set(check.probedIds);
- for (const id of suspects) if (!probed.has(id)) unprobed.add(id);
+ // NOTHING THE CHECK DID NOT PROBE IS JUDGED (release 10, L2 review). A
+ // suspect the check never asked about still has whatever `availability.json`
+ // it already had — which can be months old and say `public`. Read below,
+ // that stale record would clear the video for an irreversible delete. So
+ // every suspect missing from `probedIds` is `unverified`: skipped, no
+ // marker, retried on the next sweep. Two ways to be missing: the check
+ // stopped at a rate-limited probe before reaching it (`blocked`), or it was
+ // skipped — no `webpage_url` in its metadata, so there was nothing to probe.
+ // The second was judged on its old record before this; no video on the live
+ // corpus was in that state (2026-09-26 review scan), and none can be now.
+ const probed = new Set(check.probedIds);
+ const unprobed = new Set(suspects.filter((id) => !probed.has(id)));
+ if (unprobed.size > 0) {
log(
- `Verify before clean: the source rate-limited the confirmation — ` +
+ (check.blocked
+ ? `Verify before clean: the source rate-limited the confirmation — `
+ : `Verify before clean: the confirmation could not probe every suspect — `) +
`${unprobed.size} suspect(s) it did not reach are left unverified, not judged on an old record.`,
);
}