---
phase: 1
slug: stabilize-video-pipeline
status: draft
nyquist_compliant: false
wave_0_complete: false
created: 2026-05-15
---

# Phase 1 — Validation Strategy

> Per-phase validation contract for feedback sampling during execution.
> Derived from `01-RESEARCH.md` §"Validation Architecture".

---

## Test Infrastructure

| Property | Value |
|----------|-------|
| **Framework** | **Vitest** (Node mode) — recommended, NOT currently installed. Vite already in devDependencies, so Vitest is a zero-config-mismatch add. |
| **Config file** | NONE — Wave 0 creates `vitest.config.ts`. |
| **Quick run command** | `npx vitest run --reporter=dot` |
| **Full suite command** | `npx vitest run && npx tsc --noEmit` (+ grep guards) |
| **Estimated runtime** | ~10 s quick, ~30 s full |

**Why not Jest:** Vite is already the build tool; Vitest is the zero-config-mismatch choice. No transformer dance for TS.

**Why not Playwright:** `MediaRecorder` + `getDisplayMedia` ARE driveable in Chromium via Playwright with permissions auto-granted, but the acceptance gate (ffprobe on a real exported file) requires actually running the extension. Manual smoke + ffprobe is sufficient for Phase 1. Playwright-driven smoke tests are Phase 4/5 territory.

---

## Sampling Rate

- **After every task commit:** Run `npx vitest run --reporter=dot && npx tsc --noEmit` (≤ 10 s).
- **After every plan wave:** Run `npx vitest run && npx tsc --noEmit && npm run build` (≤ 30 s).
- **Before `/gsd-verify-work`:** Full suite + ffprobe gate (D-12) must be green.
- **Max feedback latency:** 30 s.

---

## Per-Task Verification Map

> The exact task IDs are owned by PLAN.md. This map enumerates the behaviors and tests; the planner wires task IDs in once plans are written. Rows are sorted by acceptance-criterion order from CONTEXT.md.

| Behavior | Plan area | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|----------|-----------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
| Ring buffer adds chunk; first chunk gets `isHeader: true` (D-10) | offscreen/recorder | 1 | REQ-video-ring-buffer | — | N/A | unit | `npx vitest run tests/offscreen/ring-buffer.test.ts -t "first chunk is header"` | ❌ Wave 0 | ⬜ pending |
| Ring buffer trims chunks older than 30 s; keeps header (D-11) | offscreen/recorder | 1 | REQ-video-ring-buffer | — | N/A | unit | `npx vitest run tests/offscreen/ring-buffer.test.ts -t "trim 30s"` | ❌ Wave 0 | ⬜ pending |
| Codec strict-mode throws when vp9 unsupported (D-20) | offscreen/recorder | 1 | REQ-video-ring-buffer | T-1-01 (codec downgrade) | Fail loud; no silent fallback to vp8/h264 | unit | `npx vitest run tests/offscreen/codec-check.test.ts` | ❌ Wave 0 | ⬜ pending |
| OFFSCREEN_READY handshake sent before SW emits START_RECORDING (Pattern 4) | offscreen + SW | 1 | REQ-video-ring-buffer | — | N/A | unit | `npx vitest run tests/offscreen/handshake.test.ts` | ❌ Wave 0 | ⬜ pending |
| Port reconnect on `onDisconnect` within 1 s (Pattern 5) | offscreen ↔ SW | 1 | REQ-video-ring-buffer | — | N/A | unit | `npx vitest run tests/offscreen/port.test.ts -t "reconnects"` | ❌ Wave 0 | ⬜ pending |
| `chrome.alarms` keepalive deleted (D-18) | SW | 1 | REQ-video-ring-buffer | — | N/A | grep | `! grep -RIn "chrome.alarms" src/background/` | NO CODE NEEDED | ⬜ pending |
| IndexedDB SW path deleted (D-19) | SW | 1 | REQ-video-ring-buffer | — | N/A | grep | `! grep -RIn "VideoRecorderDB\|openIndexedDB" src/` | NO CODE NEEDED | ⬜ pending |
| `vite.config.ts:11-184` inline plugin deleted (D-08) | build | 1 | REQ-video-ring-buffer | — | N/A | grep | `! grep -RIn "copy-offscreen\|chromeMediaSource" vite.config.ts` | NO CODE NEEDED | ⬜ pending |
| `last_30sec.webm` plays ffprobe-clean (D-12 acceptance gate) | manual smoke | last | REQ-video-ring-buffer | — | N/A | integration | `ffprobe -v error -f matroska -i sample/last_30sec.webm; test $? -eq 0` | Sample produced during manual smoke; in CI a known-good fixture verifies the gate itself works | ⬜ pending |
| Zero `as any` / `@ts-ignore` regressions in Phase 1 surface | static | each | REQ-video-ring-buffer | — | N/A | static | `npx tsc --noEmit && ! grep -RIn "as any\|@ts-ignore" src/offscreen/ src/background/index.ts` | EXISTS (build script) | ⬜ pending |
| Manifest permission swap (D-A6 / D-05) | manifest | doc-cascade | REQ-video-ring-buffer | T-1-02 (excess permissions) | Drop `tabCapture`; add `desktopCapture` exactly | grep | `! grep "tabCapture" manifest.json && grep "desktopCapture" manifest.json` | NO CODE NEEDED | ⬜ pending |
| Build produces a loadable extension | build | last | REQ-video-ring-buffer | — | N/A | manual | `npm run build && ls dist/manifest.json dist/src/offscreen/index.html dist/assets/*.js` | NO TEST FILE; shell check | ⬜ pending |

