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:
@@ -8,13 +8,15 @@
|
||||
// Wave 3A scope — wires A0+A1+A2+A3+A4+A6 (A6 via the proven Wave-2
|
||||
// driver). A5+A7..A13 throw `NOT YET IMPLEMENTED — Wave 3<X> wires this`
|
||||
// from `tests/uat/lib/harness-page-driver.ts`; the bail-on-first-failure
|
||||
// loop stops at the first such throw. Expected Wave-3A diagnostic:
|
||||
// "UAT harness: 5/14 assertions passed (A0+A1+A2+A3+A4 GREEN; bail at A5)"
|
||||
// A6 PASSES via the standalone `npx tsx tests/uat/a6.test.ts` entry —
|
||||
// the orchestrator-level A6 won't reach in Wave 3A because the
|
||||
// sequential loop bails at A5; A6 lands in the loop output once Wave 3B
|
||||
// implements driveA5. The orchestrator structure is final from Wave 3A
|
||||
// onward; future waves only fill in the assertion-driver stubs.
|
||||
// loop stops at the first such throw.
|
||||
//
|
||||
// Wave 3B (this file's current state) wires A5 (SAVE_ARCHIVE → zip on
|
||||
// disk) + A7 (genuine RECORDING_ERROR → ERR + recovery notification).
|
||||
// Expected diagnostic: "8/14 GREEN: A0+A1+A2+A3+A4+A5+A6+A7; bail at A8".
|
||||
// Wave 3C will wire A8+A9+A10; Wave 3D wires A11+A12+A13 for 14/14 GREEN.
|
||||
//
|
||||
// The orchestrator structure is final from Wave 3A onward; future waves
|
||||
// only fill in the assertion-driver stubs.
|
||||
//
|
||||
// Architectural commitments (per 01-11-SUMMARY.md, DO NOT REGRESS):
|
||||
// - Single browser, single recording per run (state machine: idle →
|
||||
@@ -245,11 +247,27 @@ async function main(): Promise<number> {
|
||||
// recording), then dispatch-ended. After A6 the recording is torn
|
||||
// down — A7+ would need to re-start or test post-stop state.
|
||||
//
|
||||
// Wave 3A only A1..A4 wire to real impls; A5..A13 throw NOT YET
|
||||
// IMPLEMENTED. Bail-on-first-failure stops the loop at A5 — A6's
|
||||
// driver wires (via Wave 2's driveA6) but won't reach in this run.
|
||||
// Wave 3B wires A5 + A7 in addition to A1..A4 + A6 — bail-on-first-
|
||||
// failure stops at A8 (Wave 3C wires that). Expected diagnostic:
|
||||
// "8/14 GREEN: A0+A1+A2+A3+A4+A5+A6+A7; A8..A13 NOT YET IMPLEMENTED".
|
||||
// The standalone `npx tsx tests/uat/a6.test.ts` entry remains the
|
||||
// way to verify A6 in isolation during Wave 3A.
|
||||
// way to verify A6 in isolation for inner-loop iteration.
|
||||
process.stdout.write('Launching Chrome + opening harness page...\n');
|
||||
const handles = await launchHarnessBrowser();
|
||||
process.stdout.write(`Extension id: ${handles.extensionId}\n`);
|
||||
process.stdout.write(`Downloads dir: ${handles.downloadsDir}\n\n`);
|
||||
|
||||
// Adapter: driveA5 needs `downloadsDir` (host-side fs polling); driveA12 +
|
||||
// driveA13 return `AssertionWithBytes`. We wrap each in a closure that
|
||||
// hides those signature differences so the orchestrator's driver list
|
||||
// is uniform `Page -> Promise<AssertionRecord>`. The byte-returning
|
||||
// drivers' extra fields are out-of-scope for Wave 3B; Wave 3D will
|
||||
// extend the orchestrator to surface them when A12/A13 land. The driver
|
||||
// list is constructed AFTER `launchHarnessBrowser` returns so the
|
||||
// closure can capture `handles.downloadsDir` without a TDZ trap.
|
||||
const driveA5Wrapped: (page: import('puppeteer').Page) => Promise<AssertionRecord> =
|
||||
(page) => driveA5(page, handles.downloadsDir);
|
||||
|
||||
const drivers: ReadonlyArray<{
|
||||
readonly name: string;
|
||||
readonly drive: (page: import('puppeteer').Page) => Promise<AssertionRecord>;
|
||||
@@ -258,7 +276,7 @@ async function main(): Promise<number> {
|
||||
{ name: 'A2', drive: driveA2 },
|
||||
{ name: 'A3', drive: driveA3 },
|
||||
{ name: 'A4', drive: driveA4 },
|
||||
{ name: 'A5', drive: driveA5 as (page: import('puppeteer').Page) => Promise<AssertionRecord> },
|
||||
{ name: 'A5', drive: driveA5Wrapped },
|
||||
{ name: 'A6', drive: driveA6 },
|
||||
{ name: 'A7', drive: driveA7 },
|
||||
{ name: 'A8', drive: driveA8 },
|
||||
@@ -269,11 +287,6 @@ async function main(): Promise<number> {
|
||||
{ name: 'A13', drive: driveA13 as (page: import('puppeteer').Page) => Promise<AssertionRecord> },
|
||||
];
|
||||
|
||||
process.stdout.write('Launching Chrome + opening harness page...\n');
|
||||
const handles = await launchHarnessBrowser();
|
||||
process.stdout.write(`Extension id: ${handles.extensionId}\n`);
|
||||
process.stdout.write(`Downloads dir: ${handles.downloadsDir}\n\n`);
|
||||
|
||||
const buffers = { swConsole: handles.swConsole, offConsole: handles.offConsole };
|
||||
const results: Array<{ name: string; passed: boolean; error?: string }> = [];
|
||||
let bailReason: string | null = null;
|
||||
|
||||
Reference in New Issue
Block a user