Commit Graph

24 Commits

Author SHA1 Message Date
333e0dcb18 test(01-09): RED — displaySurface:'monitor' + cursor:'always' constraint contract
Plan 01-09 Task 1 RED — pins 4 tests for D-15-display-surface contract:
1. getDisplayMedia called with strict {video:{displaySurface:'monitor',
   cursor:'always'},audio:false} (deep-equality, NOT objectContaining).
2. Non-monitor pick (browser/window) tears down stream + emits
   RECORDING_ERROR wrong-display-surface.
3. Monitor pick does NOT trip wrong-display-surface (over-fire guard).
4. classifyCaptureError routes 'wrong-display-surface' message prefix
   to 'wrong-display-surface' code.

Task 2 will flip Tests 1, 2, 4 to GREEN by adding constraints +
post-grant validation + extending CaptureErrorCode union.

Deviation Rule 3: navigator getter-only in Vitest's node env required
Object.defineProperty wrapper (installNavigatorStub helper) instead
of direct assignment.
2026-05-17 15:08:35 +02:00
e40949d1d2 test(01-08): regenerate last_30sec.webm fixture + split remux input/output fixtures
Plan 01-08 Task 5 closeout. The post-B+ smoke run produced a working
single-EBML WebM (28.76s, 676 frames, 1.89 MB, monotonic 0→28.76s
timestamps). Operator-confirmed empirically (timer overlay in smoke
HTML showed the latest frames matched expectations).

Two-fixture split resolves a test-design conflict surfaced when
last_30sec.webm flipped from pre-remux input shape to post-remux
output shape:

- tests/fixtures/last_30sec.webm — POST-REMUX output (single EBML,
  41 ffmpeg dry-run lines). Validates webm-playback.test.ts'
  playable-duration + structural assertions.

- tests/fixtures/raw-3ebml-concat.webm — PRE-REMUX input (3-EBML
  concat, 299 ffmpeg dry-run lines = 3 segment boundaries).
  Preserved from the original 2026-05-15 Phase 1 closure fixture.
  Used by webm-remux.test.ts to test that remuxSegments correctly
  transforms 3-EBML input → single-EBML output.

tests/background/webm-remux.test.ts FIXTURE_PATH updated to point at
raw-3ebml-concat.webm; the hardcoded EBML byte offsets [0, 509038,
970967] and frame bounds [905, 912] remain valid against that
preserved input.

Result: 64/64 vitest GREEN (was 61/64). tsc clean. Build exit 0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 12:44:50 +02:00
761dfc0388 test(debug-01-08): extend Tier-1 gate to Layer 2 (exercises remuxSegments)
The original Layer 1 gate (74400ae) verified module-init under
SW-simulated globals. It did not exercise remuxSegments — the
actual runtime code path the SW reaches on SAVE_ARCHIVE.

Layer 2 imports webm-remux.ts as SOURCE in a spawned Node child
under SW-simulated globals, invokes remuxSegments with a synthetic
single-segment EBML payload, and classifies the outcome:
- `ok` (returned a Blob) or `domain_error` (e.g. invalid EBML
  header — proves runtime path is structurally reachable) → PASS
- `sw_incompat` (ReferenceError for Node globals, EvalError /
  unsafe-eval for CSP) → FAIL with the specific error surfaced

This is the gate that empirically caught the ts-ebml Buffer issue
addressed by the preceding polyfill commit; it closes the loop
between "bundle loads" (Layer 1) and "bundle works at runtime"
(Layer 2).

Polyfill-aware design: Layer 2 leaves `Buffer` AVAILABLE in the
child env (split strip list: SW_SOURCE_STRIP_GLOBALS omits
'Buffer'). The vite-plugin-node-polyfills rewrite is BUNDLER-LEVEL
(Buffer → imported polyfill chunk) and does not apply when source
is loaded outside Vite. Leaving Buffer available faithfully
models what the polyfilled bundle provides at SW runtime, while
keeping the classifier ready to flag Buffer regressions if the
polyfill ever gets removed. `process`/`window`/`document` remain
stripped (polyfill is configured globals.process: false; SW
genuinely lacks DOM).

Node 24 native TS transform (`--experimental-transform-types`)
is used for source loading; a tiny inline resolution hook
appends `.ts` to extensionless relative specifiers, mimicking
vite/rollup's extension policy. Hook is base64-encoded as a
data: URL so the test stays self-contained (no on-disk hook file).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 12:18:04 +02:00
74400ae6ac test(debug-01-08): complete SW-bundle-import gate — mock chrome.* surface
The Tier-1 SW-bundle-loadability gate (c75854c) stripped
Buffer/process/window/document from the spawned Node isolate
but did not mock chrome.*. A correctly-bundled SW that reaches
addListener calls at module init would (correctly) progress to
chrome.runtime.onMessage.addListener(...) and throw
ReferenceError because chrome was undefined — a false-positive
RED.

