commit 3f7b09b9d5972169ac64a9a91cc33d9ffc401d4c
parent 9f315744f6b5f6658fad5cf6256338ea9f3aeb53
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Fri, 17 Jul 2026 20:22:35 -0400
Audio-check: e2e coverage for the adaptive probe interval
Drive the AIMD backoff end-to-end: a fake source that serves corruption on its
first two download attempts (new `corruptRuns` sidecar knob, generalizing
recoverOnResume) then behaves. Each malformed checkpoint halves the live probe
interval (300->150->75ms), and the ensuing clean run steps it back up — asserted
from the job log ("Audio-check interval backoff:" / "recovering:").
Enable the adaptive cadence in the test server env (dev:test / start:test):
a 50ms floor plus recovery step/threshold overrides give the 300ms test
interval room to shrink. The change is inert for non-corrupting downloads, so
the rest of the audio-check suite is unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat:
3 files changed, 66 insertions(+), 2 deletions(-)
diff --git a/editor/e2e/audio-check-scenarios.spec.ts b/editor/e2e/audio-check-scenarios.spec.ts
@@ -14,6 +14,10 @@ const VIDEO_ID = "abc123";
type FakeSidecar = {
mode: "happy" | "corrupt-after" | "corrupt-final" | "corrupt-from-start";
recoverOnResume?: boolean;
+ // Serve corruption on the first N audio-check invocations, then behave.
+ // Generalizes recoverOnResume (= corruptRuns:1) to drive several successive
+ // rollbacks (e.g. to watch the adaptive probe interval shrink).
+ corruptRuns?: number;
deterministicCorrupt?: boolean;
totalChunks?: number;
chunkDelayMs?: number;
@@ -191,6 +195,55 @@ test.describe("audio-checked download scenarios", () => {
expect(await pathExists(`${CHANNEL_ROOT}/data/${VIDEO_ID}/audio.mp3`)).toBe(true);
});
+ test("adaptive interval: repeated corruption halves the probe cadence, then it recovers", async ({
+ page,
+ }) => {
+ // The source serves corruption on its first two download attempts (each
+ // caught mid-stream and rolled back to the .good baseline), then behaves.
+ // With the test server's adaptive-cadence env (300ms interval, 50ms floor,
+ // ×0.5 backoff, +100ms recovery after 2 clean probes), each malformed
+ // checkpoint halves the live interval (300→150→75), and the ensuing clean
+ // run steps it back up — proving the AIMD backoff end-to-end, not just in
+ // the unit test (common/ytdlp/audioCheckCadence.test.ts).
+ await resetData("audio-check-channel");
+ await writeFakeConfig({
+ mode: "corrupt-after",
+ corruptAfterChunk: 3,
+ corruptRuns: 2,
+ totalChunks: 10,
+ chunkDelayMs: 150,
+ });
+ await triggerDownload(page);
+
+ const log = page.getByLabel("Download videos output");
+ // Two successive malformed checkpoints, each halving the live interval. The
+ // reduced cadence persists across the yt-dlp relaunch between them.
+ await expect(log).toContainText(
+ "Audio-check interval backoff: 300ms -> 150ms",
+ { timeout: 30_000 },
+ );
+ await expect(log).toContainText(
+ "Audio-check interval backoff: 150ms -> 75ms",
+ { timeout: 30_000 },
+ );
+ // Once the source stabilises, a run of clean checkpoints relaxes it back up.
+ await expect(log).toContainText("Audio-check interval recovering:", {
+ timeout: 30_000,
+ });
+
+ const outcome = await waitForOutcome(
+ (o) => o.status === "ok-audio-checked",
+ "adaptive-interval recovery outcome",
+ );
+ // Two corruption episodes → at least two rollback/restart decisions; the
+ // download then finalizes cleanly.
+ expect(
+ (outcome.attempts[0].audioCheck?.rollbacks ?? 0) +
+ (outcome.attempts[0].audioCheck?.restarts ?? 0),
+ ).toBeGreaterThanOrEqual(2);
+ expect(await pathExists(`${CHANNEL_ROOT}/data/${VIDEO_ID}/audio.mp3`)).toBe(true);
+ });
+
test("final-extraction probe failure triggers rollback and retry", async ({ page }) => {
await resetData("audio-check-channel");
await writeFakeConfig({
diff --git a/editor/e2e/fixtures/bin/fake-ytdlp.mjs b/editor/e2e/fixtures/bin/fake-ytdlp.mjs
@@ -247,6 +247,16 @@ async function modeAudioCheckOneUrl(url) {
const recoverOnResume =
sidecar.recoverOnResume === true ||
process.env.FAKE_YTDLP_RECOVER_ON_RESUME === "1";
+ // Generalizes recoverOnResume (= corruptRuns:1): serve corruption on the
+ // first N audio-check invocations, then behave. Lets a test drive several
+ // successive mid-stream rollbacks (e.g. to observe the adaptive probe
+ // interval shrinking) before the source recovers. null = unset.
+ const corruptRuns =
+ sidecar.corruptRuns != null
+ ? Number.parseInt(String(sidecar.corruptRuns), 10)
+ : process.env.FAKE_YTDLP_CORRUPT_RUNS != null
+ ? Number.parseInt(process.env.FAKE_YTDLP_CORRUPT_RUNS, 10)
+ : null;
const deterministicCorrupt =
sidecar.deterministicCorrupt === true ||
process.env.FAKE_YTDLP_DETERMINISTIC_CORRUPT === "1";
@@ -299,6 +309,7 @@ async function modeAudioCheckOneUrl(url) {
const shouldEmitMarker = (() => {
if (mode === "happy") return false;
if (deterministicCorrupt) return true;
+ if (corruptRuns != null) return priorAudioCheckRuns < corruptRuns;
if (recoverOnResume && priorAudioCheckRuns > 0) return false;
return true;
})();
diff --git a/editor/package.json b/editor/package.json
@@ -5,8 +5,8 @@
"type": "module",
"scripts": {
"dev": "next dev --port ${EDITOR_PORT:-3001}",
- "dev:test": "WORKER_TOKEN=test-worker-token TRANSCRIPTS_DIR=$(pwd)/test-transcripts EXPORT_PUBLIC_DIR=$(pwd)/test-transcripts/.export-public SETTINGS_FILE=$(pwd)/test-settings.json YTDLP_BIN=$(pwd)/e2e/fixtures/bin/fake-ytdlp.mjs WHISPER_BIN=$(pwd)/e2e/fixtures/bin/fake-whisper.mjs WHISPER_MODEL=/dev/null CHOUGH_BIN=$(pwd)/e2e/fixtures/bin/fake-chough.mjs CHOUGH_MODEL=/dev/null PARAKEET_STITCH_BIN=$(pwd)/e2e/fixtures/bin/fake-parakeet-stitch.mjs PARAKEET_CLI=/dev/null PARAKEET_MODEL=/dev/null FFMPEG_BIN=$(pwd)/e2e/fixtures/bin/fake-ffmpeg.mjs FFPROBE_BIN=$(pwd)/e2e/fixtures/bin/fake-ffprobe.mjs AUDIO_CHECK_INTERVAL_MS_OVERRIDE=300 AUDIO_CHECK_SIZE_GATE_OVERRIDE=4096 next dev --port ${PORT:-3011}",
- "start:test": "WORKER_TOKEN=test-worker-token TRANSCRIPTS_DIR=$(pwd)/test-transcripts EXPORT_PUBLIC_DIR=$(pwd)/test-transcripts/.export-public SETTINGS_FILE=$(pwd)/test-settings.json YTDLP_BIN=$(pwd)/e2e/fixtures/bin/fake-ytdlp.mjs WHISPER_BIN=$(pwd)/e2e/fixtures/bin/fake-whisper.mjs WHISPER_MODEL=/dev/null CHOUGH_BIN=$(pwd)/e2e/fixtures/bin/fake-chough.mjs CHOUGH_MODEL=/dev/null PARAKEET_STITCH_BIN=$(pwd)/e2e/fixtures/bin/fake-parakeet-stitch.mjs PARAKEET_CLI=/dev/null PARAKEET_MODEL=/dev/null FFMPEG_BIN=$(pwd)/e2e/fixtures/bin/fake-ffmpeg.mjs FFPROBE_BIN=$(pwd)/e2e/fixtures/bin/fake-ffprobe.mjs AUDIO_CHECK_INTERVAL_MS_OVERRIDE=300 AUDIO_CHECK_SIZE_GATE_OVERRIDE=4096 next start --port ${PORT:-3011}",
+ "dev:test": "WORKER_TOKEN=test-worker-token TRANSCRIPTS_DIR=$(pwd)/test-transcripts EXPORT_PUBLIC_DIR=$(pwd)/test-transcripts/.export-public SETTINGS_FILE=$(pwd)/test-settings.json YTDLP_BIN=$(pwd)/e2e/fixtures/bin/fake-ytdlp.mjs WHISPER_BIN=$(pwd)/e2e/fixtures/bin/fake-whisper.mjs WHISPER_MODEL=/dev/null CHOUGH_BIN=$(pwd)/e2e/fixtures/bin/fake-chough.mjs CHOUGH_MODEL=/dev/null PARAKEET_STITCH_BIN=$(pwd)/e2e/fixtures/bin/fake-parakeet-stitch.mjs PARAKEET_CLI=/dev/null PARAKEET_MODEL=/dev/null FFMPEG_BIN=$(pwd)/e2e/fixtures/bin/fake-ffmpeg.mjs FFPROBE_BIN=$(pwd)/e2e/fixtures/bin/fake-ffprobe.mjs AUDIO_CHECK_INTERVAL_MS_OVERRIDE=300 AUDIO_CHECK_SIZE_GATE_OVERRIDE=4096 AUDIO_CHECK_INTERVAL_FLOOR_MS_OVERRIDE=50 AUDIO_CHECK_RECOVER_STEP_MS_OVERRIDE=100 AUDIO_CHECK_RECOVER_AFTER_OVERRIDE=2 next dev --port ${PORT:-3011}",
+ "start:test": "WORKER_TOKEN=test-worker-token TRANSCRIPTS_DIR=$(pwd)/test-transcripts EXPORT_PUBLIC_DIR=$(pwd)/test-transcripts/.export-public SETTINGS_FILE=$(pwd)/test-settings.json YTDLP_BIN=$(pwd)/e2e/fixtures/bin/fake-ytdlp.mjs WHISPER_BIN=$(pwd)/e2e/fixtures/bin/fake-whisper.mjs WHISPER_MODEL=/dev/null CHOUGH_BIN=$(pwd)/e2e/fixtures/bin/fake-chough.mjs CHOUGH_MODEL=/dev/null PARAKEET_STITCH_BIN=$(pwd)/e2e/fixtures/bin/fake-parakeet-stitch.mjs PARAKEET_CLI=/dev/null PARAKEET_MODEL=/dev/null FFMPEG_BIN=$(pwd)/e2e/fixtures/bin/fake-ffmpeg.mjs FFPROBE_BIN=$(pwd)/e2e/fixtures/bin/fake-ffprobe.mjs AUDIO_CHECK_INTERVAL_MS_OVERRIDE=300 AUDIO_CHECK_SIZE_GATE_OVERRIDE=4096 AUDIO_CHECK_INTERVAL_FLOOR_MS_OVERRIDE=50 AUDIO_CHECK_RECOVER_STEP_MS_OVERRIDE=100 AUDIO_CHECK_RECOVER_AFTER_OVERRIDE=2 next start --port ${PORT:-3011}",
"build": "next build",
"start": "next start --port ${EDITOR_PORT:-3001}",
"lint": "eslint",