feat(01-13): wave-3B — A5+A6+A7 GREEN + Bug B canonical regression rewind
Wave 3B lands the A5 (SAVE_ARCHIVE → zip on disk) and A7 (genuine
RECORDING_ERROR → ERR + recovery notification) assertions, completing
8/14 of the orchestrator's GREEN floor (A0+A1+A2+A3+A4+A5+A6+A7).
Bails at A8 (Wave 3C scope).
Changes per file:
tests/uat/extension-page-harness.ts
- assertA5: 11s settle (>= SEGMENT_DURATION_MS so first rotation
lands a segment) + send SAVE_ARCHIVE + assert resp.success=true.
Page-side only checks SW handler ack; host-side driver verifies
disk-side outcome (zip presence + size floor).
- assertA7: setupFreshRecording helper (A6 tears down; A7 needs
REC state) → snapshot notif count → send RECORDING_ERROR with
a non-Bug-B error code ('codec-unsupported') → 200ms settle →
assert badge='ERR' + popup endsWith popup.html + notif delta=1
+ set-membership for 'mokosh-recovery-*' prefix.
- setupFreshRecording: shared helper for A7 + future assertions
that need a fresh REC state after a teardown.
tests/uat/lib/harness-page-driver.ts
- driveA5: page.evaluate(assertA5) THEN host-side fs polling for
*.zip in handles.downloadsDir. The CDP Browser.setDownloadBehavior
override renames the file to download.zip (data: URL filename
gap), so we accept any *.zip suffix. Merges page-side check +
host-side checks into a single AssertionRecord. Signature now
takes downloadsDir as a second arg.
- driveA7: standard page.evaluate wrapper (no host-side work).
tests/uat/harness.test.ts
- Wraps driveA5 in a closure that captures handles.downloadsDir.
- Reordered: launchHarnessBrowser MUST run before driver list so
the closure can read handles without a TDZ trap.
tests/uat/lib/launch.ts
- Victim page switched from about:blank to a file:// URL backed by
a tmp HTML file in downloadsDir. About:blank breaks A5 because
chrome.tabs.captureVisibleTab needs <all_urls> permission which
matches http/https/file/ftp but NOT about: or data: URLs. The
stub HTML satisfies <all_urls> + provides a real .url for the
production saveArchive's chrome.tabs.query.
src/test-hooks/offscreen-hooks.ts (test-only — tree-shaken from prod)
- installFakeDisplayMedia: mintStream() helper called per
fakeGetDisplayMedia invocation; each call mints a FRESH
MediaStream from the persistent canvas. Real getDisplayMedia
returns a new stream per call — fake now matches. Required for
A7's setupFreshRecording where the previous recording's stream
tracks were stopped by A6's onUserStoppedSharing teardown.
- Added 33ms setInterval-driven drawFrame() alongside the
existing requestAnimationFrame loop. RAF can throttle in
headless Chrome on offscreen documents (page-visibility
heuristics produce 0 fps), which yields zero-byte
MediaRecorder segments that crash ts-ebml's VINT decode in
webm-remux.extractFramesFromSegment with "Unrepresentable
length: Infinity". The setInterval is redundant when RAF fires
at full rate; it's a safety net for the headless-MV3 corner.
Bug B regression-catch demo (success_criteria #3 — MANDATORY per plan):
Step 1 — apply local regression patch (NOT committed):
src/background/index.ts:792 setIdleMode() → setErrorMode()
Step 2 — npm run build:test && npm run test:uat RED snippet:
A6 — BUG B canonical: user-stopped-sharing routes via setIdleMode: FAIL
[PASS] SETUP: badge becomes REC after start
[FAIL] A6.1: badge text is '' (NOT 'ERR') after user-stop
expected: ""
actual: "ERR"
[FAIL] A6.2: popup is '' (NOT manifest default) after user-stop
expected: ""
actual: "chrome-extension://<id>/src/popup/index.html"
[PASS] A6.3: NO recovery notification fired (count delta === 0)
[PASS] A6.4: isRecording=false (via badge proxy)
UAT harness: 6/14 assertions passed (bailed: A6 failed; see above)
Step 3 — revert local patch (git checkout -- src/background/index.ts).
Step 4 — npm run build:test && npm run test:uat GREEN snippet:
A6 — BUG B canonical: user-stopped-sharing routes via setIdleMode: PASS
[PASS] SETUP: badge becomes REC after start
[PASS] A6.1: badge text is '' (NOT 'ERR') after user-stop
[PASS] A6.2: popup is '' (NOT manifest default) after user-stop
[PASS] A6.3: NO recovery notification fired (count delta === 0)
[PASS] A6.4: isRecording=false (via badge proxy)
UAT harness: 8/14 assertions passed (bailed: A8 failed — NOT YET
IMPLEMENTED — Wave 3C wires driveA8)
The harness CORRECTLY catches the Bug B regression — the canonical
debug 01-09-recovery-flow scenario (operator-initiated stop routed
through setErrorMode locks the operator out of restart because popup
stays pinned to SAVE-only mode). Bug B is now CI-callable end-to-end.
vitest 93/93 GREEN throughout (unit-test layer unaffected). Tier-1
grep gate GREEN (9 forbidden hook strings: 0 occurrences in dist/).
npm run build exit 0; npx tsc --noEmit exit 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -47,10 +47,10 @@
|
||||
// - Node fs.mkdtempSync:
|
||||
// https://nodejs.org/api/fs.html#fsmkdtempsyncprefix-options
|
||||
|
||||
import { existsSync, mkdtempSync, statSync } from 'node:fs';
|
||||
import { existsSync, mkdtempSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join, resolve as resolvePath } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
import puppeteer, { type Browser, type Page } from 'puppeteer';
|
||||
|
||||
@@ -318,11 +318,43 @@ export async function launchHarnessBrowser(
|
||||
const offConsole: string[] = [];
|
||||
|
||||
// Open the victim page FIRST so it's already in the tab list when
|
||||
// the harness page opens. About:blank's `tab.url` resolves to
|
||||
// 'about:blank' (truthy), passing production
|
||||
// chrome.tabs.query({active:true}) presence checks.
|
||||
// the harness page opens. The victim URL must satisfy two constraints:
|
||||
// 1. `chrome.tabs.query({active:true,currentWindow:true})` returns a
|
||||
// tab with non-empty `.id` (so production `saveArchive` proceeds);
|
||||
// every real tab does, so this is automatic.
|
||||
// 2. `chrome.tabs.captureVisibleTab(...)` (used inside saveArchive's
|
||||
// screenshot capture path) succeeds. captureVisibleTab needs
|
||||
// EITHER `<all_urls>` host permission to match the active tab's
|
||||
// URL OR `activeTab` granted (which only fires on a real user
|
||||
// gesture, not a Puppeteer-scripted call). `<all_urls>` matches
|
||||
// `http://`, `https://`, `file://`, `ftp://` — but NOT
|
||||
// `about:blank` and NOT `data:` URLs (Chromium treats `data:`
|
||||
// with an opaque origin and rejects the capture under
|
||||
// "activeTab permission not in effect"). The canonical scheme
|
||||
// that DOES satisfy `<all_urls>` without needing a localhost
|
||||
// server is `file://`, so we write a stub HTML file inside the
|
||||
// per-run `downloadsDir` (already mkdtempSync'd above) and load
|
||||
// it as `file://<absPath>`.
|
||||
//
|
||||
// Plan 01-13 Wave 3B deviation (Rule 3 — blocking issue): the original
|
||||
// `about:blank` victim page (carried over from the c647f61 prototype
|
||||
// because A6 does not exercise captureVisibleTab) breaks A5's
|
||||
// SAVE_ARCHIVE round-trip. The file:// victim is the simplest fix
|
||||
// that does not touch production code or add network dependencies
|
||||
// (an http://localhost server would also work but introduces port-
|
||||
// allocation race conditions on CI).
|
||||
const victimHtmlPath = resolvePath(downloadsDir, 'mokosh-uat-victim.html');
|
||||
const victimHtml =
|
||||
'<!doctype html><html><head><meta charset="utf-8">' +
|
||||
'<title>Mokosh UAT Victim</title></head>' +
|
||||
'<body><h1>UAT victim page</h1>' +
|
||||
'<p>This tab exists so chrome.tabs.captureVisibleTab succeeds during A5 (SAVE_ARCHIVE).</p>' +
|
||||
'</body></html>';
|
||||
writeFileSync(victimHtmlPath, victimHtml, 'utf8');
|
||||
const victimUrl = pathToFileURL(victimHtmlPath).toString();
|
||||
|
||||
const victimPage = await browser.newPage();
|
||||
await victimPage.goto('about:blank');
|
||||
await victimPage.goto(victimUrl);
|
||||
|
||||
// Open the harness page; attach console + pageerror listeners
|
||||
// BEFORE the goto so we don't miss bootstrap-time messages.
|
||||
|
||||
Reference in New Issue
Block a user