Production changes (src/background/index.ts):
- pendingDownloadUrlResolvers Map<requestId, resolver> routes DOWNLOAD_URL
responses back to the in-flight downloadArchive Promise; mirrors the
pendingBufferRequests pattern from the BUFFER round-trip so port
replacement mid-mint does not lose the response.
- pendingRevokes Map<downloadId, url> tracks (downloadId → minted blob:URL)
for the chrome.downloads.onChanged revoke dispatch.
- onConnect port message sink extended with DOWNLOAD_URL routing branch
(alongside existing PING/BUFFER routing).
- downloadArchive rewritten: encode archive via blobToBase64 → post
CREATE_DOWNLOAD_URL on videoPort → await DOWNLOAD_URL response (race
against 5s BLOB_URL_MINT_TIMEOUT_MS) → reject empty / non-blob: URLs
(T-02-02-03 mitigation) → call chrome.downloads.download → register
(downloadId, url) in pendingRevokes. NO data:URL fallback — typed
errors route through saveArchive's catch to RECORDING_ERROR.
- chrome.downloads.onChanged listener registered at module init:
on terminal state ('complete' / 'interrupted'), posts REVOKE_DOWNLOAD_URL
to videoPort and clears the pendingRevokes entry.
Deviation (Rule 3 — auto-fix blocking issue):
- Plan 02-01's test helpers in blob-url-download.test.ts +
meta-json-urls-schema.test.ts + strict-meta-json-validation.test.ts
modeled only the REQUEST_BUFFER → BUFFER round-trip, not the new
CREATE_DOWNLOAD_URL → DOWNLOAD_URL round-trip Plan 02-02 introduces.
Without the test-side mint simulation, the SW's downloadArchive
times out at the offscreen mint step → chrome.downloads.download
never called → ALL existing meta.json tests timeout.
- Each helper extended with a tryFireDownloadUrl block that decodes
the CREATE_DOWNLOAD_URL.dataBase64, mints a Node-native blob:URL via
URL.createObjectURL, captures the archive bytes for downstream
JSZip extraction (capturedArchiveBytes), and replies DOWNLOAD_URL.
Test 3 (revoke lifecycle) additionally shims port.postMessage to
call URL.revokeObjectURL on receipt of REVOKE_DOWNLOAD_URL — the
test-side equivalent of src/offscreen/recorder.ts handleCreateDownloadUrl.
- Pre-existing Plan-02-02-era TODO comments in both test files
explicitly anticipated this extension ("Plan 02-03 implementer will
likely need a different helper, e.g. spy on URL.createObjectURL").
Verification (full §verification block from plan):
- npx tsc --noEmit: clean
- npm run build: clean
- npx vitest run tests/background/blob-url-download.test.ts: 3/3 GREEN (was 3 RED)
- npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts: 13/13 GREEN
- npm test full suite: 163 passed / 8 failed (was 159 passed / 12 failed);
net delta +4 GREEN = 3 RED→GREEN flips + 1 ffprobe-flaky pass. 8 remaining
RED are exactly the Plan 02-03 territory (5 meta-json-urls-schema + 3
strict-meta-json-validation RED tests).
- grep -c "data:application/zip;base64," src/background/index.ts: 0 (gone)
- grep -c "blob:" src/background/index.ts: 8 (new pipeline)
- grep -c "chrome.downloads.onChanged" src/background/index.ts: 5 (listener wired)
- dist/ post-build: 0 "data:application/zip;base64," matches; 1 file with
"chrome.downloads.onChanged" (the SW chunk).
Plan 02-01 Task 1 RED gate. Three failing tests pin D-P2-01
(offscreen-minted Blob URL pipeline) ahead of Plan 02-02 implementation:
1. chrome.downloads.download is called with a blob: URL and NOT a
data:application/zip;base64, URL (closes audit P0-6).
2. A 6 MB archive completes through downloadArchive in under 5 s AND
emits a blob: URL (REQ-archive-export-latency; vi-mocked
remuxSegments short-circuits the muxer for the 6 MB stress path).
3. URL.revokeObjectURL is scheduled with the minted URL after
chrome.downloads.onChanged fires 'complete' (lifecycle hygiene).
RED evidence (vitest 4.1.6 against current HEAD):
× Test 1: chrome.downloads.download was called with
url='data:application/zip;base64,UEsDBAoAAAAAAL1qtFw...'
— D-P2-01 forbids data:application/zip;base64, prefix.
× Test 2: chrome.downloads.download was called with
url='data:application/zip;base64,...' at the 6 MB scale —
D-P2-01 requires blob: prefix.
× Test 3: URL.revokeObjectURL was never called after
chrome.downloads.onChanged 'complete' fired
(chrome.downloads.onChanged._callbacks.length === 0 at probe time).
Implementation notes:
- vitest default env is 'node' (vitest.config.ts); Node 24 ships
URL.createObjectURL + URL.revokeObjectURL + performance as globals,
so no jsdom override is required.
- FileReader is NOT in Node 24; added a minimal FileReader polyfill
(delegates to Blob.arrayBuffer()) so JSZip's Blob ingestion works.
- Test 2 mocks remuxSegments via vi.doMock to bypass muxer monotonic-
timestamp constraints for the synthetic 6 MB payload.
- Tests 1 + 3 drive the SW with the canonical 3-slice raw-3ebml-concat
fixture (same byte offsets as tests/background/webm-remux.test.ts).
- T-02-01-01 mitigation: grep -c '\.skip' returns 0.
Baseline: 155 GREEN preserved (no regressions); this plan adds 3 NEW
RED tests. Plan 02-02 flips them GREEN.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>