This commit adds a minimal Proxy-based chrome.* stub that
no-ops any chrome.<api>.<method>(...) chain. The gate now
verifies what its file-header comment claims: "bundled artifact
reaches module-init completion under SW-simulated globals."

RED->GREEN: the gate now correctly passes against the post-fix
bundle and would catch any future regression in SW
bundle-loadability.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 11:16:05 +02:00
c75854cbef test(debug-01-08): RED Tier-1 SW-bundle-loadability gate + corrected hypothesis
Adds tests/background/sw-bundle-import.test.ts that loads the built SW
chunk under SW-simulated globals (Buffer/process/window/document stripped)
via a spawned Node child process. Pins the orchestrator-side gap that
caused Plan 01-08's SW init crash: the prior deps test only checked
SOURCE packages under default Node globals, never the bundled output, so
Vite/Rollup's CJS-interop bug (tree-shaking the `ebml` package while
leaving a dangling `{tools:f}=Pc` destructure against an empty Pc) went
undetected until operator empirical smoke.

RED against HEAD aabbd0c — failure surfaces the exact production error
("Cannot read properties of undefined (reading 'readVint')"), proving
the test is a true regression gate, not a tautology.

Also rewrites .planning/debug/01-08-sw-incompatibility.md to reflect the
actual root cause (Vite/Rollup CJS interop) rather than the orchestrator's
initial falsified hypothesis (new Function + Buffer globals — disproven
by Node simulation showing the throw fires at module-init line 12:33809
before any CSP-eval or Buffer-ref code path executes).

Full vitest: 60 passing + 3 RED (this gate + the 2 pre-existing Task 5
fixture-dependent duration tests). No regressions.

Per feedback-pre-checkpoint-bundle-gates.md (auto-loaded memory): any
future plan executor whose work surfaces a SW must run this test before
any operator-empirical checkpoint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 09:52:45 +02:00
407e683e9b test(01-08): RED unit tests for remuxSegments — single-EBML + monotonic + frame-count + size + empty
- 5 RED tests pinning the contract for src/background/webm-remux.ts
  (created in Task 3). All fail with "module missing" today — the
  Task 3 GREEN gate.
- Test 1: exactly 1 EBML header + 1 Segment magic in output.
- Test 2: output size within [0.7x, 1.3x] of input sum.
- Test 3: ffprobe format=duration >= 25_000 ms (skip-if-no-ffprobe).
- Test 4: ffprobe -count_frames in [905, 912] (per-seg sum 912 ± 3 boundary
  partial-frame drops, I-01 tightening).
- Test 5: empty input -> empty Blob (defense-in-depth).
- Fixture sliced at d13-confirmed byte offsets (0 / 509038 / 970967);
  verified against committed last_30sec.webm at Task 2 land time.
- Baseline counts: 13 files / 62 tests / 7 failed (2 webm-playback +
  5 new webm-remux) | 55 passed. tsc exit 0.
2026-05-17 09:23:53 +02:00
503531485c feat(01-08): install ts-ebml + webm-muxer; pin SW-compat via deps test
- Add ts-ebml ^3.0.2 (parse half) and webm-muxer ^5.1.4 (write half) per
  CONTEXT.md amendment D-14-remux; both MIT, both verified SW-compatible
  in the d13 debug-session library survey.
- tests/background/webm-remux-deps.test.ts pins two contracts:
  (a) named exports surface (Muxer + ArrayBufferTarget + Decoder).
  (b) both libraries import cleanly when window/document are absent on
      globalThis — guards the published dist against accidentally
      acquiring DOM globals on the hot path that would crash the
      Chrome service-worker runtime.
- Note: webm-muxer 5.1.4 upstream-deprecated in favor of Mediabunny; the
  pinned version still meets the d13 architectural requirement
  (single-EBML output via addVideoChunkRaw). Migration to Mediabunny is
  out of scope for Plan 01-08 and would require a new ADR.
- Baseline 53 GREEN + 2 new GREEN; tsc clean; 2 webm-playback duration
  RED still pending (drive to GREEN in Tasks 3-5).
2026-05-17 09:22:46 +02:00
bc310d98cf revert(01): reopen Phase 1 — D-13 multi-EBML-concat is unplayable
REQ-video-ring-buffer flipped from [x] back to [ ]. ROADMAP.md Phase 1
row reverted from [x] Closed 2026-05-15 to [ ] reopened 2026-05-16.
STATE.md status flipped phase_complete → phase_reopened with full
historical narrative preserved.

