From 1baaf45702b32aae195f588f4305f7aaad05f0f5 Mon Sep 17 00:00:00 2001 From: Mark Date: Tue, 19 May 2026 17:33:53 +0200 Subject: [PATCH] =?UTF-8?q?feat(01-13-A14-invert):=20A14=20=E2=80=94=20inv?= =?UTF-8?q?ert=20to=20assert=20continuous-recording=20post-SAVE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan 01-09 Amendment 3 (2026-05-19) end-to-end lock. Inverted A14 to match the reversed charter (SAVE creates zip, recording continues). Page-side (tests/uat/extension-page-harness.ts): - assertA14: assert badge==='REC' (was ''), popup endsWith 'src/popup/index.html' (was ''), no-new-recovery-notif (unchanged). - A14 name + check labels updated to reflect continuous-recording semantic. - New constant A14_POPUP_HTML_SUFFIX for the popup endsWith check (ext-id-agnostic via suffix match). - A13 docstring + diag strings refreshed: setupFreshRecording is now defensive (orthogonal to A12 ordering) rather than a workaround for the prior auto-stop. 11s settle preserved (same wall-clock cost). Host-side (tests/uat/lib/harness-page-driver.ts): - driveA14 docstring refreshed to mention Amendment 3 + the inverted contract; mechanical wrapper unchanged. Verification: - npm run test:uat: 15/15 GREEN - A14 actual output: badge='REC' popup='chrome-extension:///src/popup/index.html' recoveryDelta=0 Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/uat/extension-page-harness.ts | 137 +++++++++++++-------------- tests/uat/lib/harness-page-driver.ts | 17 ++-- 2 files changed, 78 insertions(+), 76 deletions(-) diff --git a/tests/uat/extension-page-harness.ts b/tests/uat/extension-page-harness.ts index 88a15b9..bd6c623 100644 --- a/tests/uat/extension-page-harness.ts +++ b/tests/uat/extension-page-harness.ts @@ -1700,17 +1700,16 @@ async function assertA12(): Promise { * helper to read `extensionVersion`, since it's the actual production * field per src/background/index.ts:572). * - * Plan 01-13 Task 9 (debug session 01-09-save-stops-recording) amendment: - * after the SAVE-auto-stops-recording fix in src/background/index.ts, - * A12's SAVE_ARCHIVE now stops the recording (per SPEC one-shot intent). - * A13's own SAVE_ARCHIVE therefore needs a FRESH recording — without it, - * A13 would dispatch against an empty buffer and saveArchive would - * return success=false (the EmptyVideoBufferError path). A13 thus - * does its own setupFreshRecording + segment-settle before dispatching. - * Trade-off: adds ~11s wall-clock to the harness (segment-settle for - * the first rotation) — acceptable; the SPEC SAVE=stop contract is the - * load-bearing requirement, A13 was designed under the old "save keeps - * recording" assumption. + * Plan 01-09 Amendment 3 (2026-05-19, debug session + * 01-09-save-does-not-stop-recording) update: the SAVE_ARCHIVE auto-stop + * was REVERSED. Under the new charter SAVE does NOT stop the recorder, + * so A12's SAVE leaves the recording live and A13 could in principle + * SAVE directly against A12's still-buffered segments. We KEEP the + * setupFreshRecording + 11s settle here as a defensive guarantee that + * A13 sees a known-good fresh-rotation buffer regardless of upstream + * assertion ordering (A12 itself might fail mid-flight or be reordered + * by a future maintainer; this isolation keeps A13's contract orthogonal). + * The 11s wall-clock cost is preserved — same as before Amendment 3. * * @returns Structured result with checks (SETUP + A13.1). */ @@ -1723,13 +1722,14 @@ async function assertA13(): Promise { }; try { - // Plan 01-13 Task 9 amendment: A12's SAVE_ARCHIVE now auto-stops - // recording (per the SAVE=stop SPEC contract). A13 needs to - // re-establish a fresh recording + wait for the first segment - // rotation before dispatching its own SAVE — otherwise the - // SW's saveArchive throws EmptyVideoBufferError on an empty - // segments buffer and returns success=false. - diag(result, 'Step 1: setupFreshRecording (A12 SAVE stopped recording per 01-09 fix)'); + // Plan 01-09 Amendment 3 (2026-05-19): SAVE_ARCHIVE no longer + // auto-stops recording (charter reversed). A13 still re-establishes + // a fresh recording as a defensive guarantee — see function-level + // docstring above for rationale. Without the setupFreshRecording + + // settle, if A13 were ever reordered ahead of A11 or run in + // isolation against a buffer with stale/missing segments, + // saveArchive would throw EmptyVideoBufferError. + diag(result, 'Step 1: setupFreshRecording (defensive — guarantees fresh buffer for A13 SAVE)'); const setupResp = await setupFreshRecording(); if (!setupResp.ok) { throw new Error( @@ -1738,7 +1738,7 @@ async function assertA13(): Promise { } diag(result, 'Step 1 OK — fresh recording active'); result.checks.push({ - name: 'SETUP: fresh recording established (post-A12 SAVE auto-stop)', + name: 'SETUP: fresh recording established (defensive — orthogonal to A12 ordering)', expected: true, actual: true, passed: true, @@ -1781,50 +1781,57 @@ async function assertA13(): Promise { return result; } -/** Settle window for the SW state machine after SAVE_ARCHIVE completes - * — the post-save STOP_RECORDING + setIdleMode block runs synchronously - * in saveArchive's finally, but chrome.action.getBadgeText reads are - * cross-event-loop. 500ms provides comfortable headroom; the - * observed lag is typically a few ms. */ +/** Settle window for the SW state machine after SAVE_ARCHIVE completes. + * Under Amendment 3 (2026-05-19, charter reversed) saveArchive has no + * finally block — the state machine should remain in REC throughout. + * The 500ms settle is preserved for cross-event-loop quiescence of + * any in-flight chrome.action.* postings; the observed lag is + * typically a few ms. */ const A14_POST_SAVE_SETTLE_MS = 500; +/** Expected popup path suffix in REC mode. setRecordingMode pins the + * popup to POPUP_HTML_PATH ('src/popup/index.html'); chrome.action.getPopup + * resolves this to an absolute chrome-extension:/// URL. + * We assert via endsWith to stay ext-id-agnostic. */ +const A14_POPUP_HTML_SUFFIX = 'src/popup/index.html'; + /** - * A14 — post-SAVE auto-stop state check. Plan 01-13 Task 9 closure for - * the operator empirical UAT bug `.planning/debug/01-09-save-stops-recording.md`. + * A14 — post-SAVE continuous-recording state check. Plan 01-09 Amendment 3 + * (2026-05-19, debug session 01-09-save-does-not-stop-recording). + * INVERTED from the prior Amendment 2 contract which asserted post-SAVE + * IDLE; under the reversed charter the SW MUST remain in REC after SAVE. * * Verifies that A13's SAVE_ARCHIVE (and by extension every SAVE) leaves - * the SW state machine in IDLE: - * - badge text === '' (setBadgeState('OFF') side effect of setIdleMode) - * - popup === '' (chrome.action.setPopup('') side effect of setIdleMode; - * observed as the Chrome-resolved absolute URL ending in a path that - * is NOT 'src/popup/index.html' — under the IDLE contract Chrome - * returns an empty string when getPopup is called against a popup - * that was set to '') + * the SW state machine UNCHANGED — recording continues: + * - badge text === 'REC' (setRecordingMode from the earlier + * setupFreshRecording is still in effect; no setIdleMode was called) + * - popup endsWith 'src/popup/index.html' (setRecordingMode pinned it; + * getPopup resolves to chrome-extension:///src/popup/index.html) * - no NEW notification with the 'mokosh-recovery-' prefix surfaced * since A14 entered (delta-based — A7 left a recovery notification - * in the active set; the SAVE auto-stop must NOT add another one, - * per the "deliberate stop ≠ error" contract that mirrors Bug B) + * in the active set; SAVE must NOT add another one, per the + * "save is not an error" contract — same as the prior Amendment 2 + * contract, regression-preserved) * - * Pre-condition: A13 just completed (its SAVE_ARCHIVE auto-stopped the - * recording under the new fix; the state machine should already be in - * IDLE by the time A14 runs). + * Pre-condition: A13 just completed. A13 calls setupFreshRecording so + * the state machine entered REC for that assertion; under the REVERSED + * charter saveArchive does not transition out of REC, so the state + * remains REC when A14 reads it. * * Post-condition: no state change — A14 is read-only. * - * Per the spec instruction (alternative simpler design from the - * orchestrator prompt): we settle for badge='' + popup='' + no-NEW-recovery-notif - * as the A14 contract. Direct isRecording check is transitively verified - * via the absence of the REC badge — the production SW state machine + * Direct isRecording check is transitively verified via the presence of + * the REC badge — the production SW state machine * (src/background/index.ts:setRecordingMode/setIdleMode/setErrorMode) * pairs isRecording transitions with badge transitions atomically, so - * the badge serves as a reliable proxy. + * badge='REC' is a reliable proxy for isRecording=true. * * @returns Structured result with 3 checks (badge + popup + no-new-recovery-notif). */ async function assertA14(): Promise { const result: AssertionResult = { passed: false, - name: 'A14 — post-SAVE auto-stop state: badge=\'\' + popup=\'\' + no new mokosh-recovery-* notif', + name: 'A14 — post-SAVE continuous-recording: badge=\'REC\' + popup endsWith popup.html + no new mokosh-recovery-* notif', checks: [], diagnostics: [], }; @@ -1833,18 +1840,12 @@ async function assertA14(): Promise { // Snapshot the recovery-notification ids BEFORE the A14 settle. // A7 left at least one recovery-* in the active set; the delta we // care about is "did any NEW recovery surface since A13's SAVE - // completed". A13's SAVE finished a few ms ago (the orchestrator - // ran assertA13 → returned → host moved to A14); the post-save - // finally block in saveArchive already executed setIdleMode + - // STOP_RECORDING. But we cannot observe what A13 did atomically - // — the cleanest contract is to snapshot now, settle, and check - // for a DELTA only. The empty-buffer-error branch comment in - // src/background/index.ts notes that the recovery notification - // would briefly appear; in this assertion we verify that under - // the HAPPY (success-path) SAVE, no recovery notification - // surfaces. A13's setup dispatched a successful SAVE (against a - // fresh recording with >= 1 segment from the 11s settle), so the - // happy-path branch ran — no RECORDING_ERROR → no recovery notif. + // completed". Under the REVERSED charter A13's SAVE does NOT + // dispatch STOP_RECORDING/setIdleMode; the SW stays in REC. The + // empty-buffer-error branch is not exercised under A13's happy + // path (it ran setupFreshRecording + 11s settle so segments + // are non-empty), so no RECORDING_ERROR is broadcast and no + // recovery notification surfaces. diag(result, 'Step 1: snapshot mokosh-recovery-* notification ids (delta baseline)'); const idsBefore = await getActiveNotificationIds(); const recoveryIdsBefore = idsBefore.filter( @@ -1855,7 +1856,7 @@ async function assertA14(): Promise { `Step 1 result: ${recoveryIdsBefore.length} active recovery-prefix ids: ${JSON.stringify(recoveryIdsBefore)}`, ); - diag(result, `Step 2: settle ${A14_POST_SAVE_SETTLE_MS}ms for post-A13 state machine to land`); + diag(result, `Step 2: settle ${A14_POST_SAVE_SETTLE_MS}ms for post-A13 state machine to quiesce`); await new Promise((r) => setTimeout(r, A14_POST_SAVE_SETTLE_MS)); diag(result, 'Step 3: read post-SAVE state (badge + popup + recovery ids delta)'); @@ -1872,24 +1873,22 @@ async function assertA14(): Promise { ); result.checks.push({ - name: 'A14.1: badge text is \'\' after SAVE_ARCHIVE auto-stop (setIdleMode)', - expected: '', + name: 'A14.1: badge text is \'REC\' after SAVE_ARCHIVE (recording continues; setRecordingMode persists)', + expected: 'REC', actual: badge, - passed: badge === '', + passed: badge === 'REC', }); - // Chrome's chrome.action.getPopup() returns an absolute URL when the - // popup was set via a non-empty path. When setPopup was called with - // an empty string (the setIdleMode case), getPopup returns an empty - // string per the Chrome runtime API contract. Assert empty-string - // equality. + // chrome.action.getPopup() returns an absolute chrome-extension:// + // URL ending in the registered path (POPUP_HTML_PATH = 'src/popup/index.html'). + // Assert via endsWith to stay extension-id-agnostic. result.checks.push({ - name: 'A14.2: popup is \'\' after SAVE_ARCHIVE auto-stop (setIdleMode re-enables onClicked)', - expected: '', + name: `A14.2: popup endsWith '${A14_POPUP_HTML_SUFFIX}' after SAVE_ARCHIVE (setRecordingMode still pinned; onClicked stays inert)`, + expected: A14_POPUP_HTML_SUFFIX, actual: popup, - passed: popup === '', + passed: popup.endsWith(A14_POPUP_HTML_SUFFIX), }); result.checks.push({ - name: 'A14.3: NO new mokosh-recovery-* notification (deliberate stop != error)', + name: 'A14.3: NO new mokosh-recovery-* notification (save is not an error)', expected: 0, actual: recoveryDelta, passed: recoveryDelta === 0, diff --git a/tests/uat/lib/harness-page-driver.ts b/tests/uat/lib/harness-page-driver.ts index ac7795c..38c0f72 100644 --- a/tests/uat/lib/harness-page-driver.ts +++ b/tests/uat/lib/harness-page-driver.ts @@ -968,15 +968,18 @@ export async function getManifestVersion(page: Page): Promise { }) as string; } -/* ─── Plan 01-13 Task 9 — driveA14 ─────────────────────────────────── */ +/* ─── Plan 01-09 Amendment 3 — driveA14 (INVERTED 2026-05-19) ──────── */ /** - * Drive A14 (post-SAVE auto-stop state check). Plan 01-13 Task 9 closure - * for debug session 01-09-save-stops-recording. Standard page.evaluate - * wrapper — A14 is a read-only assertion of the SW state machine left - * by A13's SAVE_ARCHIVE: badge='', popup='', no new mokosh-recovery-* - * notification. All work happens page-side; host side just triggers + - * reads the result. + * Drive A14 (post-SAVE continuous-recording state check). Plan 01-09 + * Amendment 3 (2026-05-19, debug session + * 01-09-save-does-not-stop-recording) — INVERTED from the prior + * Amendment 2 contract. Standard page.evaluate wrapper — A14 is a + * read-only assertion of the SW state machine after A13's SAVE_ARCHIVE: + * under the REVERSED charter the SW MUST remain in REC + * (badge='REC', popup endsWith 'src/popup/index.html', no new + * mokosh-recovery-* notification). All work happens page-side; host + * side just triggers + reads the result. * * @param page - The harness page from `launchHarnessBrowser`. * @returns Structured AssertionRecord with 3 checks (badge + popup + no-new-recovery).