Files
mokosh/tests/offscreen/segment-rotation.test.ts
Mark 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

126 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// tests/offscreen/segment-rotation.test.ts
//
// Replaces tests/offscreen/ring-buffer.test.ts (retired). Pins the D-13
// restart-segments invariants against the production recorder module.
//
// What this exercises (per .planning/debug/webm-playback-freeze.md
// "Activation Plan" + RESEARCH.md Pattern 3 + CONTEXT.md D-13):
// 1. `getSegments()` is the canonical buffer accessor (Blob[]); the
// legacy chunk-level `getBuffer()` API is no longer present.
// 2. The segment ring caps at MAX_SEGMENTS = 3 — the 4th push evicts
// the oldest, preserving the most-recent 3 segments (= 30 s window
// at SEGMENT_DURATION_MS = 10_000 ms).
// 3. `resetBuffer()` clears the segment array — used on
// track-ended ("user stopped sharing") cleanup and between tests.
// 4. Each retained entry is a real Blob with `type === 'video/webm'`
// so the SW's `mergeVideoSegments` concatenation produces a valid
// multi-EBML-header WebM payload (Chrome handles this natively;
// see Pattern 3 trade-off discussion).
//
// The test seam `pushSegmentForTest(blob)` drives rotation
// deterministically — production code reaches the same code path via
// the MediaRecorder.onstop handler, but instantiating a real
// MediaRecorder inside vitest is not portable (jsdom lacks the
// underlying capture pipeline). The seam name encodes its purpose.
import { beforeEach, describe, expect, it } from 'vitest';
import {
getSegments,
pushSegmentForTest,
resetBuffer,
MAX_SEGMENTS,
SEGMENT_DURATION_MS,
VIDEO_BUFFER_DURATION_MS,
} from '../../src/offscreen/recorder';
function makeSegmentBlob(seedByte: number): Blob {
// A 4-byte placeholder is enough — these tests assert on length /
// ordering / type, not on decodability. The real ffmpeg-driven
// contract lives in webm-playback.test.ts and runs against the
// committed fixture.
return new Blob([new Uint8Array([seedByte, 0x00, 0x00, 0x00])], {
type: 'video/webm',
});
}
describe('segment rotation (D-13 restart-segments)', () => {
beforeEach(() => {
resetBuffer();
});
it('exposes the D-13 constants — 3 segments × 10 s = 30 s window', () => {
expect(MAX_SEGMENTS).toBe(3);
expect(SEGMENT_DURATION_MS).toBe(10_000);
// The product matches the legacy VIDEO_BUFFER_DURATION_MS so the
// operator-facing "last 30 seconds" promise stays intact.
expect(MAX_SEGMENTS * SEGMENT_DURATION_MS).toBe(VIDEO_BUFFER_DURATION_MS);
});
it('starts empty before any segment is pushed', () => {
expect(getSegments()).toEqual([]);
});
it('first push appends one segment', () => {
pushSegmentForTest(makeSegmentBlob(0x1a));
const segments = getSegments();
expect(segments).toHaveLength(1);
expect(segments[0]).toBeInstanceOf(Blob);
expect(segments[0].type).toBe('video/webm');
});
it('preserves segments in push order (oldest first)', () => {
pushSegmentForTest(makeSegmentBlob(0xaa));
pushSegmentForTest(makeSegmentBlob(0xbb));
pushSegmentForTest(makeSegmentBlob(0xcc));
const segments = getSegments();
expect(segments).toHaveLength(3);
// Read out the seed byte from each blob to confirm order.
return Promise.all(segments.map((s) => s.arrayBuffer())).then((bufs) => {
const firstBytes = bufs.map((b) => new Uint8Array(b)[0]);
expect(firstBytes).toEqual([0xaa, 0xbb, 0xcc]);
});
});
it('caps at MAX_SEGMENTS — 4th push evicts the oldest', () => {
pushSegmentForTest(makeSegmentBlob(0x11));
pushSegmentForTest(makeSegmentBlob(0x22));
pushSegmentForTest(makeSegmentBlob(0x33));
pushSegmentForTest(makeSegmentBlob(0x44));
const segments = getSegments();
expect(segments).toHaveLength(MAX_SEGMENTS);
return Promise.all(segments.map((s) => s.arrayBuffer())).then((bufs) => {
const firstBytes = bufs.map((b) => new Uint8Array(b)[0]);
// Oldest (0x11) is gone; the kept window is the 3 most-recent.
expect(firstBytes).toEqual([0x22, 0x33, 0x44]);
});
});
it('many pushes never exceed the cap', () => {
for (let i = 0; i < 20; i++) {
pushSegmentForTest(makeSegmentBlob(i));
}
expect(getSegments().length).toBeLessThanOrEqual(MAX_SEGMENTS);
expect(getSegments()).toHaveLength(MAX_SEGMENTS);
});
it('resetBuffer clears the segment array', () => {
pushSegmentForTest(makeSegmentBlob(0x1));
pushSegmentForTest(makeSegmentBlob(0x2));
expect(getSegments()).toHaveLength(2);
resetBuffer();
expect(getSegments()).toEqual([]);
});
it('getSegments returns a snapshot — mutating the result does not corrupt internal state', () => {
pushSegmentForTest(makeSegmentBlob(0x1));
pushSegmentForTest(makeSegmentBlob(0x2));
const snapshot = getSegments();
expect(snapshot).toHaveLength(2);
// Caller-side mutation must not leak back into the recorder module
// (defense against accidental aliasing in mergeVideoSegments).
snapshot.pop();
snapshot.push(makeSegmentBlob(0xff));
expect(getSegments()).toHaveLength(2);
});
});