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 3 RED gate. Eight strict-validation tests pin D-P2-03
(strict 8-field meta.json schema) plus F2 plan-checker-iter-1
resolution (empty urls[] permitted for whole-desktop-no-tab sessions).
Tests (3 RED-today + 5 GREEN-today regression guards; ALL 8 GREEN
after Plan 02-03):
1. RED — Object.keys(meta).length === 8.
2. GREEN — timestamp matches ISO-8601 Z-suffix regex.
3. RED — urls is Array of valid URLs (empty permitted per F2).
4. GREEN — extensionVersion matches semver.
5. GREEN — totalEvents is non-negative integer.
6. GREEN — videoBufferSeconds === 30 (CON-video-window).
7. GREEN — logDurationMinutes === 10 (CON-event-log-window).
8. RED — no extra fields beyond EXPECTED_KEYS.
RED evidence (vitest 4.1.6 against current HEAD):
× Test 1: meta.json has 7 fields; D-P2-03 requires exactly 8.
Current keys: [timestamp, url, userAgent, extensionVersion,
videoBufferSeconds, logDurationMinutes, totalEvents].
× Test 3: meta.urls is not an Array. Got: undefined.
× Test 8: meta.json contains extra (unexpected) fields: ["url"].
PLANNER-RESOLVED TENSIONS (documented in file header):
- D-P2-03 'non-empty urls[]' vs CONTEXT.md permissive empty-array:
resolved in favor of the permissive clause (F2 — empty is the
canonical representation of whole-desktop-no-tab sessions).
- 8th field name 'schemaVersion': tentative planner pick to mark
the D-P2-02 url→urls breaking-change cutover.
- Plan's 'ALL 8 fail' claim vs reality: 5 of 8 already pass under
the current 7-field shape (timestamp, semver, totalEvents,
videoBufferSeconds, logDurationMinutes). These stay GREEN as
regression guards after Plan 02-03 lands.
EXPECTED_KEYS constant:
['timestamp', 'urls', 'userAgent', 'extensionVersion',
'videoBufferSeconds', 'logDurationMinutes', 'totalEvents',
'schemaVersion']
Plan 02-03 implementer MUST add `schemaVersion` (recommended value:
'2') to satisfy Tests 1 + 8 simultaneously.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>