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).
This commit is contained in:
2026-05-17 09:22:46 +02:00
parent 2e499d7387
commit 503531485c
3 changed files with 256 additions and 3 deletions

View File

@@ -0,0 +1,137 @@
// tests/background/webm-remux-deps.test.ts
//
// Plan 01-08 Task 1: SW-compatibility + presence contract for the two new
// runtime dependencies that the WebM remux pipeline rests on. Pins the
// architectural commitment that landed in CONTEXT.md amendment D-14-remux:
// - `ts-ebml` ^3.0.2 (MIT, parses each VideoSegment's EBML structure)
// - `webm-muxer` ^5.1.4 (MIT, writes the single-EBML-headered output)
//
// Both libraries were surveyed in `.planning/debug/d13-multi-ebml-concat-
// unplayable.md` (Evidence/library-survey, lines 380-410). They are pure
// JS, pure ESM/CJS, and were grep-verified at survey time to contain no
// hard DOM-global references on the hot path. Chrome's service-worker
// runtime (where `remuxSegments()` will execute) does not provide
// `window` or `document`; this test pins that compat at the
// devDependency-import surface so a future bump that accidentally adds
// a DOM global is caught at test time rather than at runtime in a
// production SW.
//
// Test 1 asserts named-export presence (RED until `npm install` lands).
// Test 2 asserts both libraries load under default Node globals without
// referencing `window` or `document` synchronously at import time.
//
// Skip discipline: none — these are pure import-shape tests, no external
// binaries, no fixtures. Vitest's default Node environment is sufficient.
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
interface GlobalSnapshot {
window: unknown;
document: unknown;
hadWindow: boolean;
hadDocument: boolean;
}
/**
* Capture the current values of `window` and `document` on `globalThis`
* so they can be restored after a test that deletes them. Vitest's
* `vi.stubGlobal` is not used here because we want to assert the
* absence of the globals at import time, not just stub them.
*
* @returns Snapshot the caller passes back to {@link restoreGlobals}.
*/
function snapshotGlobals(): GlobalSnapshot {
const g = globalThis as unknown as Record<string, unknown>;
return {
window: g.window,
document: g.document,
hadWindow: 'window' in g,
hadDocument: 'document' in g,
};
}
/**
* Restore the globals captured by {@link snapshotGlobals}. Idempotent.
*
* @param snap - Snapshot returned by {@link snapshotGlobals}.
*/
function restoreGlobals(snap: GlobalSnapshot): void {
const g = globalThis as unknown as Record<string, unknown>;
if (snap.hadWindow) {
g.window = snap.window;
} else {
delete g.window;
}
if (snap.hadDocument) {
g.document = snap.document;
} else {
delete g.document;
}
}
describe('webm-remux dependencies (Plan 01-08 Task 1)', () => {
it('exports Muxer + ArrayBufferTarget + Decoder', async () => {
// Dynamic import so a missing package surfaces as a precise test
// failure ("Cannot find module 'webm-muxer'") rather than a Vitest
// collection error that hides which dependency is the cause.
const webmMuxer = await import('webm-muxer');
expect(webmMuxer.Muxer).toBeDefined();
expect(webmMuxer.ArrayBufferTarget).toBeDefined();
const tsEbml = await import('ts-ebml');
expect(tsEbml.Decoder).toBeDefined();
});
describe('loads under default Node globals without DOM-global ReferenceErrors', () => {
let snap: GlobalSnapshot;
beforeEach(() => {
snap = snapshotGlobals();
const g = globalThis as unknown as Record<string, unknown>;
delete g.window;
delete g.document;
});
afterEach(() => {
restoreGlobals(snap);
});
it('webm-muxer + ts-ebml do not throw on import when window/document are absent', async () => {
// The Chrome service-worker runtime provides neither `window` nor
// `document`. If either library's published dist references one
// of these synchronously at module evaluation time (e.g. a UMD
// wrapper falling through to `window`), the import below would
// throw a ReferenceError and this test would fail with a clear
// signal.
//
// ts-ebml's UMD wrapper does contain a `typeof window` check
// with a `self`/`global` fallback per the d13 library survey
// — `typeof` does NOT throw on undeclared identifiers per
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof
// so the fallback path resolves cleanly.
//
// webm-muxer is documented zero-DOM-ref.
let webmMuxerError: unknown = null;
try {
await import('webm-muxer');
} catch (e) {
webmMuxerError = e;
}
expect(
webmMuxerError,
`webm-muxer threw at import time without window/document: ${String(webmMuxerError)}`,
).toBeNull();
let tsEbmlError: unknown = null;
try {
await import('ts-ebml');
} catch (e) {
tsEbmlError = e;
}
expect(
tsEbmlError,
`ts-ebml threw at import time without window/document: ${String(tsEbmlError)}`,
).toBeNull();
});
});
});