feat(01-13-A14-invert): A14 — invert to assert continuous-recording post-SAVE
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://<ext-id>/src/popup/index.html'
recoveryDelta=0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1700,17 +1700,16 @@ async function assertA12(): Promise<AssertionResult> {
|
|||||||
* helper to read `extensionVersion`, since it's the actual production
|
* helper to read `extensionVersion`, since it's the actual production
|
||||||
* field per src/background/index.ts:572).
|
* field per src/background/index.ts:572).
|
||||||
*
|
*
|
||||||
* Plan 01-13 Task 9 (debug session 01-09-save-stops-recording) amendment:
|
* Plan 01-09 Amendment 3 (2026-05-19, debug session
|
||||||
* after the SAVE-auto-stops-recording fix in src/background/index.ts,
|
* 01-09-save-does-not-stop-recording) update: the SAVE_ARCHIVE auto-stop
|
||||||
* A12's SAVE_ARCHIVE now stops the recording (per SPEC one-shot intent).
|
* was REVERSED. Under the new charter SAVE does NOT stop the recorder,
|
||||||
* A13's own SAVE_ARCHIVE therefore needs a FRESH recording — without it,
|
* so A12's SAVE leaves the recording live and A13 could in principle
|
||||||
* A13 would dispatch against an empty buffer and saveArchive would
|
* SAVE directly against A12's still-buffered segments. We KEEP the
|
||||||
* return success=false (the EmptyVideoBufferError path). A13 thus
|
* setupFreshRecording + 11s settle here as a defensive guarantee that
|
||||||
* does its own setupFreshRecording + segment-settle before dispatching.
|
* A13 sees a known-good fresh-rotation buffer regardless of upstream
|
||||||
* Trade-off: adds ~11s wall-clock to the harness (segment-settle for
|
* assertion ordering (A12 itself might fail mid-flight or be reordered
|
||||||
* the first rotation) — acceptable; the SPEC SAVE=stop contract is the
|
* by a future maintainer; this isolation keeps A13's contract orthogonal).
|
||||||
* load-bearing requirement, A13 was designed under the old "save keeps
|
* The 11s wall-clock cost is preserved — same as before Amendment 3.
|
||||||
* recording" assumption.
|
|
||||||
*
|
*
|
||||||
* @returns Structured result with checks (SETUP + A13.1).
|
* @returns Structured result with checks (SETUP + A13.1).
|
||||||
*/
|
*/
|
||||||
@@ -1723,13 +1722,14 @@ async function assertA13(): Promise<AssertionResult> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Plan 01-13 Task 9 amendment: A12's SAVE_ARCHIVE now auto-stops
|
// Plan 01-09 Amendment 3 (2026-05-19): SAVE_ARCHIVE no longer
|
||||||
// recording (per the SAVE=stop SPEC contract). A13 needs to
|
// auto-stops recording (charter reversed). A13 still re-establishes
|
||||||
// re-establish a fresh recording + wait for the first segment
|
// a fresh recording as a defensive guarantee — see function-level
|
||||||
// rotation before dispatching its own SAVE — otherwise the
|
// docstring above for rationale. Without the setupFreshRecording +
|
||||||
// SW's saveArchive throws EmptyVideoBufferError on an empty
|
// settle, if A13 were ever reordered ahead of A11 or run in
|
||||||
// segments buffer and returns success=false.
|
// isolation against a buffer with stale/missing segments,
|
||||||
diag(result, 'Step 1: setupFreshRecording (A12 SAVE stopped recording per 01-09 fix)');
|
// saveArchive would throw EmptyVideoBufferError.
|
||||||
|
diag(result, 'Step 1: setupFreshRecording (defensive — guarantees fresh buffer for A13 SAVE)');
|
||||||
const setupResp = await setupFreshRecording();
|
const setupResp = await setupFreshRecording();
|
||||||
if (!setupResp.ok) {
|
if (!setupResp.ok) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -1738,7 +1738,7 @@ async function assertA13(): Promise<AssertionResult> {
|
|||||||
}
|
}
|
||||||
diag(result, 'Step 1 OK — fresh recording active');
|
diag(result, 'Step 1 OK — fresh recording active');
|
||||||
result.checks.push({
|
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,
|
expected: true,
|
||||||
actual: true,
|
actual: true,
|
||||||
passed: true,
|
passed: true,
|
||||||
@@ -1781,50 +1781,57 @@ async function assertA13(): Promise<AssertionResult> {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Settle window for the SW state machine after SAVE_ARCHIVE completes
|
/** Settle window for the SW state machine after SAVE_ARCHIVE completes.
|
||||||
* — the post-save STOP_RECORDING + setIdleMode block runs synchronously
|
* Under Amendment 3 (2026-05-19, charter reversed) saveArchive has no
|
||||||
* in saveArchive's finally, but chrome.action.getBadgeText reads are
|
* finally block — the state machine should remain in REC throughout.
|
||||||
* cross-event-loop. 500ms provides comfortable headroom; the
|
* The 500ms settle is preserved for cross-event-loop quiescence of
|
||||||
* observed lag is typically a few ms. */
|
* any in-flight chrome.action.* postings; the observed lag is
|
||||||
|
* typically a few ms. */
|
||||||
const A14_POST_SAVE_SETTLE_MS = 500;
|
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://<ext-id>/<path> 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
|
* A14 — post-SAVE continuous-recording state check. Plan 01-09 Amendment 3
|
||||||
* the operator empirical UAT bug `.planning/debug/01-09-save-stops-recording.md`.
|
* (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
|
* Verifies that A13's SAVE_ARCHIVE (and by extension every SAVE) leaves
|
||||||
* the SW state machine in IDLE:
|
* the SW state machine UNCHANGED — recording continues:
|
||||||
* - badge text === '' (setBadgeState('OFF') side effect of setIdleMode)
|
* - badge text === 'REC' (setRecordingMode from the earlier
|
||||||
* - popup === '' (chrome.action.setPopup('') side effect of setIdleMode;
|
* setupFreshRecording is still in effect; no setIdleMode was called)
|
||||||
* observed as the Chrome-resolved absolute URL ending in a path that
|
* - popup endsWith 'src/popup/index.html' (setRecordingMode pinned it;
|
||||||
* is NOT 'src/popup/index.html' — under the IDLE contract Chrome
|
* getPopup resolves to chrome-extension://<ext-id>/src/popup/index.html)
|
||||||
* returns an empty string when getPopup is called against a popup
|
|
||||||
* that was set to '')
|
|
||||||
* - no NEW notification with the 'mokosh-recovery-' prefix surfaced
|
* - no NEW notification with the 'mokosh-recovery-' prefix surfaced
|
||||||
* since A14 entered (delta-based — A7 left a recovery notification
|
* since A14 entered (delta-based — A7 left a recovery notification
|
||||||
* in the active set; the SAVE auto-stop must NOT add another one,
|
* in the active set; SAVE must NOT add another one, per the
|
||||||
* per the "deliberate stop ≠ error" contract that mirrors Bug B)
|
* "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
|
* Pre-condition: A13 just completed. A13 calls setupFreshRecording so
|
||||||
* recording under the new fix; the state machine should already be in
|
* the state machine entered REC for that assertion; under the REVERSED
|
||||||
* IDLE by the time A14 runs).
|
* 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.
|
* Post-condition: no state change — A14 is read-only.
|
||||||
*
|
*
|
||||||
* Per the spec instruction (alternative simpler design from the
|
* Direct isRecording check is transitively verified via the presence of
|
||||||
* orchestrator prompt): we settle for badge='' + popup='' + no-NEW-recovery-notif
|
* the REC badge — the production SW state machine
|
||||||
* as the A14 contract. Direct isRecording check is transitively verified
|
|
||||||
* via the absence of the REC badge — the production SW state machine
|
|
||||||
* (src/background/index.ts:setRecordingMode/setIdleMode/setErrorMode)
|
* (src/background/index.ts:setRecordingMode/setIdleMode/setErrorMode)
|
||||||
* pairs isRecording transitions with badge transitions atomically, so
|
* 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).
|
* @returns Structured result with 3 checks (badge + popup + no-new-recovery-notif).
|
||||||
*/
|
*/
|
||||||
async function assertA14(): Promise<AssertionResult> {
|
async function assertA14(): Promise<AssertionResult> {
|
||||||
const result: AssertionResult = {
|
const result: AssertionResult = {
|
||||||
passed: false,
|
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: [],
|
checks: [],
|
||||||
diagnostics: [],
|
diagnostics: [],
|
||||||
};
|
};
|
||||||
@@ -1833,18 +1840,12 @@ async function assertA14(): Promise<AssertionResult> {
|
|||||||
// Snapshot the recovery-notification ids BEFORE the A14 settle.
|
// Snapshot the recovery-notification ids BEFORE the A14 settle.
|
||||||
// A7 left at least one recovery-* in the active set; the delta we
|
// 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
|
// care about is "did any NEW recovery surface since A13's SAVE
|
||||||
// completed". A13's SAVE finished a few ms ago (the orchestrator
|
// completed". Under the REVERSED charter A13's SAVE does NOT
|
||||||
// ran assertA13 → returned → host moved to A14); the post-save
|
// dispatch STOP_RECORDING/setIdleMode; the SW stays in REC. The
|
||||||
// finally block in saveArchive already executed setIdleMode +
|
// empty-buffer-error branch is not exercised under A13's happy
|
||||||
// STOP_RECORDING. But we cannot observe what A13 did atomically
|
// path (it ran setupFreshRecording + 11s settle so segments
|
||||||
// — the cleanest contract is to snapshot now, settle, and check
|
// are non-empty), so no RECORDING_ERROR is broadcast and no
|
||||||
// for a DELTA only. The empty-buffer-error branch comment in
|
// recovery notification surfaces.
|
||||||
// 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.
|
|
||||||
diag(result, 'Step 1: snapshot mokosh-recovery-* notification ids (delta baseline)');
|
diag(result, 'Step 1: snapshot mokosh-recovery-* notification ids (delta baseline)');
|
||||||
const idsBefore = await getActiveNotificationIds();
|
const idsBefore = await getActiveNotificationIds();
|
||||||
const recoveryIdsBefore = idsBefore.filter(
|
const recoveryIdsBefore = idsBefore.filter(
|
||||||
@@ -1855,7 +1856,7 @@ async function assertA14(): Promise<AssertionResult> {
|
|||||||
`Step 1 result: ${recoveryIdsBefore.length} active recovery-prefix ids: ${JSON.stringify(recoveryIdsBefore)}`,
|
`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));
|
await new Promise((r) => setTimeout(r, A14_POST_SAVE_SETTLE_MS));
|
||||||
|
|
||||||
diag(result, 'Step 3: read post-SAVE state (badge + popup + recovery ids delta)');
|
diag(result, 'Step 3: read post-SAVE state (badge + popup + recovery ids delta)');
|
||||||
@@ -1872,24 +1873,22 @@ async function assertA14(): Promise<AssertionResult> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
result.checks.push({
|
result.checks.push({
|
||||||
name: 'A14.1: badge text is \'\' after SAVE_ARCHIVE auto-stop (setIdleMode)',
|
name: 'A14.1: badge text is \'REC\' after SAVE_ARCHIVE (recording continues; setRecordingMode persists)',
|
||||||
expected: '',
|
expected: 'REC',
|
||||||
actual: badge,
|
actual: badge,
|
||||||
passed: badge === '',
|
passed: badge === 'REC',
|
||||||
});
|
});
|
||||||
// Chrome's chrome.action.getPopup() returns an absolute URL when the
|
// chrome.action.getPopup() returns an absolute chrome-extension://
|
||||||
// popup was set via a non-empty path. When setPopup was called with
|
// URL ending in the registered path (POPUP_HTML_PATH = 'src/popup/index.html').
|
||||||
// an empty string (the setIdleMode case), getPopup returns an empty
|
// Assert via endsWith to stay extension-id-agnostic.
|
||||||
// string per the Chrome runtime API contract. Assert empty-string
|
|
||||||
// equality.
|
|
||||||
result.checks.push({
|
result.checks.push({
|
||||||
name: 'A14.2: popup is \'\' after SAVE_ARCHIVE auto-stop (setIdleMode re-enables onClicked)',
|
name: `A14.2: popup endsWith '${A14_POPUP_HTML_SUFFIX}' after SAVE_ARCHIVE (setRecordingMode still pinned; onClicked stays inert)`,
|
||||||
expected: '',
|
expected: A14_POPUP_HTML_SUFFIX,
|
||||||
actual: popup,
|
actual: popup,
|
||||||
passed: popup === '',
|
passed: popup.endsWith(A14_POPUP_HTML_SUFFIX),
|
||||||
});
|
});
|
||||||
result.checks.push({
|
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,
|
expected: 0,
|
||||||
actual: recoveryDelta,
|
actual: recoveryDelta,
|
||||||
passed: recoveryDelta === 0,
|
passed: recoveryDelta === 0,
|
||||||
|
|||||||
@@ -968,15 +968,18 @@ export async function getManifestVersion(page: Page): Promise<string> {
|
|||||||
}) as string;
|
}) 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
|
* Drive A14 (post-SAVE continuous-recording state check). Plan 01-09
|
||||||
* for debug session 01-09-save-stops-recording. Standard page.evaluate
|
* Amendment 3 (2026-05-19, debug session
|
||||||
* wrapper — A14 is a read-only assertion of the SW state machine left
|
* 01-09-save-does-not-stop-recording) — INVERTED from the prior
|
||||||
* by A13's SAVE_ARCHIVE: badge='', popup='', no new mokosh-recovery-*
|
* Amendment 2 contract. Standard page.evaluate wrapper — A14 is a
|
||||||
* notification. All work happens page-side; host side just triggers +
|
* read-only assertion of the SW state machine after A13's SAVE_ARCHIVE:
|
||||||
* reads the result.
|
* 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`.
|
* @param page - The harness page from `launchHarnessBrowser`.
|
||||||
* @returns Structured AssertionRecord with 3 checks (badge + popup + no-new-recovery).
|
* @returns Structured AssertionRecord with 3 checks (badge + popup + no-new-recovery).
|
||||||
|
|||||||
Reference in New Issue
Block a user