feat(01-09-no-stop): GREEN — remove SAVE_ARCHIVE finally block; recording continues

Plan 01-09 Amendment 3 (2026-05-19) — code surgery to drive
tests/background/save-archive-does-not-stop-recording.test.ts from
RED (commit 6ac23fd) to GREEN.

Surgery: removed the entire `finally` block from saveArchive() in
src/background/index.ts (introduced by Amendment 2 commit 4f4c3e2).
SAVE_ARCHIVE now returns to its original semantics:
  create zip → download → done. No state transitions.

Updated the function-level docstring to reflect the new charter +
point at the regression-locking test file and harness A14 assertion.

Verification:
- save-archive-does-not-stop-recording.test.ts: 4/4 GREEN
- Full vitest baseline: 98/98 GREEN (SKIP_BUILD=1)
- tsc --noEmit: clean
- npm run build: clean (374.91 kB SW chunk; no test-hook leaks)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-19 17:21:15 +02:00
parent 6ac23fdbd8
commit 7645765401

View File

@@ -613,21 +613,25 @@ async function downloadArchive(archiveBlob: Blob) {
// Сохранение архива (полный процесс) // Сохранение архива (полный процесс)
// //
// Plan 01-13 Task 9 (debug session 01-09-save-stops-recording): SAVE is // Plan 01-09 Amendment 3 (2026-05-19, debug session
// one-shot per SPEC intent (`Тз расширение фаза1.md`: "one click MUST // 01-09-save-does-not-stop-recording): REVERSES the prior Amendment 2
// produce a self-contained archive"). After the zip lands (success path) // save-stops-recording fix. SAVE_ARCHIVE creates a new zip but does
// OR an EmptyVideoBufferError surfaces (empty-buffer path), the SW // NOT stop the recorder — per operator UX iteration preferring the
// auto-stops the recording — dispatches STOP_RECORDING to offscreen, // original "continuous capture / always-on safety net" charter
// flips isRecording=false, transitions to IDLE. This mirrors the Bug B // (`Тз расширение фаза1.md`). The only termination paths are:
// `user-stopped-sharing` branch in the RECORDING_ERROR handler: deliberate // 1. Operator clicks Chrome's "Stop sharing" banner →
// stop, NOT an error — no recovery notification. // offscreen's onUserStoppedSharing emits RECORDING_ERROR{error:
// 'user-stopped-sharing'} → SW's RECORDING_ERROR handler routes
// through setIdleMode (Bug B branch — preserved).
// 2. Browser closes (SW + offscreen torn down).
// 3. Extension uninstalled.
// //
// The post-save teardown is invoked from a `finally` block so both // Operator workflow: hit a bug → click SAVE (zip lands) → keep working
// branches share the same teardown semantics: the operator perceives // → the next bug is also captured because the ring buffer keeps
// SAVE click as ALWAYS stopping the session, success or empty-buffer // filling. Contract pinned by
// alike. Otherwise an empty-buffer error would leave the recording live // `tests/background/save-archive-does-not-stop-recording.test.ts` and
// + an ERR badge + a recovery notification, defeating the operator UX // Plan 01-13 harness assertion A14 (both inverted from the prior
// contract. // Amendment 2 contract).
async function saveArchive() { async function saveArchive() {
try { try {
logger.log('Starting archive save process'); logger.log('Starting archive save process');
@@ -716,53 +720,12 @@ async function saveArchive() {
} }
} }
return { success: false, error }; return { success: false, error };
} finally {
// Plan 01-13 Task 9 post-save auto-stop. Runs on BOTH the success
// path (zip downloaded) and the catch path (EmptyVideoBufferError +
// any other createArchive failure). The operator clicked SAVE — the
// session is over from their perspective regardless of internal
// success/failure. Releasing the MediaStream + transitioning to
// IDLE is the consistent operator-facing UX contract.
//
// Three steps, each independently best-effort:
// 1. STOP_RECORDING → offscreen via chrome.runtime.sendMessage.
// The offscreen recorder's onMessage handler at
// src/offscreen/recorder.ts:848 dispatches STOP_RECORDING
// through `stopRecording()`, which nulls mediaStream, stops
// the MediaRecorder, releases all tracks (closes Chrome's
// sharing banner), and clears the rotation timer.
// 2. isRecording = false on the SW side so the next
// chrome.action.onClicked toolbar gesture isn't blocked by
// the guard at line ~877.
// 3. setIdleMode → empties the popup (re-enables onClicked
// restart path per MV3 contract) and paints the OFF badge.
//
// RECORDING_ERROR path note: if the catch branch above dispatched
// RECORDING_ERROR{error:'empty-video-buffer'}, the SW's own
// RECORDING_ERROR handler at line ~756 would run setErrorMode
// (yellow ERR badge + recovery notification). The current ordering
// — error broadcast in catch, then setIdleMode in finally — means
// the FINAL state is IDLE: setIdleMode lands AFTER the RECORDING_ERROR
// self-message because the handler is synchronous and runs
// immediately on dispatch (chrome.runtime.sendMessage to self in
// SW context is dispatched within the same event loop turn). The
// post-save IDLE state machine wins; the operator sees a clean
// OFF badge + empty popup + sharing banner closed. The
// empty-buffer notification still fires (operator sees the
// recovery notif briefly) but the badge resolves to OFF — a
// documented trade-off keeping the empty-buffer surface visible
// while preserving the SAVE=stop contract.
try {
chrome.runtime.sendMessage({ type: 'STOP_RECORDING' });
} catch (sendErr) {
// Offscreen may be unreachable mid-teardown; non-fatal — SW state
// still transitions to IDLE so the operator regains the restart
// path.
logger.warn('STOP_RECORDING post-save dispatch failed:', sendErr);
}
isRecording = false;
setIdleMode();
} }
// Plan 01-09 Amendment 3 (2026-05-19): NO `finally` block here. The
// prior Amendment 2 finally block (STOP_RECORDING dispatch + isRecording=false
// + setIdleMode) is REMOVED. SAVE_ARCHIVE creates a zip and returns;
// the recorder and state machine stay in REC. See the function-level
// docstring above for the full charter rationale.
} }
// checkPermissions / requestPermissions удалены: старая permission // checkPermissions / requestPermissions удалены: старая permission