Inserts Plan 04-08 between Plans 04-06 and 04-07 (Wave 5.5) per debug session-2 verdict (REFUTED-architecture; canvas-captureStream issue). Scope: replace canvas.captureStream(30) source in installFakeDisplayMedia() at src/test-hooks/offscreen-hooks.ts:139-264 with HTMLVideoElement.captureStream backed by a bundled VP9 WebM at tests/uat/fixtures/synthetic-display-source.webm. Bundled via Vite ?url import per Plan 01-10 mokosh-mark precedent. Revives the A33 harness assertion (Plan 04-04 Pattern 4 verbatim) under valid methodology; stopServiceWorker helper from Plan 04-04 reused. Closes ROADMAP SC #1 within v1. Architecture (offscreen-RAM segments: Blob[]) UNCHANGED per debug session-2 segment-count probe evidence. 2 tasks atomic: (1) bundle fixture + rewrite installFakeDisplayMedia + ambient *.webm?url decl; (2) re-run spike + land driveA33 + orchestrator wiring + SKIP_LONG_UAT env-gate + SUMMARY + STATE/ROADMAP markers. UAT 33 -> 34 GREEN target. FORBIDDEN_HOOK_STRINGS unchanged at 12. Pre-checkpoint bundle gates 6/6 PASS preserved. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
805 lines
56 KiB
Markdown
805 lines
56 KiB
Markdown
---
|
|
phase: 04
|
|
slug: harden-clean-up-optional
|
|
plan: 08
|
|
type: auto
|
|
wave: 5.5
|
|
depends_on:
|
|
- 01
|
|
- 02
|
|
- 03
|
|
- 04
|
|
- 05
|
|
- 06
|
|
files_modified:
|
|
- tests/uat/fixtures/synthetic-display-source.webm
|
|
- src/test-hooks/offscreen-hooks.ts
|
|
- globals.d.ts
|
|
- manifest.json
|
|
- vite.config.ts
|
|
- vite.test.config.ts
|
|
- tests/uat/extension-page-harness.ts
|
|
- tests/uat/lib/harness-page-driver.ts
|
|
- tests/uat/harness.test.ts
|
|
- tests/uat/spike-a33-sw-persistence.ts
|
|
autonomous: true
|
|
requirements: []
|
|
tags:
|
|
- uat-harness
|
|
- a33
|
|
- methodology-reframe
|
|
- video-file-source
|
|
- htmlvideoelement-capturestream
|
|
- canvas-throttling-fix
|
|
- chrome-bug-653548
|
|
- charter-d-p4-01
|
|
- roadmap-sc-1-closes
|
|
- post-debug-session-2
|
|
user_setup: []
|
|
must_haves:
|
|
truths:
|
|
- "installFakeDisplayMedia() at src/test-hooks/offscreen-hooks.ts mints its MediaStream from an HTMLVideoElement playing a bundled WebM (NOT from canvas.captureStream); the video element loops a >=30s VP9 source so the MediaRecorder sees real frame data across the full 5-min spike window"
|
|
- "tests/uat/fixtures/synthetic-display-source.webm exists as a CC0/internal-owned >=1MB VP9 WebM with duration >=30s; bundled into the test-config build via Vite ?url import (per Plan 01-10 mokosh-mark.svg precedent)"
|
|
- "tests/uat/spike-a33-sw-persistence.ts re-runs end-to-end and produces videoSize > 100_000 (canonical run with worker.close()) — empirically refutes the previous 8505-byte 0-frames methodology failure and confirms ROADMAP SC #1 architectural integrity per debug session-2 verdict"
|
|
- "assertA33 / driveA33 / orchestrator wiring land per the original Plan 04-04 Pattern 4 verbatim (cs-injection-world precedent NOT applicable — A33 is host-side CDP-driven, not page-side rrweb-driven)"
|
|
- "stopServiceWorker(browser, extensionId) helper at tests/uat/lib/harness-page-driver.ts is reused verbatim from Plan 04-04 (already committed at 3726eee); no new helper symbol needed"
|
|
- "A33 env-gated by SKIP_LONG_UAT (default RUN for closure + alpha gate; SKIP_LONG_UAT=1 to skip per-commit iteration); 95s baseline preserved when skipped"
|
|
- "UAT harness count flips 33 -> 34 (A33 added); 34/34 GREEN when SKIP_LONG_UAT unset; vitest 183/183 GREEN preserved"
|
|
- "ROADMAP SC #1 (SW state persistence) GREEN — A33 empirical evidence that a 5-min idle + Puppeteer worker.close() + SAVE_ARCHIVE produces video/last_30sec.webm with size > 100 KB"
|
|
- "Tier-1 FORBIDDEN_HOOK_STRINGS inventory unchanged at 12 entries (lockstep across tests/uat/harness.test.ts + tests/background/no-test-hooks-in-prod-bundle.test.ts) — Plan 04-08 introduces NO new __MOKOSH_UAT__-gated symbols on production bundle; the bundled WebM ships only in test build per Vite ?url + test-config gating"
|
|
- "Pre-checkpoint bundle gates 6/6 PASS unchanged from Plan 04-03 baseline (new Function=0 + eval=0 + Buffer.=1 pre-existing JSZip + window./document.=0 in SW + Tier-1 inventory=12 + en/ru parity preserved) — production source delta is zero (only src/test-hooks/* changes; tree-shaken in production per __MOKOSH_UAT__ gate)"
|
|
- "Architecture integrity preserved per debug session-2 verdict — src/offscreen/recorder.ts:91 `let segments: Blob[] = []` RAM-only architecture is canonically correct; no IndexedDB persistence work, no chrome.storage migration, no offscreen-document lifecycle changes"
|
|
artifacts:
|
|
- path: "tests/uat/fixtures/synthetic-display-source.webm"
|
|
provides: "Bundled CC0/internal VP9 WebM source for HTMLVideoElement.captureStream — replaces canvas-captureStream invisible-source throttling. Size 1-3 MB; duration >=30s; loopable (autoplay+loop+muted). License: internal capture by this project's MediaRecorder pipeline (CC0-equivalent project-owned)"
|
|
min_size_bytes: 1000000
|
|
- path: "src/test-hooks/offscreen-hooks.ts"
|
|
provides: "installFakeDisplayMedia() rewritten to mint MediaStream from HTMLVideoElement.captureStream(30); displaySurface monkey-patch preserved; lifecycle (idempotent install + idempotent uninstall) preserved; A23 constraints capture preserved; all 6 existing bridge ops (install-fake-display-media + dispatch-ended + has-stream + get-display-surface + get-segment-count + get-last-getDisplayMedia-constraints) preserved"
|
|
contains: "videoEl.captureStream"
|
|
- path: "globals.d.ts"
|
|
provides: "Ambient declaration for `*.webm?url` Vite asset import (mirrors existing `*.svg?url` block at lines 34-37)"
|
|
contains: "webm?url"
|
|
- path: "tests/uat/extension-page-harness.ts"
|
|
provides: "assertA33 page-side method registered on __mokoshHarness OR existing assertA2 reused for prime step (decided by read_first; Plan 04-04 REVISION iter-2 Option B precedent prefers reuse)"
|
|
contains: "assertA33"
|
|
- path: "tests/uat/lib/harness-page-driver.ts"
|
|
provides: "driveA33(page, browser, extensionId, downloadsDir) host-side driver implementing the 5-min idle + stopServiceWorker + SAVE_ARCHIVE + JSZip video-size check (Plan 04-04 Pattern 4 verbatim; REVISION iter-2 Option B inline SAVE_ARCHIVE dispatch)"
|
|
contains: "driveA33"
|
|
- path: "tests/uat/harness.test.ts"
|
|
provides: "driveA33 import + driveA33Wrapped wrapped-driver const + drivers-array push entry with SKIP_LONG_UAT env-gate (per Plan 04-04 Wave 1 spec verbatim)"
|
|
contains: "driveA33Wrapped"
|
|
key_links:
|
|
- from: "src/test-hooks/offscreen-hooks.ts installFakeDisplayMedia"
|
|
to: "tests/uat/fixtures/synthetic-display-source.webm via Vite ?url import"
|
|
via: "import videoUrl from '../../tests/uat/fixtures/synthetic-display-source.webm?url' OR equivalent gated import"
|
|
pattern: "synthetic-display-source\\.webm\\?url"
|
|
- from: "src/test-hooks/offscreen-hooks.ts mintStream()"
|
|
to: "HTMLVideoElement.captureStream(30) + displaySurface monkey-patch"
|
|
via: "videoEl.captureStream(30); patchDisplaySurface(stream)"
|
|
pattern: "videoEl\\.captureStream"
|
|
- from: "tests/uat/harness.test.ts driveA33Wrapped"
|
|
to: "tests/uat/lib/harness-page-driver.ts driveA33(page, browser, extensionId, downloadsDir)"
|
|
via: "(page) => driveA33(page, handles.browser, handles.extensionId, handles.downloadsDir)"
|
|
pattern: "handles\\.browser.*handles\\.extensionId"
|
|
- from: "tests/uat/lib/harness-page-driver.ts driveA33 video-size check"
|
|
to: "zip.file('video/last_30sec.webm') -> byteLength > 100_000"
|
|
via: "JSZip.loadAsync + entry.async('uint8array')"
|
|
pattern: "video/last_30sec\\.webm"
|
|
---
|
|
|
|
<objective>
|
|
**Methodology reframe for ROADMAP SC #1.** Per debug session-2 (`.planning/debug/sw-offscreen-persistence-investigation-session-2.md`, commit `4ea1bbb`), the Plan 04-04 SPIKE FAILED outcome (8505 bytes) is empirically REFUTED as an architectural failure. Three independent probes converged on the canonical NO answer:
|
|
|
|
1. **Segment-count probes** at POST-PRIME/PRE-KILL/POST-KILL: count=0 / count=3 / count=3 — segments accumulated correctly AND survived the SW kill. The `let segments: Blob[] = []` RAM-only architecture (src/offscreen/recorder.ts:91) is NOT broken.
|
|
2. **Step C variant** (SPIKE_SKIP_SW_KILL=1; no worker.close()): IDENTICAL 8505-byte failure — Puppeteer CDP `worker.close()` is NOT the cause.
|
|
3. **Direct Remux logs** (visible because SW respawn was skipped in Step C): `Segment ts=1..3: 0 frames, duration=0ms, trackInfo=320x180`; `Remux complete: 0 frames, total timeline=0ms, output=8505 bytes`.
|
|
|
|
**Root cause:** `installFakeDisplayMedia()` at `src/test-hooks/offscreen-hooks.ts:139-264` mints a `canvas.captureStream(30)` from a hidden -9999px-offset 320x180 canvas. Despite the existing `setInterval(drawFrame, 33ms)` belt-and-suspenders against RAF throttling, **headless-Chromium aggressively throttles MediaRecorder on invisible-canvas sources** (Chrome bug 653548; chromium auto-throttled-screen-capture design doc; sendrec.eu "Why Canvas Breaks Your Screen Recorder"). The MediaRecorder emits structurally-valid WebM with valid V_VP9 track metadata (320x180) but **zero VP9 frames per segment** across the 5-min idle window.
|
|
|
|
**Fix:** Replace the canvas.captureStream source with an `HTMLVideoElement` playing a bundled WebM (Option 2 from session-2 recommendations). Video-file-backed `videoEl.captureStream(30)` is not subject to invisible-canvas throttling — the source is a real media element with a real decoded frame timeline. The displaySurface monkey-patch (production code's monitor-only gate) is preserved verbatim; the lifecycle (idempotent install + uninstall) is preserved; the existing bridge ops (install-fake-display-media + dispatch-ended + has-stream + get-display-surface + get-segment-count + get-last-getDisplayMedia-constraints) are preserved.
|
|
|
|
**Revival path:** Once methodology produces real frames, re-run `tests/uat/spike-a33-sw-persistence.ts` — it MUST exit 0 with `videoSize > 100_000`. Then land the A33 harness assertion per the original Plan 04-04 Pattern 4 verbatim: `driveA33` host-side driver + orchestrator wiring + `SKIP_LONG_UAT` env-gate. The `stopServiceWorker(browser, extensionId)` helper from Plan 04-04 (committed at `3726eee`) is REUSED verbatim — no new test-only symbols introduced; FORBIDDEN_HOOK_STRINGS inventory unchanged at 12.
|
|
|
|
Purpose: Closes ROADMAP SC #1 ("After running the extension idle for >5 minutes, then exporting, the archive still contains a non-empty video buffer") within v1 by reframing the verification methodology (NOT the architecture). Honors the spike-FAILED-but-architecture-OK verdict from debug session-2; rejects the previously-proposed IndexedDB persistence plan-fix (which would not have closed SC #1 — the spike would STILL produce 8505 bytes after IDB lands because the failure is in the test's fake stream, not in segment persistence).
|
|
|
|
Output: 1 NEW bundled fixture (`tests/uat/fixtures/synthetic-display-source.webm`); rewrite of `installFakeDisplayMedia()` (canvas -> HTMLVideoElement); 1 ambient module declaration for `*.webm?url`; 1 NEW harness assertion (A33; harness count 33->34); driveA33 + orchestrator wiring; spike script re-runs PASSED. ROADMAP SC #1 flips OPEN -> GREEN.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/04-harden-clean-up-optional/04-CONTEXT.md
|
|
@.planning/phases/04-harden-clean-up-optional/04-PATTERNS.md
|
|
@.planning/phases/04-harden-clean-up-optional/04-04-PLAN.md
|
|
@.planning/phases/04-harden-clean-up-optional/04-04-SUMMARY.md
|
|
@.planning/debug/sw-offscreen-persistence-investigation-session-2.md
|
|
|
|
# Source files — locus of the methodology reframe
|
|
@src/test-hooks/offscreen-hooks.ts
|
|
@src/offscreen/recorder.ts
|
|
@tests/uat/extension-page-harness.ts
|
|
@tests/uat/lib/harness-page-driver.ts
|
|
@tests/uat/harness.test.ts
|
|
@tests/uat/lib/launch.ts
|
|
@tests/uat/spike-a33-sw-persistence.ts
|
|
|
|
# Vite ?url import precedent (Plan 01-10 mokosh-mark.svg pattern)
|
|
@src/welcome/welcome.ts
|
|
@globals.d.ts
|
|
@manifest.json
|
|
@vite.config.ts
|
|
@vite.test.config.ts
|
|
|
|
# Prior plan SUMMARYs — direct ancestors
|
|
@.planning/phases/01-stabilize-video-pipeline/01-10-SUMMARY.md
|
|
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-04-SUMMARY.md
|
|
|
|
<interfaces>
|
|
<!-- Key shapes the executor consumes directly. Extracted from codebase + debug session-2 2026-05-22. -->
|
|
|
|
From src/test-hooks/offscreen-hooks.ts:139-264 (CURRENT installFakeDisplayMedia — the methodology failure locus):
|
|
```typescript
|
|
// CURRENT (broken under 5-min headless idle per debug session-2 verdict):
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = 320; canvas.height = 180;
|
|
canvas.style.position = 'fixed'; canvas.style.top = '-9999px'; canvas.style.left = '-9999px';
|
|
document.body.appendChild(canvas);
|
|
const ctx = canvas.getContext('2d');
|
|
// ... drawFrame loop + setInterval(drawFrame, 33) ...
|
|
const mintStream = (): MediaStream => {
|
|
const stream = canvas.captureStream(30); // <- THE THROTTLING SOURCE
|
|
patchDisplaySurface(stream);
|
|
return stream;
|
|
};
|
|
```
|
|
|
|
REPLACEMENT pattern (HTMLVideoElement.captureStream — Plan 04-08):
|
|
```typescript
|
|
// Plan 04-08 methodology reframe — video-file-backed MediaStream.
|
|
// canvas.captureStream(30) on a hidden -9999px canvas is throttled to
|
|
// near-zero frames per second under headless Chromium 5-min idle
|
|
// (Chrome bug 653548; debug session-2 2026-05-22). An HTMLVideoElement
|
|
// playing a real WebM source is NOT subject to invisible-canvas
|
|
// throttling because the source has a real decoded frame timeline
|
|
// independent of canvas pixel mutations.
|
|
//
|
|
// References:
|
|
// - debug session-2 verdict at .planning/debug/sw-offscreen-persistence-investigation-session-2.md
|
|
// - HTMLVideoElement.captureStream:
|
|
// https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/captureStream
|
|
// - Chrome bug 653548 (auto-throttled canvas captureStream):
|
|
// https://bugs.chromium.org/p/chromium/issues/detail?id=653548
|
|
|
|
import sourceUrl from '../../tests/uat/fixtures/synthetic-display-source.webm?url';
|
|
|
|
const videoEl = document.createElement('video');
|
|
videoEl.src = sourceUrl;
|
|
videoEl.loop = true; // loop indefinitely so 5-min idle has source frames
|
|
videoEl.muted = true; // muted required for autoplay per Chrome policy
|
|
videoEl.autoplay = true; // ensure playback starts without user gesture
|
|
videoEl.style.position = 'fixed';
|
|
videoEl.style.top = '-9999px';
|
|
videoEl.style.left = '-9999px';
|
|
document.body.appendChild(videoEl);
|
|
|
|
// Wait for the first frame to ensure captureStream returns live tracks
|
|
// (not stalled tracks awaiting first decode). Use 'canplay' event +
|
|
// explicit .play() for headless headless reliability.
|
|
await new Promise<void>((resolve, reject) => {
|
|
videoEl.addEventListener('canplay', () => resolve(), { once: true });
|
|
videoEl.addEventListener('error', () => reject(new Error('video load failed')), { once: true });
|
|
});
|
|
await videoEl.play();
|
|
|
|
const mintStream = (): MediaStream => {
|
|
// captureStream(30) — request 30 fps; the actual cadence is driven
|
|
// by the source video's frame timeline.
|
|
const stream = (videoEl as HTMLVideoElement & {
|
|
captureStream: (fps?: number) => MediaStream;
|
|
}).captureStream(30);
|
|
patchDisplaySurface(stream); // preserve existing monkey-patch
|
|
return stream;
|
|
};
|
|
```
|
|
|
|
From src/welcome/welcome.ts:46 (Plan 01-10 Vite ?url precedent — direct analog):
|
|
```typescript
|
|
import markUrl from '../shared/brand/mokosh-mark.svg?url';
|
|
```
|
|
|
|
From globals.d.ts:34-37 (Plan 01-10 ambient module decl — direct analog to copy):
|
|
```typescript
|
|
declare module '*.svg?url' {
|
|
const url: string;
|
|
export default url;
|
|
}
|
|
// Plan 04-08 adds the parallel block:
|
|
declare module '*.webm?url' {
|
|
const url: string;
|
|
export default url;
|
|
}
|
|
```
|
|
|
|
From src/test-hooks/offscreen-hooks.ts:271-291 (uninstallFakeDisplayMedia — preserve idempotent teardown):
|
|
```typescript
|
|
// Existing teardown handles canvas removal + RAF cancel + interval clear.
|
|
// Plan 04-08 adapts: cancel video, removeChild(videoEl), null the refs.
|
|
// Idempotency contract unchanged.
|
|
```
|
|
|
|
From tests/uat/lib/harness-page-driver.ts:68-80 (stopServiceWorker helper — REUSED verbatim from Plan 04-04):
|
|
```typescript
|
|
// Already committed at 3726eee; Plan 04-08 reuses without modification.
|
|
export async function stopServiceWorker(browser: Browser, extensionId: string): Promise<void> {
|
|
const host = `chrome-extension://${extensionId}`;
|
|
const target = await browser.waitForTarget(
|
|
(t) => t.type() === 'service_worker' && t.url().startsWith(host),
|
|
);
|
|
const worker = await target.worker();
|
|
if (worker !== null) {
|
|
await worker.close();
|
|
}
|
|
}
|
|
```
|
|
|
|
From Plan 04-04 PLAN.md Pattern 4 (driveA33 host-side body — REVIVED VERBATIM under valid methodology):
|
|
```typescript
|
|
const A33_IDLE_WAIT_MS = 5 * 60 * 1000;
|
|
const A33_NEW_SW_BOOT_MS = 500;
|
|
const A33_OVERALL_TIMEOUT_MS = A33_IDLE_WAIT_MS + 60_000;
|
|
const A33_SAVE_ARCHIVE_TIMEOUT_MS = 15_000;
|
|
const A33_DOWNLOAD_SETTLE_MS = 5_000;
|
|
const A33_VIDEO_SIZE_FLOOR_BYTES = 100_000;
|
|
|
|
export async function driveA33(
|
|
page: Page,
|
|
browser: Browser,
|
|
extensionId: string,
|
|
downloadsDir: string,
|
|
): Promise<AssertionRecord> {
|
|
const r: AssertionRecord = { name: 'A33', passed: false, checks: [], diagnostics: [] };
|
|
|
|
// Step 1: prime via __mokoshHarness.assertA2 (canonical fresh-recording bootstrap;
|
|
// Plan 04-04 REVISION iter-2 Option B; verified via existing harness surface).
|
|
await page.evaluate(async () => {
|
|
const harness = (window as unknown as {
|
|
__mokoshHarness: { assertA2: () => Promise<{ passed: boolean; error?: string }> };
|
|
}).__mokoshHarness;
|
|
const a2 = await harness.assertA2();
|
|
if (!a2.passed) {
|
|
throw new Error(`assertA2 priming failed: ${a2.error ?? '(no error)'}`);
|
|
}
|
|
});
|
|
r.diagnostics.push('Step 1 OK: assertA2 prime -> REC');
|
|
|
|
// Step 2: 5-min wall-clock idle.
|
|
r.diagnostics.push(`Step 2: waiting ${A33_IDLE_WAIT_MS}ms for SW idle window`);
|
|
await new Promise((res) => setTimeout(res, A33_IDLE_WAIT_MS));
|
|
|
|
// Step 3: force SW termination via CDP.
|
|
await stopServiceWorker(browser, extensionId);
|
|
r.diagnostics.push('Step 3 OK: SW terminated via worker.close()');
|
|
|
|
// Step 4: brief settle for SW teardown.
|
|
await new Promise((res) => setTimeout(res, A33_NEW_SW_BOOT_MS));
|
|
|
|
// Step 5: SAVE_ARCHIVE inline dispatch from harness-page realm
|
|
// (Plan 04-04 REVISION iter-2 Option B; wakes SW event-driven).
|
|
const saveResult = await page.evaluate(
|
|
(timeoutMs: number) =>
|
|
new Promise<{ success: boolean; error?: string }>((resolve) => {
|
|
const timer = setTimeout(() => {
|
|
resolve({ success: false, error: `SAVE_ARCHIVE timed out after ${timeoutMs}ms` });
|
|
}, timeoutMs);
|
|
chrome.runtime.sendMessage({ type: 'SAVE_ARCHIVE' }, (response: unknown) => {
|
|
clearTimeout(timer);
|
|
if (chrome.runtime.lastError !== undefined) {
|
|
resolve({ success: false, error: String(chrome.runtime.lastError.message) });
|
|
return;
|
|
}
|
|
resolve(response as { success: boolean; error?: string });
|
|
});
|
|
}),
|
|
A33_SAVE_ARCHIVE_TIMEOUT_MS,
|
|
);
|
|
r.checks.push({
|
|
name: 'A33.1: SAVE_ARCHIVE ack success after 5-min idle + SW kill',
|
|
expected: true,
|
|
actual: saveResult.success,
|
|
passed: saveResult.success === true,
|
|
});
|
|
|
|
// Step 6: settle for chrome.downloads to finish.
|
|
await new Promise((res) => setTimeout(res, A33_DOWNLOAD_SETTLE_MS));
|
|
|
|
// Step 7: locate zip + measure video entry.
|
|
const zipPath = findLatestZip(downloadsDir);
|
|
if (zipPath === null) {
|
|
r.checks.push({ name: 'A33.0: zip present', expected: '>=1 zip', actual: 'none', passed: false });
|
|
r.passed = false;
|
|
return r;
|
|
}
|
|
const zip = await JSZip.loadAsync(readFileSync(zipPath));
|
|
const videoEntry = zip.file('video/last_30sec.webm');
|
|
const videoSize = videoEntry !== null
|
|
? (await videoEntry.async('uint8array')).byteLength
|
|
: 0;
|
|
r.checks.push({
|
|
name: 'A33.2: video/last_30sec.webm size > 0 (buffer survived SW eviction)',
|
|
expected: '>0',
|
|
actual: String(videoSize),
|
|
passed: videoSize > 0,
|
|
});
|
|
r.checks.push({
|
|
name: 'A33.3: video size > 100 KB (sanity floor; real archives 1-3 MB)',
|
|
expected: `>${A33_VIDEO_SIZE_FLOOR_BYTES}`,
|
|
actual: String(videoSize),
|
|
passed: videoSize > A33_VIDEO_SIZE_FLOOR_BYTES,
|
|
});
|
|
|
|
r.passed = r.checks.every((c) => c.passed);
|
|
return r;
|
|
}
|
|
```
|
|
|
|
From tests/uat/harness.test.ts:486 (orchestrator drivers-array — append site for A33):
|
|
```typescript
|
|
// Plan 04-08 — driveA33 lands HERE, after the existing A32 entry at line 486.
|
|
{
|
|
name: 'A33',
|
|
drive: process.env.SKIP_LONG_UAT === '1'
|
|
? async (): Promise<AssertionRecord> => ({
|
|
name: 'A33',
|
|
passed: true,
|
|
checks: [],
|
|
diagnostics: ['A33 SKIPPED (SKIP_LONG_UAT=1; unset to run 5-min idle test)'],
|
|
})
|
|
: driveA33Wrapped,
|
|
},
|
|
```
|
|
|
|
From tests/fixtures/last_30sec.webm (existing internal project artifact — candidate fixture source):
|
|
- size: 1.9 MB
|
|
- codec: VP9
|
|
- dimensions: 1142x1044
|
|
- license: internal project capture (CC0-equivalent project-owned)
|
|
- A direct copy/symlink to `tests/uat/fixtures/synthetic-display-source.webm` is the simplest baseline; the executor MAY regenerate a smaller/looped/cleaner WebM via ffmpeg if preferred. Either choice satisfies the >=1 MB + VP9 + >=30s contract.
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Bundle WebM fixture + replace canvas.captureStream with HTMLVideoElement.captureStream in installFakeDisplayMedia()</name>
|
|
<files>tests/uat/fixtures/synthetic-display-source.webm, src/test-hooks/offscreen-hooks.ts, globals.d.ts, manifest.json, vite.config.ts, vite.test.config.ts</files>
|
|
<read_first>src/test-hooks/offscreen-hooks.ts (full file — installFakeDisplayMedia at lines 139-264 + uninstallFakeDisplayMedia at 271-291), src/welcome/welcome.ts:30-50 (Vite ?url import precedent), globals.d.ts (full — ambient module declaration pattern), manifest.json (full — web_accessible_resources block at line 20), vite.config.ts (full — rollupOptions.input + nodePolyfills config), vite.test.config.ts (full — test-build entry mirror), tests/fixtures/last_30sec.webm (verify file exists + VP9 codec + ~1.9 MB), tests/uat/spike-a33-sw-persistence.ts (full — to understand the exact prime + SAVE flow the new methodology will be tested against), .planning/debug/sw-offscreen-persistence-investigation-session-2.md (Resolution section — fix recommendations)</read_first>
|
|
<action>
|
|
**Step 1 — Bundle the WebM fixture.**
|
|
- Create directory: `mkdir -p tests/uat/fixtures/`.
|
|
- Copy `tests/fixtures/last_30sec.webm` to `tests/uat/fixtures/synthetic-display-source.webm`. The existing artifact is a 1.9 MB VP9 WebM internally captured by this project's MediaRecorder pipeline — CC0-equivalent project-owned per the "internal capture" provenance documented in `.planning/STATE.md` Phase 1 Closure Notes. Verify codec via `ffprobe -v error -show_entries stream=codec_name,width,height -of default=nw=1 tests/uat/fixtures/synthetic-display-source.webm` reports `codec_name=vp9`.
|
|
- The fixture must be >=1 MB AND >=30 seconds duration (loopable). The existing 1.9 MB capture satisfies size; duration is `N/A` per ffprobe (live-capture WebM lacks a duration tag), BUT the 1.9 MB VP9 at 400 kbps yields ~38s of decoded timeline — sufficient for the 5-min spike via `videoEl.loop = true`. If the executor wants a stricter contract, regenerate via `ffmpeg -f lavfi -i testsrc=duration=35:size=320x180:rate=30 -c:v libvpx-vp9 -b:v 400k tests/uat/fixtures/synthetic-display-source.webm` (this is OPTIONAL — the copy baseline works; only regenerate if the copy's duration tag is load-bearing for the chosen tests).
|
|
- Decision (per `feedback-no-unilateral-scope-reduction.md`): the executor's default is the COPY path (zero-cost, project-owned, already proven to play in Chrome per Plan 01-07). Regeneration is an OPTIONAL upgrade if the baseline fails to loop cleanly in headless captureStream.
|
|
|
|
**Step 2 — Add ambient module declaration for `*.webm?url`.**
|
|
- Edit `globals.d.ts`. After the existing `declare module '*.svg?url' { ... }` block at lines 34-37, append:
|
|
```typescript
|
|
// Plan 04-08 — Vite `?url` import for bundled test-only WebM fixture
|
|
// (tests/uat/fixtures/synthetic-display-source.webm). Mirrors the
|
|
// Plan 01-10 mokosh-mark.svg precedent; only gated test builds resolve
|
|
// the import (offscreen-hooks.ts is tree-shaken in production per
|
|
// `__MOKOSH_UAT__`).
|
|
declare module '*.webm?url' {
|
|
const url: string;
|
|
export default url;
|
|
}
|
|
```
|
|
|
|
**Step 3 — Rewrite `installFakeDisplayMedia()` to use HTMLVideoElement.captureStream.**
|
|
- Edit `src/test-hooks/offscreen-hooks.ts` at lines 139-264.
|
|
- Add top-of-module import (after existing imports):
|
|
```typescript
|
|
// Plan 04-08 — bundled WebM source for HTMLVideoElement-backed MediaStream.
|
|
// Replaces the prior canvas.captureStream(30) source which was throttled
|
|
// to 0 frames/segment under 5-min headless Chromium idle (Chrome bug
|
|
// 653548; debug session-2 verdict 2026-05-22). The video file's real
|
|
// decoded frame timeline is not subject to invisible-canvas throttling.
|
|
//
|
|
// The `?url` import resolves to a Vite-emitted hashed asset URL only in
|
|
// test builds (offscreen-hooks.ts is gated by `__MOKOSH_UAT__` in
|
|
// src/offscreen/recorder.ts and tree-shaken in production).
|
|
//
|
|
// References:
|
|
// - .planning/debug/sw-offscreen-persistence-investigation-session-2.md
|
|
// - https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/captureStream
|
|
// - Chrome bug 653548: https://bugs.chromium.org/p/chromium/issues/detail?id=653548
|
|
import syntheticDisplaySourceUrl from '../../tests/uat/fixtures/synthetic-display-source.webm?url';
|
|
```
|
|
- Replace the `installFakeDisplayMedia` body (lines 139-264). Preserve:
|
|
- Idempotency guard (`if (fakeInstalled) return;`).
|
|
- The `lastGetDisplayMediaConstraints` capture (A23 contract).
|
|
- The displaySurface monkey-patch via `patchDisplaySurface(stream)`.
|
|
- The `mintStream()` factory pattern (fresh MediaStream per `getDisplayMedia` call).
|
|
- The fakeGetDisplayMedia function shape (cast-through-unknown to assign to navigator.mediaDevices.getDisplayMedia).
|
|
- Replace canvas creation with HTMLVideoElement creation:
|
|
```typescript
|
|
// Build a hidden HTMLVideoElement playing the bundled WebM source.
|
|
// The element stays off-screen (-9999px) so it has zero visual side
|
|
// effect; loop=true ensures source frames never run out across the
|
|
// 5-min spike window; muted=true is required for autoplay per the
|
|
// Chrome autoplay policy (audio policy doesn't gate muted video).
|
|
const videoEl = document.createElement('video');
|
|
videoEl.src = syntheticDisplaySourceUrl;
|
|
videoEl.loop = true;
|
|
videoEl.muted = true;
|
|
videoEl.autoplay = true;
|
|
videoEl.playsInline = true;
|
|
videoEl.style.position = 'fixed';
|
|
videoEl.style.top = '-9999px';
|
|
videoEl.style.left = '-9999px';
|
|
document.body.appendChild(videoEl);
|
|
fakeVideoEl = videoEl;
|
|
|
|
// Wait for first-frame readiness so captureStream returns live tracks
|
|
// (not stalled tracks awaiting decode). canplay = readyState >= 3
|
|
// (HAVE_FUTURE_DATA). Reject path drops a single explicit error so a
|
|
// misconfigured fixture surfaces immediately (rather than producing a
|
|
// mysterious 0-frames downstream).
|
|
await new Promise<void>((resolve, reject) => {
|
|
const onCanPlay = (): void => {
|
|
videoEl.removeEventListener('canplay', onCanPlay);
|
|
videoEl.removeEventListener('error', onError);
|
|
resolve();
|
|
};
|
|
const onError = (): void => {
|
|
videoEl.removeEventListener('canplay', onCanPlay);
|
|
videoEl.removeEventListener('error', onError);
|
|
reject(new Error('synthetic-display-source.webm failed to load'));
|
|
};
|
|
videoEl.addEventListener('canplay', onCanPlay);
|
|
videoEl.addEventListener('error', onError);
|
|
});
|
|
|
|
// Explicit .play() — autoplay attr is best-effort; .play() returns a
|
|
// Promise that resolves once playback starts. Headless Chrome reliably
|
|
// honors muted-autoplay but the explicit call removes any policy edge.
|
|
await videoEl.play();
|
|
```
|
|
- Delete the canvas + ctx + drawFrame + requestAnimationFrame + setInterval block (lines 160-199 in current source). The video-file source provides frames inherently — no synthetic frame driver needed.
|
|
- Update `mintStream()` to call `videoEl.captureStream(30)` instead of `canvas.captureStream(30)`:
|
|
```typescript
|
|
const mintStream = (): MediaStream => {
|
|
// HTMLVideoElement.captureStream(30) — the source video's frame
|
|
// timeline drives cadence; the 30 fps argument is the requested
|
|
// upper bound. The track's getSettings() reports the resolved
|
|
// frameRate after capture begins.
|
|
//
|
|
// TypeScript cast through unknown because HTMLMediaElement.captureStream
|
|
// is not in the default lib.dom.d.ts at all versions (it's a non-
|
|
// standard but widely-implemented method; the cast pins our reliance
|
|
// on the runtime surface).
|
|
const stream = (videoEl as HTMLVideoElement & {
|
|
captureStream: (fps?: number) => MediaStream;
|
|
}).captureStream(30);
|
|
patchDisplaySurface(stream);
|
|
return stream;
|
|
};
|
|
```
|
|
- Update module-level state declarations:
|
|
```typescript
|
|
// Replace: let fakeCanvas: HTMLCanvasElement | null = null;
|
|
// Replace: let fakeAnimationHandle: number | null = null;
|
|
// Replace: let fakeDrawInterval: ReturnType<typeof setInterval> | null = null;
|
|
// With:
|
|
let fakeVideoEl: HTMLVideoElement | null = null;
|
|
```
|
|
- Note: `installFakeDisplayMedia()` becomes async (it now `await`s the canplay event + .play()). The existing module-load eager call at the bottom of the file (lines 533-537) wraps in try/catch — update to either: (a) `void installFakeDisplayMedia().catch(...)` OR (b) leave the try/catch but accept that the wrapper now logs the rejection. Recommendation: option (a) for explicit fire-and-forget semantics; the bridge op `install-fake-display-media` (line 408-418) already awaits-and-responds properly.
|
|
- Update the bridge handler at lines 408-418 to await the async install:
|
|
```typescript
|
|
if (op === 'install-fake-display-media') {
|
|
try {
|
|
await installFakeDisplayMedia();
|
|
sendResponse({ ok: true });
|
|
} catch (err) {
|
|
sendResponse({
|
|
ok: false,
|
|
error: err instanceof Error ? err.message : String(err),
|
|
});
|
|
}
|
|
return true; // signal async response (was false; required for await)
|
|
}
|
|
```
|
|
Note: the listener's outer signature must allow `return true` for async ops — verify by checking neighboring ops; the listener returns false for sync, true for async. Update the wrapper to match the async contract.
|
|
|
|
**Step 4 — Update `uninstallFakeDisplayMedia()` to teardown the video element.**
|
|
- Edit `src/test-hooks/offscreen-hooks.ts` at lines 271-291. Replace canvas cleanup with video cleanup:
|
|
```typescript
|
|
export function uninstallFakeDisplayMedia(): void {
|
|
if (!fakeInstalled) {
|
|
return;
|
|
}
|
|
fakeInstalled = false;
|
|
if (fakeVideoEl !== null) {
|
|
try {
|
|
fakeVideoEl.pause();
|
|
} catch {
|
|
// ignore pause errors during teardown
|
|
}
|
|
fakeVideoEl.remove();
|
|
fakeVideoEl = null;
|
|
}
|
|
// We deliberately do NOT restore the original getDisplayMedia — the
|
|
// offscreen document is throwaway and gets a fresh navigator on the
|
|
// next createDocument() anyway.
|
|
}
|
|
```
|
|
|
|
**Step 5 — Bundle gating (decide if WAR entry needed).**
|
|
- The Vite `?url` import resolves the WebM via Vite's asset pipeline. For the offscreen document (which is loaded by `chrome.offscreen.createDocument({url: 'src/offscreen/index.html', ...})`), the `?url` import emits the asset to `dist-test/assets/<hash>.webm` and `@crxjs/vite-plugin` auto-generates a `web_accessible_resources` entry for the offscreen-reachable asset (same pattern as Plan 01-10 mokosh-mark.svg per the existing welcome.ts precedent).
|
|
- **Decision (executor verifies during Step 6 verify):** if `@crxjs/vite-plugin` auto-WARs the asset (high confidence per Plan 01-10 precedent + Plan 01-12 RESEARCH §155), NO manifest.json edit is required. If WAR auto-generation fails (verified by `grep` of the test-build dist-test/manifest.json post-build), add an explicit entry:
|
|
```json
|
|
{
|
|
"resources": ["assets/*.webm"],
|
|
"matches": ["<all_urls>"]
|
|
}
|
|
```
|
|
The decision is captured in the SUMMARY; the BASELINE is "no manifest edit needed" per the existing analog.
|
|
|
|
**Step 6 — Update Vite configs if necessary.**
|
|
- `vite.config.ts`: production build — verify nothing changes. The test-hooks module is tree-shaken in production per `__MOKOSH_UAT__`; the `?url` import resolves only in the test build.
|
|
- `vite.test.config.ts`: test build — verify the existing rollupOptions covers the offscreen entry (it already does per Plan 01-13). No new rollupOptions entry needed; the `?url` import is a side-effect of the offscreen-hooks.ts module being included in the test build via the existing gated dynamic import at src/offscreen/recorder.ts:46-48.
|
|
|
|
**Step 7 — Verify TypeScript + production build clean.**
|
|
- `npx tsc --noEmit` exits 0.
|
|
- `npm run build` exits 0 (production build; test-hooks tree-shake verified — `grep -c 'synthetic-display-source' dist/` returns 0 across all production chunks).
|
|
- `npm run build:test` exits 0 (test build; `?url` import resolves to hashed asset in dist-test/; `grep -l 'synthetic-display-source\.webm' dist-test/` returns ≥ 1 hit pointing at the offscreen chunk).
|
|
- `wc -c tests/uat/fixtures/synthetic-display-source.webm` reports ≥ 1_000_000 bytes.
|
|
- `grep -c "canvas.captureStream\|fakeCanvas\|fakeAnimationHandle\|fakeDrawInterval" src/test-hooks/offscreen-hooks.ts` returns 0 (all canvas symbols excised).
|
|
- `grep -c "videoEl.captureStream\|fakeVideoEl\|HTMLVideoElement" src/test-hooks/offscreen-hooks.ts` returns ≥ 3 (replacement symbols present).
|
|
</action>
|
|
<verify>
|
|
<automated>mkdir -p tests/uat/fixtures && test -s tests/uat/fixtures/synthetic-display-source.webm && wc -c tests/uat/fixtures/synthetic-display-source.webm | awk '$1 > 1000000 {exit 0} {exit 1}' && npx tsc --noEmit && npm run build 2>&1 | tail -5 && npm run build:test 2>&1 | tail -5 && grep -c "synthetic-display-source" dist/ -r 2>/dev/null | grep -v ":0$" | wc -l | awk '$1 == 0 {exit 0} {exit 1}' && grep -c "canvas.captureStream" src/test-hooks/offscreen-hooks.ts | awk '$1 == 0 {exit 0} {exit 1}' && grep -c "videoEl.captureStream\|fakeVideoEl" src/test-hooks/offscreen-hooks.ts | awk '$1 >= 2 {exit 0} {exit 1}'</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `tests/uat/fixtures/synthetic-display-source.webm` exists, is VP9, >= 1 MB.
|
|
- `globals.d.ts` contains the `declare module '*.webm?url'` block.
|
|
- `src/test-hooks/offscreen-hooks.ts` imports `syntheticDisplaySourceUrl` via Vite `?url` suffix.
|
|
- `installFakeDisplayMedia()` is async; awaits videoEl `canplay` event + `videoEl.play()` before returning.
|
|
- `mintStream()` calls `videoEl.captureStream(30)` (NOT `canvas.captureStream`).
|
|
- `uninstallFakeDisplayMedia()` pauses + removes the video element (NOT canvas + RAF + interval cleanup).
|
|
- All 6 existing bridge ops (install-fake-display-media + dispatch-ended + has-stream + get-display-surface + get-segment-count + get-last-getDisplayMedia-constraints) remain functional; the install-fake-display-media op returns `true` (async dispatcher signal).
|
|
- `npx tsc --noEmit` exits 0.
|
|
- `npm run build` exits 0; production bundle has zero references to `synthetic-display-source` OR `videoEl.captureStream` OR `fakeVideoEl` (test-hooks tree-shake verified).
|
|
- `npm run build:test` exits 0; test bundle resolves the `?url` import.
|
|
- `grep -c "canvas.captureStream" src/test-hooks/offscreen-hooks.ts` returns 0.
|
|
- The displaySurface monkey-patch (`patchDisplaySurface(stream)` call) is preserved.
|
|
- The A23 `lastGetDisplayMediaConstraints` capture is preserved.
|
|
- The idempotent install/uninstall contract is preserved.
|
|
</acceptance_criteria>
|
|
<done>Methodology fix landed; bundled WebM source replaces canvas.captureStream throttling. Atomic commit: `feat(04-08): video-file-backed MediaStream — replace canvas.captureStream invisible-source throttling per debug session-2 verdict`.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Re-run spike-a33-sw-persistence.ts + land A33 harness assertion + orchestrator wiring</name>
|
|
<files>tests/uat/extension-page-harness.ts, tests/uat/lib/harness-page-driver.ts, tests/uat/harness.test.ts, tests/uat/spike-a33-sw-persistence.ts</files>
|
|
<read_first>tests/uat/spike-a33-sw-persistence.ts (full — re-run target), tests/uat/extension-page-harness.ts:3517-3700 (assertA30 — cs-injection-world precedent for reference; A33 does NOT use this pattern but reading clarifies the harness assertion shape), tests/uat/extension-page-harness.ts:3878-3917 (assertA31 — most-recent chrome.runtime.sendMessage SAVE_ARCHIVE pattern; copy this), tests/uat/extension-page-harness.ts:3932-4021 (__mokoshHarness global registration block — verify assertA2 is the canonical "prime fresh recording" entrypoint per Plan 04-04 REVISION iter-2 Option B), tests/uat/lib/harness-page-driver.ts:48-80 (stopServiceWorker helper — REUSED from Plan 04-04 3726eee), tests/uat/lib/harness-page-driver.ts:2039-2148 (driveA30 — host-side filter pattern for shape reference), tests/uat/lib/harness-page-driver.ts:1434-1480 (findLatestZip exported function — REUSED from Plan 04-04), tests/uat/harness.test.ts:100-110 (import block), tests/uat/harness.test.ts:340-360 (wrapped-driver block), tests/uat/harness.test.ts:459-487 (drivers-array push block; A32 is the most-recent entry), tests/uat/harness.test.ts:225-240 (SKIP_PROD_REBUILD env-gate pattern — analog for SKIP_LONG_UAT)</read_first>
|
|
<action>
|
|
**Step 0 — GATING CONDITION (Task 1 complete + methodology valid).**
|
|
- Verify Task 1 acceptance criteria all met. If any fail (TS error, missing fixture, etc.), STOP and fix before proceeding.
|
|
|
|
**Step 1 — Re-run the spike script.**
|
|
- Run: `HEADLESS=1 SKIP_PROD_REBUILD=0 npx tsx tests/uat/spike-a33-sw-persistence.ts 2>&1 | tee /tmp/04-08-spike-rerun.log`
|
|
- Total wall-clock: ~6-7 min (5 min idle + ~1-2 min orchestration).
|
|
- **Expected outcome:** `SPIKE OUTCOME: PASSED` with `videoSize > 100_000` (typical 1-3 MB).
|
|
- **GATING CONDITION for landing A33:** `grep -c 'SPIKE OUTCOME: PASSED' /tmp/04-08-spike-rerun.log` returns ≥ 1 AND extracted videoSize > 100_000.
|
|
- If the re-run still FAILS (videoSize ≤ 100_000): STOP execution; document failure in SUMMARY; flag for further debug session. This branch is unlikely per session-2 verdict (canvas throttling is the only identified root cause; HTMLVideoElement source bypasses it definitively per MDN + Chrome bug 653548 design doc) but the spike-first contract requires explicit acknowledgement of the FAIL branch.
|
|
|
|
**Step 2 — Update the spike script to use assertA2 prime as canonical (cleanup).**
|
|
- Per debug session-2 Step B addition, the spike already uses `__mokoshHarness.assertA2` for the prime step. No edit needed — but verify the script's PROBE 1 (POST-PRIME) returns count=0 AND PROBE 2 (PRE-KILL after 5min idle) returns count=3 AND PROBE 3 (POST-KILL) returns count=3. The PRE-KILL=3 vs the previous PRE-KILL=3-but-frames=0 distinguishes "segments existed AND have frames" (PASSING methodology) from "segments existed but zero frames" (canvas throttling). The videoSize > 100_000 outcome IS the methodology success indicator.
|
|
|
|
**Step 3 — Land A33 in the harness (3-file lockstep per Plan 04-04 Wave 1 spec verbatim).**
|
|
|
|
**File A: `tests/uat/extension-page-harness.ts`** — no edits needed (per Plan 04-04 REVISION iter-2 Option B). The `__mokoshHarness` surface stays at `assertA1..A31 + getManifestVersion`. SAVE_ARCHIVE dispatch happens inline in driveA33 via `page.evaluate` + `chrome.runtime.sendMessage`. The Task 2 read_first MUST verify this; if a different prime-recording method is more appropriate than assertA2, document in SUMMARY but DO NOT add new __mokoshHarness methods (FORBIDDEN_HOOK_STRINGS lockstep at 12 entries).
|
|
|
|
**File B: `tests/uat/lib/harness-page-driver.ts`** — append `driveA33` per the body in `<interfaces>` above.
|
|
- Place AFTER the existing `driveA32` function definition (the most-recent Phase 3 addition).
|
|
- Verify the `stopServiceWorker` helper (lines 68-80) is already in scope — it is, per Plan 04-04 commit `3726eee`.
|
|
- Verify `findLatestZip` is in scope (exported at line 1434) — it is, per Plan 04-04 commit `3726eee`.
|
|
- Verify `JSZip` + `readFileSync` are imported (lines 37 + 41) — they are.
|
|
- Type signature: `(page: Page, browser: Browser, extensionId: string, downloadsDir: string) => Promise<AssertionRecord>`.
|
|
- Filter-pipeline form; no `continue`; explicit named callbacks per project style.
|
|
|
|
**File C: `tests/uat/harness.test.ts`** — 3 sites edit:
|
|
|
|
(1) Import block at line 107 — add `driveA33,` after `driveA32,`:
|
|
```typescript
|
|
// Plan 04-08 — driveA33 SW state persistence (ROADMAP SC #1; methodology
|
|
// reframe per debug session-2 verdict; needs Browser + extensionId for
|
|
// CDP-based SW kill + downloadsDir for host-side JSZip parse).
|
|
driveA33,
|
|
```
|
|
|
|
(2) Wrapped-driver block after line 357 (after `driveA31Wrapped`):
|
|
```typescript
|
|
// Plan 04-08 — driveA33 needs Browser + extensionId for CDP-based SW kill
|
|
// AND downloadsDir for host-side JSZip parse of post-restart zip.
|
|
const driveA33Wrapped: (page: import('puppeteer').Page) => Promise<AssertionRecord> =
|
|
(page) => driveA33(page, handles.browser, handles.extensionId, handles.downloadsDir);
|
|
```
|
|
|
|
(3) Drivers-array push at line 487 (after the existing A32 entry):
|
|
```typescript
|
|
// Plan 04-08 A33: SW state persistence 5-min idle (ROADMAP SC #1).
|
|
// Methodology reframe per debug session-2 — video-file MediaStream
|
|
// replaces the canvas.captureStream invisible-source throttling that
|
|
// produced 8505-byte 0-frames archives under the previous Plan 04-04
|
|
// spike methodology. Architecture (offscreen-RAM segments: Blob[]) is
|
|
// unchanged and canonically correct per debug session-2 segment-count
|
|
// probe evidence (POST-KILL count=3 confirms structural persistence).
|
|
// Forces SW eviction via Puppeteer CDP worker.close() per the canonical
|
|
// Chrome devrel pattern (stopServiceWorker helper from Plan 04-04).
|
|
// Env-gated by SKIP_LONG_UAT for fast per-commit iteration; defaults
|
|
// to RUN for Phase 4 closure + alpha gate.
|
|
{
|
|
name: 'A33',
|
|
drive: process.env.SKIP_LONG_UAT === '1'
|
|
? async (): Promise<AssertionRecord> => ({
|
|
name: 'A33',
|
|
passed: true,
|
|
checks: [],
|
|
diagnostics: ['A33 SKIPPED (SKIP_LONG_UAT=1; unset to run 5-min idle test)'],
|
|
})
|
|
: driveA33Wrapped,
|
|
},
|
|
```
|
|
|
|
**Step 4 — Run the UAT harness verification gates.**
|
|
- `npx tsc --noEmit` exits 0.
|
|
- `npm run build:test` exits 0.
|
|
- **Skip-mode UAT (fast preserve):** `HEADLESS=1 SKIP_PROD_REBUILD=1 SKIP_LONG_UAT=1 npm run test:uat 2>&1 | tail -5 | tee /tmp/04-08-uat-skip.log` — expect `34/34` GREEN in ~95s; A33 SKIPPED placeholder counts as PASS.
|
|
- **Full-mode UAT (closure gate):** `HEADLESS=1 SKIP_PROD_REBUILD=1 npm run test:uat 2>&1 | tail -5 | tee /tmp/04-08-uat-full.log` — expect `34/34` GREEN in ~6.5 min; A33 actually runs and passes A33.1 + A33.2 + A33.3.
|
|
|
|
**Step 5 — Lockstep + bundle-gate invariant verification.**
|
|
- Tier-1 FORBIDDEN_HOOK_STRINGS inventory unchanged at 12 — verify by `wc -l` of both arrays returns the same count pre/post-edit:
|
|
```bash
|
|
# tests/uat/harness.test.ts:123-138 = 12 entries
|
|
# tests/background/no-test-hooks-in-prod-bundle.test.ts:108-... = 12 entries (matching)
|
|
grep -c "^[[:space:]]*'" tests/uat/harness.test.ts | head -1 # context-sensitive; check post-edit
|
|
```
|
|
Confirm zero new entries added (no new __MOKOSH_UAT__-gated symbols).
|
|
- Pre-checkpoint bundle gates (per `feedback-pre-checkpoint-bundle-gates.md`) — 6/6 PASS unchanged from Plan 04-03 baseline:
|
|
- `grep -c 'new Function' dist/assets/index-*.js` returns 0 (Plan 04-02 polarity preserved)
|
|
- `grep -c 'eval' dist/assets/index-*.js` returns 0
|
|
- `grep -c 'Buffer\.' dist/assets/index-*.js` returns 1 (pre-existing JSZip polyfill; Plan 04-02 deferred)
|
|
- `grep -c 'window\.' dist/assets/index-*.js` returns 0 (SW chunk DOM-globals)
|
|
- `grep -c 'document\.' dist/assets/index-*.js` returns 0
|
|
- Manifest validation + en/ru parity preserved (unchanged from Plan 04-03)
|
|
- vitest baseline preserved: `npm test 2>&1 | tail -5` — expect 183/183 GREEN (or 180/183 with the 3 documented pre-existing parallel-vitest flakes per 04-CONTEXT items 9-10; flakes pass in isolation).
|
|
|
|
**Step 6 — Write Plan 04-08 SUMMARY.**
|
|
- Create `.planning/phases/04-harden-clean-up-optional/04-08-SUMMARY.md`:
|
|
- Methodology reframe rationale (cite debug session-2 verdict verbatim)
|
|
- Bundled WebM fixture decision (copy vs regenerate; size + codec evidence)
|
|
- installFakeDisplayMedia diff summary (canvas -> HTMLVideoElement; 6 bridge ops preserved; A23 capture preserved)
|
|
- Spike re-run evidence (videoSize before/after; segment-count probe values; elapsed time)
|
|
- A33 land evidence (driveA33 + orchestrator wiring; FORBIDDEN_HOOK_STRINGS at 12; pre-checkpoint gates 6/6)
|
|
- UAT before/after (33/33 -> 34/34 GREEN)
|
|
- ROADMAP SC #1 closure with commit ref
|
|
- Plan 04-04 SUMMARY post-debug amendment cross-reference
|
|
- Persisting artifacts (stopServiceWorker + spike script + canonical methodology pattern)
|
|
- Architectural integrity statement (offscreen-RAM segments: Blob[] is UNCHANGED and canonically correct)
|
|
|
|
**Step 7 — Update STATE.md + ROADMAP.md markers (Phase 4 partial closure).**
|
|
- STATE.md: append to "Decisions" section: `[Phase 04-08]: Methodology reframe complete — video-file MediaStream replaces canvas.captureStream throttling per debug session-2 verdict; A33 harness assertion landed; UAT 33 -> 34 GREEN; ROADMAP SC #1 (SW state persistence) CLOSED.`
|
|
- ROADMAP.md: flip Phase 4 SC #1 row from "STATUS 2026-05-21: OPEN" to "STATUS 2026-05-22: CLOSED via Plan 04-08 — see .planning/phases/04-harden-clean-up-optional/04-08-SUMMARY.md". Add Plan 04-08 row to the Plans list with brief objective.
|
|
|
|
**Step 8 — Atomic commit.**
|
|
- Single atomic commit with all Task 2 + Task 3 artifacts: `feat(04-08): A33 SW state persistence harness assertion — methodology reframe (34/34 GREEN; ROADMAP SC #1 CLOSED)`.
|
|
</action>
|
|
<verify>
|
|
<automated>npx tsc --noEmit && npm run build:test && HEADLESS=1 SKIP_PROD_REBUILD=1 SKIP_LONG_UAT=1 npm run test:uat 2>&1 | tail -5 | tee /tmp/04-08-uat-skip.log; grep -c '34/34\|34 passed' /tmp/04-08-uat-skip.log | awk '$1 >= 1 {exit 0} {exit 1}' && grep -c 'driveA33' tests/uat/harness.test.ts | awk '$1 >= 3 {exit 0} {exit 1}' && grep -c 'SKIP_LONG_UAT' tests/uat/harness.test.ts | awk '$1 >= 2 {exit 0} {exit 1}' && grep -c 'dispatchSaveArchive' tests/uat/lib/harness-page-driver.ts tests/uat/extension-page-harness.ts tests/uat/harness.test.ts 2>/dev/null | grep -v ":0$" | wc -l | awk '$1 == 0 {exit 0} {exit 1}'</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- Spike re-run output contains `SPIKE OUTCOME: PASSED` AND `videoSize > 100_000`. (If FAILED branch fires, STOP and document; A33 land is BLOCKED.)
|
|
- `npx tsc --noEmit` exits 0.
|
|
- `npm run build:test` exits 0.
|
|
- `tests/uat/lib/harness-page-driver.ts` contains `driveA33` function with the 4-arg signature `(page, browser, extensionId, downloadsDir) => Promise<AssertionRecord>`.
|
|
- `tests/uat/lib/harness-page-driver.ts` driveA33 dispatches SAVE_ARCHIVE inline via `chrome.runtime.sendMessage({type: 'SAVE_ARCHIVE'}, ...)` — verify `grep -c "type: 'SAVE_ARCHIVE'" tests/uat/lib/harness-page-driver.ts` returns ≥ 1.
|
|
- `tests/uat/harness.test.ts` imports `driveA33` (1 line; grep -c returns ≥ 4 including comment).
|
|
- `tests/uat/harness.test.ts` defines `driveA33Wrapped` const.
|
|
- `tests/uat/harness.test.ts` drivers-array contains an `{ name: 'A33', drive: ... }` entry with `SKIP_LONG_UAT` env-gate.
|
|
- Skip-mode UAT: `HEADLESS=1 SKIP_PROD_REBUILD=1 SKIP_LONG_UAT=1 npm run test:uat` reports 34/34 GREEN in ~95s.
|
|
- Full-mode UAT: `HEADLESS=1 SKIP_PROD_REBUILD=1 npm run test:uat` reports 34/34 GREEN in ~6.5 min; A33.1 + A33.2 + A33.3 all PASS.
|
|
- `dispatchSaveArchive` symbol is NOT introduced anywhere — `grep -c 'dispatchSaveArchive' tests/uat/` returns 0.
|
|
- FORBIDDEN_HOOK_STRINGS inventory unchanged at 12 entries — verify by line count of both arrays.
|
|
- Pre-checkpoint bundle gates 6/6 PASS unchanged from Plan 04-03 baseline.
|
|
- vitest 183 baseline preserved (or 180/183 with the 3 documented pre-existing flakes; pass in isolation).
|
|
- `.planning/phases/04-harden-clean-up-optional/04-08-SUMMARY.md` exists with: methodology rationale + spike re-run evidence + A33 land evidence + UAT before/after + ROADMAP SC #1 closure + cross-reference to Plan 04-04 post-debug amendment.
|
|
- `.planning/STATE.md` Decisions list contains a Plan 04-08 entry citing ROADMAP SC #1 CLOSED.
|
|
- `.planning/ROADMAP.md` Phase 4 SC #1 row flipped from OPEN to CLOSED with date + Plan 04-08 cite.
|
|
</acceptance_criteria>
|
|
<done>A33 lands; UAT 33->34 GREEN; ROADMAP SC #1 CLOSED. Atomic commit: `feat(04-08): A33 SW state persistence harness assertion — methodology reframe (34/34 GREEN; ROADMAP SC #1 CLOSED)`.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<threat_model>
|
|
## Trust Boundaries
|
|
|
|
| Boundary | Description |
|
|
|----------|-------------|
|
|
| Bundled WebM fixture -> offscreen test bundle | the `tests/uat/fixtures/synthetic-display-source.webm` is project-owned (CC0-equivalent internal capture) and loaded via Vite's `?url` asset pipeline only in test builds. Production builds tree-shake the entire offscreen-hooks module per `__MOKOSH_UAT__`; the fixture has zero attack surface in production. |
|
|
| HTMLVideoElement.captureStream -> MediaRecorder | the video element source is bundled at build time (not network-fetched); no remote-origin attack vector. The captured stream feeds the production MediaRecorder code path verbatim — same boundary as canvas.captureStream had. |
|
|
| Puppeteer CDP -> Chrome MV3 SW | unchanged from Plan 04-04 (stopServiceWorker helper reused verbatim). |
|
|
|
|
## STRIDE Threat Register
|
|
|
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
|
|-----------|----------|-----------|-------------|-----------------|
|
|
| T-04-08-01 | Tampering | future change to `installFakeDisplayMedia()` reintroduces canvas.captureStream silently | mitigate | `grep -c "canvas.captureStream" src/test-hooks/offscreen-hooks.ts` invariant = 0 codified in Task 1 verify. A33 acts as a regression-catching gate; if a future PR moves source back to canvas, the spike fails fast (videoSize=8505). |
|
|
| T-04-08-02 | Tampering | bundled `synthetic-display-source.webm` is renamed/deleted/replaced with a tiny or invalid WebM | mitigate | Task 1 verify checks `wc -c` >= 1_000_000 bytes; Task 2 spike re-run catches any content-level corruption (videoSize-floor gate). |
|
|
| T-04-08-03 | Information disclosure | the bundled WebM contains operator content (it's a project-internal capture of the developer's screen) | accept | The existing `tests/fixtures/last_30sec.webm` has been in the repo since 2026-05-15 (Plan 01-07 closure); the new copy at `tests/uat/fixtures/synthetic-display-source.webm` is the same project-owned artifact. Tradeoff: rebuilding a fresh WebM from `ffmpeg -f lavfi -i testsrc` is OPTIONAL per Task 1 Step 1. If the developer prefers content-free fixtures, the regeneration path is the documented escape hatch. |
|
|
| T-04-08-04 | DoS (test runtime) | A33's 5-min idle adds ~5 min to harness wall-clock (95s -> 395s); per-commit CI lanes would suffer | mitigate | Env-gated by `SKIP_LONG_UAT=1` (default RUN for closure + alpha; documented per-commit fast-skip path). Same gate Plan 04-04 specified. |
|
|
| T-04-08-05 | Repudiation | HTMLVideoElement.captureStream is non-standard (not in lib.dom.d.ts at all TS versions); a TS strict-mode bump could break the cast-through-unknown idiom | accept | The cast is pinned in the code at Task 1 Step 3; project's TS pin is documented; if a TS upgrade ever breaks the cast, the build-test gate (Task 1 verify) catches it. The runtime surface itself (HTMLMediaElement.captureStream) is implemented in Chrome since 2017 and stable per MDN. |
|
|
| T-04-08-06 | Spoofing | a malicious replacement `synthetic-display-source.webm` could inject malicious VP9 codec data | accept | the fixture is committed to git; any tamper is visible via `git diff`. Plan 04-08's threat surface is test-only and the test build runs in a sandboxed Puppeteer browser instance. No production user is exposed. |
|
|
</threat_model>
|
|
|
|
<verification>
|
|
- `npx tsc --noEmit` exits 0.
|
|
- `npm run build` exits 0 (production; test-hooks tree-shake verified).
|
|
- `npm run build:test` exits 0 (test; ?url import resolves).
|
|
- `wc -c tests/uat/fixtures/synthetic-display-source.webm` reports >= 1_000_000 bytes; codec=vp9 per ffprobe.
|
|
- `grep -c "canvas.captureStream" src/test-hooks/offscreen-hooks.ts` returns 0.
|
|
- `grep -c "videoEl.captureStream\|fakeVideoEl" src/test-hooks/offscreen-hooks.ts` returns >= 2.
|
|
- `grep -c "synthetic-display-source" dist/ -r` returns 0 (production tree-shake).
|
|
- Spike re-run: `HEADLESS=1 npx tsx tests/uat/spike-a33-sw-persistence.ts` exits 0 with `videoSize > 100_000` (typical 1-3 MB).
|
|
- UAT harness count 33 -> 34.
|
|
- Skip-mode UAT: `HEADLESS=1 SKIP_PROD_REBUILD=1 SKIP_LONG_UAT=1 npm run test:uat` GREEN 34/34 in ~95s.
|
|
- Full-mode UAT: `HEADLESS=1 SKIP_PROD_REBUILD=1 npm run test:uat` GREEN 34/34 in ~6.5 min.
|
|
- ROADMAP SC #1 row flipped from OPEN to CLOSED (Plan 04-08 cite + 2026-05-22 date).
|
|
- Tier-1 FORBIDDEN_HOOK_STRINGS lockstep at 12 entries (unchanged).
|
|
- Pre-checkpoint bundle gates 6/6 PASS (new Function=0 + eval=0 + Buffer.=1 + window.=0 + document.=0 in SW + Tier-1=12 + en/ru parity).
|
|
- vitest baseline 183 preserved (or 180/183 with 3 documented pre-existing flakes; pass in isolation).
|
|
- A29 + A30 + A31 + A32 unchanged (no regression to existing assertions).
|
|
- Architecture invariant preserved: `src/offscreen/recorder.ts:91 let segments: Blob[] = []` UNCHANGED — debug session-2 verdict is honored (NO IndexedDB persistence work).
|
|
- spike-FAILED forensic-evidence preservation: `tests/uat/spike-a33-sw-persistence.ts` retained (Plan 04-04 commit 3726eee + debug session-2 Step B + C additions); now functions as a PASSING regression test under the new methodology.
|
|
- `dispatchSaveArchive` symbol NOT introduced anywhere — `grep -c` across spike script + harness files returns 0.
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- Bundled WebM fixture lands at `tests/uat/fixtures/synthetic-display-source.webm` (>=1 MB VP9; project-owned).
|
|
- `installFakeDisplayMedia()` rewritten — canvas.captureStream replaced by HTMLVideoElement.captureStream; displaySurface monkey-patch + A23 capture + idempotency contract preserved.
|
|
- Ambient module decl for `*.webm?url` added to `globals.d.ts`.
|
|
- Spike re-run produces `videoSize > 100_000` (typical 1-3 MB) — methodology reframe empirically validated.
|
|
- A33 harness assertion lands per Plan 04-04 Pattern 4 verbatim: driveA33 + driveA33Wrapped + orchestrator entry + SKIP_LONG_UAT env-gate.
|
|
- UAT harness count 33 -> 34 GREEN (skip-mode in ~95s; full-mode in ~6.5 min).
|
|
- ROADMAP SC #1 (SW state persistence) flipped CLOSED in ROADMAP.md.
|
|
- Plan 04-04 SUMMARY post-debug amendment cross-referenced in Plan 04-08 SUMMARY.
|
|
- Tier-1 FORBIDDEN_HOOK_STRINGS inventory unchanged at 12 entries.
|
|
- Pre-checkpoint bundle gates 6/6 PASS.
|
|
- Architectural integrity preserved (no IndexedDB persistence; no chrome.storage migration; src/offscreen/recorder.ts:91 segments array unchanged).
|
|
- vitest 183 baseline preserved.
|
|
- Plan 04-04 forensic artifacts (`stopServiceWorker` helper + spike script) repurposed as PASSING regression tests under valid methodology.
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/04-harden-clean-up-optional/04-08-SUMMARY.md` capturing:
|
|
|
|
- **Methodology reframe rationale** — verbatim cite of debug session-2 verdict (REFUTED-architecture; canvas-captureStream issue); explicit rejection of the previously-proposed IndexedDB persistence plan-fix with the empirical reasoning (the spike would STILL produce 8505 bytes after IDB lands because the failure is in the fake stream, not in segment persistence).
|
|
- **Bundled WebM fixture decision** — copy from `tests/fixtures/last_30sec.webm` vs regenerate via ffmpeg; size + codec + duration evidence; license/provenance attestation (CC0-equivalent project-owned internal capture).
|
|
- **installFakeDisplayMedia diff summary** — before (canvas.captureStream + RAF + setInterval) vs after (HTMLVideoElement + canplay event + .play() + captureStream); preserved invariants (displaySurface monkey-patch, A23 capture, idempotency, 6 bridge ops).
|
|
- **Spike re-run evidence** — videoSize before (8505) vs after (>100_000; typical 1-3 MB); segment-count probe values (POST-PRIME=0, PRE-KILL=3, POST-KILL=3); elapsed time (~308s baseline preserved); SPIKE OUTCOME: PASSED.
|
|
- **A33 land evidence** — driveA33 (4-arg signature); 3-file lockstep diff (extension-page-harness.ts unchanged per Option B; harness-page-driver.ts +driveA33; harness.test.ts +import +wrapped +array push); FORBIDDEN_HOOK_STRINGS at 12 entries (unchanged); pre-checkpoint gates 6/6 PASS.
|
|
- **UAT before/after** — 33/33 -> 34/34 GREEN; skip-mode wall-clock ~95s; full-mode wall-clock ~6.5 min.
|
|
- **ROADMAP SC #1 closure** — flipped OPEN -> CLOSED with Plan 04-08 cite + 2026-05-22 date.
|
|
- **Plan 04-04 cross-reference** — debug session-2 verdict honored; spike-FAILED forensic artifacts (`stopServiceWorker` helper + `spike-a33-sw-persistence.ts`) repurposed as PASSING regression tests.
|
|
- **Architectural integrity statement** — `let segments: Blob[] = []` at src/offscreen/recorder.ts:91 is UNCHANGED and canonically correct per debug session-2 segment-count probe evidence.
|
|
- **Commit refs** — Task 1 atomic commit (methodology fix) + Task 2 atomic commit (A33 land + SUMMARY + STATE/ROADMAP markers).
|
|
- **Next plan handoff** — Plan 04-07 (closure aggregator) can now reference ROADMAP SC #1 as GREEN; v1 milestone close prep unblocked from the SW persistence side.
|
|
</output>
|