Root cause (confirmed at byte level by gsd-debugger 2026-05-16):
D-13's concat-of-self-contained-WebM-segments architecture produces a
3-EBML-header WebM that standards-compliant Matroska parsers
(mpv, ffmpeg, Chrome HTMLMediaElement) play only as the first segment
(~9.94 s) and silently drop the remaining 2 segments. Confirmed via
operator mpv drag-drop test of BOTH the canonical 2026-05-15 closure
fixture and the 2026-05-16 UAT-produced fixture — both exhibit the
same broken playback.

The 2026-05-15 "operator-confirmed clean Chrome playback" assessment
was insufficient: it verified the file plays without freezing but did
not measure total duration. Phase 1's primary deliverable
(REQ-video-ring-buffer / SPEC §10 #7) is therefore NOT satisfied.

Fix path chosen by user: ts-ebml (parse) + webm-muxer (write) to
replace mergeVideoSegments file-concat with real single-EBML remux.
Will land as Plan 01-08 via fresh /gsd-plan-phase ceremony.

RED test landed in tests/offscreen/webm-playback.test.ts (2 new
assertions on container-format-duration + ffmpeg-full-decode-duration).
2 failures, 53 baseline tests still GREEN.

Option C port-lifecycle refactor (debug session
empty-archive-port-race, commits 674c415..f0871c0) DID land cleanly
and is retained — that fix was orthogonal and correctly resolved the
silent-empty-archive symptom that previously masked this deeper bug.

Debug session: .planning/debug/d13-multi-ebml-concat-unplayable.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 19:47:47 +02:00
246eadb2ef test(option-c): continuous 600 s port lifecycle pinning contract
Implements Option C step 3 per .planning/debug/empty-archive-port-race.md:

  "Continuous end-to-end vitest covering 600 s of port lifecycle
   (2 reconnects + simulated REQUEST_BUFFER round-trips). Becomes the
   new pinning contract for the port lifecycle."

The UAT Test 3 BLOCKER surfaced because no test exercised the full
operator timeline — 5+ minute recording with port-replacement windows
crossing real SAVE_ARCHIVE round-trips. This file pins that contract
end-to-end at the unit-test level.

What's exercised:
  - Both SW (src/background/index.ts) and offscreen recorder
    (src/offscreen/recorder.ts) loaded into the SAME chrome stub, with
    paired port-pair factory (one connect() yields offPort + swPort
    that talk to each other through captured listeners).
  - 12 ping/pong cycles (~300 s simulated wall-clock).
  - 3 SAVE_ARCHIVE round-trips (one before reconnect, two after each
    of the two forced reconnects).
  - 2 EXTERNAL port disconnects (port._disconnected=true) — simulates
    the SW eviction / port glitch path that the H1.b test pins.
  - JSZip mocked at file scope (vi.mock) because Node 22+ JSZip can't
    read native Blobs — preserves integration shape (size accounting)
    without depending on JSZip's Node compatibility.

Final assertions:
  1. All 3 saveArchive calls return success:true.
  2. EVERY BUFFER message that crossed the wire carried segments (no
     silent-loss path was reachable).
  3. PONGs round-tripped (proves health-probe loop closes).

Suite: 53 GREEN / 53 tests. tsc --noEmit exit 0; type-safety grep clean;
npm run build exit 0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 14:53:47 +02:00
1fb3e978cb feat(option-c-offscreen): port health probe + request-id'd BUFFER + H1 try/catch
Implements the offscreen-side architectural refactor per
.planning/debug/empty-archive-port-race.md "Fix Strategy: Option C":

1. **Retired** the 290_000 ms pre-emptive reconnect setTimeout. Its race
   window between the synchronous .disconnect() and the onDisconnect
   handler running was the bisect-confirmed proximate cause of the H1
   "Attempting to use a disconnected port object" Uncaught Errors.

2. **Added** PONG-based health probe: each ping increments missedPongs;
   if MAX_MISSED_PONGS (3) consecutive PINGs go without echo, reconnect
   via the same clean teardown path the onDisconnect handler uses.
   PONG receipt resets the counter. Liveness-based replacement for the
   time-based pre-emptive rotation.

3. **H1 fix** — wrap PING postMessage in try/catch. The port object can
   transition to disconnected synchronously (SW eviction, port glitch)
   between the interval-callback being queued and it running. The catch
   absorbs the throw and routes through reconnectPort() — no more
   uncaught throws bubble out to the offscreen console.

4. **Request-id'd protocol** — REQUEST_BUFFER carries the SW-generated
   requestId; BUFFER response echoes it. The offscreen now posts on the
   CURRENT keepalivePort (no more portAtRequest stale-port refuse-to-
   post). The SW matches BUFFER → request by id, so port replacement
   mid-encode no longer drops the response — the SW retries on the new
   port and the matching BUFFER routes correctly.

5. **reconnectPort(reason)** — new helper consolidating the
   teardown+disconnect+reconnect dance used by both the missed-PONG
   path and the synchronous-throw path. Idempotent w.r.t. the chained
   onDisconnect callback.

Test updates:
  - H2 now sends REQUEST_BUFFER with a requestId (Option C contract).
  - H1.b refactored to test the externally-disconnected path (since the
    pre-emptive timeout path is gone): port._disconnected=true, fire
    ping, assert no throw + a fresh port appears.
  - Top-level snapshots of timer globals + afterEach restoration so a
    failing test doesn't leak overridden globals into the next test.

Status: 48 GREEN, 4 RED (the remaining RED is all SW-side — addressed
in next commit). All H1 + H1.b + H2 contracts now GREEN. Pinning
contracts (D-12 port-serialization, D-13 segment-rotation, A3 webm-
playback) untouched. tsc --noEmit exit 0; type-safety grep clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 14:37:22 +02:00
4306d59dfd test(option-c): RED gate for request-id'd port protocol + health probe + error surface
Per .planning/debug/empty-archive-port-race.md "Fix Strategy: Option C
(Architectural)", land RED tests that pin the 4 sub-behaviours the
refactor must satisfy at the unit level. These complement the operator-
facing contract already pinned by port-reconnect-race.test.ts (H1+H2).

Offscreen side (tests/offscreen/port-health-probe.test.ts):
  A. Bootstrap installs no 290_000 ms pre-emptive reconnect timer
     (the timing-based race window from b064a21 is gone).
  B. Missed PONGs (5 PINGs without echo) trigger a clean reconnect via
     the same path the onDisconnect handler uses.
  C. PONG echoes within timeout keep the port alive indefinitely
     (counter-test for over-eager probe — already GREEN today).
  D. REQUEST_BUFFER with requestId → BUFFER response echoes the same id
     (the architectural mechanism that retires cross-talk).

SW side (tests/background/request-id-protocol.test.ts):
  1. getVideoBufferFromOffscreen sends REQUEST_BUFFER with a generated
     uuid requestId on the live videoPort.
  2. Stale BUFFER (mismatched requestId) is ignored — no resolution.
  3. Port replacement mid-request → SW re-issues REQUEST_BUFFER on the
     new port with the SAME requestId. Retires the H2 silent-drop class.
  4. Empty video segments → saveArchive returns {success:false, error}
     (operator-visible) instead of {success:true} with no-video archive.
  5. SW echoes PONG on PING, closing the health-probe loop.

Suite status: 10 files / 52 tests (42 GREEN, 10 RED).
  - 40 baseline + 2 new GREEN (port-health-probe C; request-id 2 & 4
    accidentally pass due to test-stub side effects — they will continue
    to pass after fix for the right reasons).
  - 3 RED in port-reconnect-race + 3 RED in port-health-probe + 4 RED
    in request-id-protocol.

Quality gates: tsc --noEmit exit 0; type-safety grep clean.
No production code touched in this commit — fix lands in subsequent commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 14:27:17 +02:00
674c415945 test(debug-empty-archive): RED gate for empty-archive-port-race (H1 + H1.b + H2)
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>
2026-05-16 14:17:45 +02:00
08a79a61ac fix(01-review): sweep #1 stopRecording nulls mediaStream first to prevent rotation race 2026-05-16 10:52:59 +02:00
680eee3cc7 fix(01-review): IN-04 delete decodeDryRun helper, retain only spawnSync-based decodeDryRunStrict 2026-05-16 10:34:03 +02:00
cb23143ccf fix(01-review): IN-03 delete vestigial ring-buffer.test.ts breadcrumb 2026-05-16 10:32:43 +02:00
e9aae09f6d fix(01-review): WR-07 base64ToBlob empty-input shortcut + SW-side empty-segment filter 2026-05-16 10:24:38 +02:00
650c546a6e fix(01-review): WR-01+WR-02 stable capture error codes + pure assertCodecSupported 2026-05-16 09:49:01 +02:00
cd61cbccb6 test(01-07): commit regenerated last_30sec.webm fixture against D-13 recorder
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
2026-05-15 21:42:13 +02:00
87909d976c test(fix-a3): commit debug-session test artifacts + stale fixture
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.
2026-05-15 21:16:02 +02:00
5530292270 feat(fix-a3): retire ring-buffer first-chunk pin tests, add segment-rotation contract
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.
2026-05-15 20:59:01 +02:00
c0d9166a1d feat(fix-d12): add binary encode/decode helpers in src/shared/binary.ts
- 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.
2026-05-15 20:06:51 +02:00
408aa3354c test(01-02): add RED handshake + port tests
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>
2026-05-15 17:25:03 +02:00
d7840a811c test(01-02): add RED codec-check tests
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>
2026-05-15 17:24:18 +02:00
2e73a21151 test(01-02): add RED ring-buffer tests
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>
2026-05-15 17:23:50 +02:00