Phase 1 UAT Test 3 surfaced a two-headed BLOCKER:
(a) silent empty-video archive when save crosses a port-reconnect window,
(b) 3x "Attempting to use a disconnected port object" Uncaught Errors
starting at the 290 s pre-emptive reconnect mark.
Bisect confirmed: H1 (port lifecycle race) was introduced by Plan 01-04
(b064a21); H2 (createArchive silent-skip on empty segments) is an upstream
defect (555eb05) that became fatal once CR-01 + sweep #5 guaranteed the
silent-skip branch would fire on every save during a reconnect window.
This commit lands the 3 RED tests at the unit-test level — they match the
UAT error string byte-for-byte for H1/H1.b and pin the silent-drop
contract for H2. They will flip GREEN as the Option C architectural
refactor (request-id'd port protocol + port-health probe + retry +
operator-visible error surface) lands across the next commits.
Baseline: 8 files / 43 tests (40 GREEN, 3 RED). tsc --noEmit exit 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the stale fixture committed in 87909d9 (test(fix-a3): commit
debug-session test artifacts + stale fixture) — that one was captured
under the D-09..D-11 single-continuous-recorder lifecycle and froze
~1 s into playback per the resolved A3 cluster-alignment session.
This fresh fixture (1.6 MB, VP9 1142×1038) was produced by ./smoke.sh
against the D-13 restart-segments recorder (commits 5530292..872f25d):
three self-contained 10 s WebM segments concatenated by the SW into a
multi-EBML-header file the operator confirmed plays cleanly in Chrome.
Acceptance evidence:
- ffprobe -v error -f matroska -i tests/fixtures/last_30sec.webm → exit 0
- ffmpeg -v warning -i tests/fixtures/last_30sec.webm -f null - → exit 0
with zero decoder errors; only the expected muxer DTS-monotonicity
warnings at segment join boundaries (non-blocking — multi-EBML-header
concat is the documented D-13 trade-off)
- Operator confirmed end-to-end Chrome playback clean (no ~1 s freeze)
- All 30/30 unit tests green incl. empirical webm-playback.test.ts gate
This closes the D-12 + A3 acceptance gates referenced in:
- .planning/debug/resolved/d12-blob-port-transfer-fails.md
- .planning/debug/resolved/webm-playback-freeze.md
Captures the RED contracts that the webm-playback-freeze debug
session landed (before this fix-a3 cycle started) plus the original
Plan 07 smoke fixture they run against. None of these files were
modified by this fix cycle — they are landed as-is from the debug
session to make the test history bisectable.
Files staged:
- tests/offscreen/segment-keyframes.test.ts
Three describe blocks (~340 LOC):
* documentation — pure-simulation tests that pin the D-09..D-11
failure mode as executable evidence (regression guard against
re-introducing single-continuous-recorder semantics)
* GREEN-pinning — pure-simulation tests that pin the D-13
segment-keyframe invariant
* production-driven — imports src/offscreen/recorder.ts and
asserts (i) `getSegments` exported as a function, (ii) it
returns at most 3 Blobs. THIS BLOCK IS NOW GREEN after the
D-13 activation in the prior commits — was the genuine TDD
anchor for fix-a3.
- tests/offscreen/webm-playback.test.ts
Two empirical-ffmpeg assertions on tests/fixtures/last_30sec.webm:
* zero "Error submitting packet to decoder" lines from the
VP9 decoder
* no "File ended prematurely" container-finalization error
Both STAY RED in this commit because the committed fixture is
still the stale one from Plan 07's pre-fix smoke. They flip
GREEN after the operator runs ./smoke.sh to regenerate the
fixture against the D-13 recorder — see the closing message
and the NEXT-STEP block of the resolved debug session.
- tests/fixtures/last_30sec.webm
The 2.1 MB Plan 07 smoke artifact. Committed deliberately so
the empirical RED test has something to run against. Will be
overwritten by the next ./smoke.sh run (single-file rotation —
the path is fixed by the smoke script + zip extraction step
in the debug-session reproduction).
Verification:
- npx vitest run --reporter=dot → Tests 2 failed | 28 passed (30)
- The 2 fails are EXACTLY the two empirical-ffmpeg assertions in
webm-playback.test.ts; the structural production-driven block
in segment-keyframes.test.ts is fully GREEN.
- npx tsc --noEmit clean.
- npm run build succeeds.
Operator action required before Phase 1 close (Plan 07 still owns
REQ-video-ring-buffer): re-run ./smoke.sh per the documented
6-step reproduction in the debug session, then re-run
`npx vitest run tests/offscreen/webm-playback.test.ts` and
expect both assertions to flip GREEN. Plan 07 success criterion
§10 #7 (playback) lands at that point.
Per debug session webm-playback-freeze "Activation Plan" step 4: the
D-09..D-11 ring-buffer semantics (first-chunk header pin + 30 s age trim)
are being replaced by D-13 restart-segments. The pinned-header assertions
were architecture-specific and become meaningless once each segment is
a self-contained WebM with its own EBML header and seed keyframe.
Changes:
- tests/offscreen/ring-buffer.test.ts: collapsed to a single breadcrumb
test pointing at the successor file. Kept the path so git history /
failure bisects land on the retirement commit cleanly.
- tests/offscreen/segment-rotation.test.ts (new): 8 tests pinning the
D-13 invariants against the production recorder module:
* MAX_SEGMENTS = 3, SEGMENT_DURATION_MS = 10_000 (= legacy 30 s window)
* empty-by-default, ordered, oldest-evicted-at-cap
* resetBuffer clears
* getSegments returns a defensive snapshot (no internal aliasing)
Uses a `pushSegmentForTest` seam so vitest can drive rotation
deterministically without instantiating a real MediaRecorder.
RED today by design (TDD discipline) — the segment-rotation suite
imports `getSegments`, `pushSegmentForTest`, `MAX_SEGMENTS`,
`SEGMENT_DURATION_MS` from src/offscreen/recorder.ts. Those exports
land in the next commit. tsconfig.include is "src" only so tsc stays
clean during the RED window.
- Add blobToBase64 / base64ToBlob in src/shared/binary.ts:
portable Blob↔base64 round-trip for the chrome.runtime.Port
wire-format. JSON.stringify(blob) returns "{}" across extension
contexts, so binary payloads must travel as base64 strings.
- Mirror the GREEN-block helper signatures from
tests/offscreen/port-serialization.test.ts so the same test pins
both the standalone helpers and the production wire format.
- Land tests/offscreen/port-serialization.test.ts as the RED+GREEN
executable contract for the D-12 fix: the RED block reproduces
the 75-byte "[object Object]" failure mode byte-for-byte; the
GREEN block pins the base64 wire-format the fix must implement.
- Uses arrayBuffer() + btoa(String.fromCharCode...) rather than
FileReader: FileReader is browser-only; the chosen approach
works in both Chrome extension contexts and the Node-based
vitest environment.
Refs: debug session d12-blob-port-transfer-fails.
Three RED tests pin Pattern 4 (handshake) and Pattern 5 / Pitfall 4
(port reconnect on disconnect) contracts:
handshake.test.ts:
- 'sends OFFSCREEN_READY after listener registration' — exactly one
OFFSCREEN_READY emitted at module load, AFTER onMessage.addListener
port.test.ts:
- 'connects on module load' — chrome.runtime.connect called once
- 'reconnects when port disconnects' — firing onDisconnect triggers
immediate re-connect (Pitfall 4 idle-timer reset)
chrome.runtime is stubbed locally (no vitest-chrome dependency added).
No 'as any' / no '@ts-ignore'; casts are 'as unknown as T'.
Plan 04 must wire OFFSCREEN_READY send + port.connect({ name:
'video-keepalive' }) + onDisconnect-driven reconnect at the import-side
effect layer of src/offscreen/recorder.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two RED tests pin D-20 (codec strict-mode, no silent fallback):
- 'throws on unsupported vp9 and emits RECORDING_ERROR'
- 'does not throw when vp9 IS supported'
vi.resetModules() between tests is critical: module-load side-effects
(handshake + port connect) happen once per import, so isolation across
the four test files depends on it.
chrome.runtime is stubbed locally (no vitest-chrome dependency added,
per threat T-1-NEW-02-01 — minimize supply chain for four test files).
No 'as any' / no '@ts-ignore'; the cast is 'as unknown as T'.
Plan 03 must export assertCodecSupported() from src/offscreen/recorder.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four RED tests pin D-10 (header pinning) and D-11 (30s trim) contracts:
- 'first chunk is header' — isFirst marker on first addChunk
- 'second chunk is NOT header' — only the first is pinned
- 'trim 30s — keeps header, evicts aged tail' — header survives indefinitely
- 'trim with empty buffer does not throw' — defensive edge case
Plan 03 must export {addChunk, trimAged, getBuffer, resetBuffer} from
src/offscreen/recorder.ts to flip these to GREEN.
Also stages tests/fixtures/.gitkeep so the fixture dir survives clean
checkouts (Plan 07 drops a known-good last_30sec.webm into it after the
manual smoke test).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>