*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*

---

## Wave 0 Requirements

- [ ] Install Vitest: `npm install -D vitest @vitest/ui` (verify current major via `npm view vitest version` at install time).
- [ ] `vitest.config.ts` — pull in path aliases from `tsconfig.json` (if any); enable Node test env by default.
- [ ] `tests/offscreen/` directory with at minimum:
  - [ ] `tests/offscreen/ring-buffer.test.ts` — D-10, D-11 (header pinning + 30 s trim).
  - [ ] `tests/offscreen/codec-check.test.ts` — D-20 strict-mode error path.
  - [ ] `tests/offscreen/handshake.test.ts` — Pattern 4 OFFSCREEN_READY.
  - [ ] `tests/offscreen/port.test.ts` — Pattern 5 reconnect.
- [ ] `tests/fixtures/` — keep a known-good WebM for ffprobe sanity (committed once; used by CI to verify the ffprobe gate runs at all).
- [ ] `npm test` script in `package.json`: `"test": "vitest run"`.
- [ ] `chrome` runtime stub for Vitest — light hand-rolled mock or `vitest-chrome` dependency (decide at install time).

---

## Manual-Only Verifications

| Behavior | Requirement | Why Manual | Test Instructions |
|----------|-------------|------------|-------------------|
| `getDisplayMedia` picker dialog appears and grants stream on operator click | REQ-video-ring-buffer | Browser picker UX cannot be driven in Vitest; auto-grant via Playwright is Phase 4 territory | Load `dist/` unpacked; click extension; observe picker; pick "Entire screen"; confirm "Sharing" indicator appears; SW console shows `[Offscreen] Stream created`. |
| `last_30sec.webm` actually plays in a browser (SPEC §10 #7) | REQ-video-ring-buffer | Decoder behavior depends on real Chromium media stack | Open the exported `last_30sec.webm` in Chrome; confirm video plays from start to end; no playback freezes. |
| SW idle survival (D-16, D-17) | REQ-video-ring-buffer | Requires real MV3 SW lifecycle | DevTools → Extensions → Service Worker → "Force stop"; wait 60 s; click extension save; confirm exported archive contains video chunks from before "Force stop". |
| "Stop sharing" recovery (D-03) | REQ-video-ring-buffer | Requires real browser stream lifecycle | Click "Stop sharing" in Chrome's screen-share banner; click extension save; expect graceful "no recording — re-prompt" UX (Phase 3 owns the popup state; Phase 1 owns the offscreen-side error signal). |

---

## Validation Sign-Off

- [ ] All tasks have `<automated>` verify or Wave 0 dependencies (planner enforces)
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
- [ ] Wave 0 covers all MISSING references (Vitest install + 4 test files + fixtures)
- [ ] No watch-mode flags (Vitest `run` mode only)
- [ ] Feedback latency < 30 s per wave
- [ ] `nyquist_compliant: true` set in frontmatter (planner flips this once PLAN.md tasks reference these test commands)

**Approval:** pending
