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>
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>
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>
- 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).
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>
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>