fix(01-09): startVideoCapture — remove stale active-tab dependency (D-01 cleanup gap)
The legacy chrome.tabs.query({ active: true, currentWindow: true }) +
"No active tab found" validation inside startVideoCapture were load-
bearing in the pre-D-01 chrome.tabCapture era but became functionally
dead after Plan 01-09's D-01 conversion to getDisplayMedia-in-offscreen.
The only post-D-01 consumer was a log line at index.ts:521.
The dead validation caused an activeTab-permission-scope asymmetry
between callers: chrome.action.onClicked grants activeTab on the click
gesture (so tab.url was readable → toolbar path worked silently) but
chrome.notifications.onClicked does NOT grant activeTab and the extension
has no `tabs` permission, so notifications.onClicked → startVideoCapture
threw "No active tab found" before reaching ensureOffscreen. Operator
2026-05-20 UAT against the new notifStartupCta CTA copy ("Mokosh ready.
Click to start a recording.", commit 4bba679) surfaced the silent
notification failure.
Surgical fix: remove the dead tab query + validation + tab-dependent log
(src/background/index.ts:514-521); replace with a tab-independent log
that documents WHY (cites D-01 + this debug session). captureScreenshot
+ saveArchive retain their genuine tab dependencies (tab.windowId for
chrome.tabs.captureVisibleTab; tab.id for content-script sendMessage).
Tests: tests/background/start-video-capture-no-tab.test.ts (NEW) pins
the contract with 3 cases (tabs.query → []; → [{id}] url-less; →
[{id,url,windowId}] regression guard for toolbar path).
Gates: vitest 153/153 GREEN (was 150/150 baseline; +3); test:uat 24/24
GREEN; tsc clean; build clean. Pre-checkpoint bundle gates per
feedback-pre-checkpoint-bundle-gates.md: SW chunk hook-string Tier-1
grep 0 matches; eval/Node-global/DOM-global matches unchanged from
baseline (all vendor-library feature-detect, guarded; no new imports).
Debug record: .planning/debug/resolved/01-09-notification-start-no-active-tab.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
---
|
||||
slug: 01-09-notification-start-no-active-tab
|
||||
status: awaiting_human_verify
|
||||
goal: find_and_fix
|
||||
trigger: "Operator UAT 2026-05-20: clicking onStartup notification ('Mokosh ready. Click to start a recording.') logs `Failed to start video capture: Error: No active tab found` and `notification-triggered start failed: Error: No active tab found` from the SW chunk (La function). Notification path silently fails; toolbar path unaffected."
|
||||
created: 2026-05-20
|
||||
updated: 2026-05-20
|
||||
phase: 01-stabilize-video-pipeline
|
||||
plan: 01-09
|
||||
orchestrator_diagnosed: true
|
||||
red_test: tests/background/start-video-capture-no-tab.test.ts
|
||||
---
|
||||
|
||||
# Debug session 01-09-notification-start-no-active-tab — startVideoCapture must not depend on an active tab (D-01 cleanup gap)
|
||||
|
||||
## Problem statement
|
||||
|
||||
After Plan 01-10 + the 2026-05-20 commit `4bba679` split the onStartup
|
||||
notification CTA text to "Mokosh ready. Click to start a recording.",
|
||||
operator UAT exercised the notification click path for the first time and
|
||||
observed silent failure: the SW devtools console logged
|
||||
|
||||
```
|
||||
[SW:Main] 2026-05-20T08:56:43.336Z Failed to start video capture: Error: No active tab found
|
||||
at La (index.ts-CKI4Evvn.js:17:95466)
|
||||
[SW:Main] 2026-05-20T08:56:43.336Z notification-triggered start failed: Error: No active tab found
|
||||
at La (index.ts-CKI4Evvn.js:17:95466)
|
||||
```
|
||||
|
||||
Badge stayed IDLE; no Chrome "Sharing your screen" banner; no recording
|
||||
began. Toolbar click path remained functional.
|
||||
|
||||
## Root cause
|
||||
|
||||
`src/background/index.ts:514-521` (pre-fix) inside `startVideoCapture()`:
|
||||
|
||||
```typescript
|
||||
// Получаем активную вкладку
|
||||
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
|
||||
if (!tab.id || !tab.url) {
|
||||
throw new Error('No active tab found');
|
||||
}
|
||||
|
||||
logger.log(`Starting video capture for tab ${tab.id}: ${tab.url}`);
|
||||
```
|
||||
|
||||
This block was load-bearing in the pre-D-01 `chrome.tabCapture` era —
|
||||
the capture stream-id was bound to the active tab. Since Plan 01-09's
|
||||
D-01 conversion (`chrome.tabCapture` → `getDisplayMedia` whole-desktop in
|
||||
offscreen), the tab reference has been functionally dead: every line
|
||||
after 521 (`ensureOffscreen()`, `await offscreenReady`,
|
||||
`chrome.runtime.sendMessage({ type: 'START_RECORDING' })`, `isRecording =
|
||||
true`, `setRecordingMode()`, error handling) makes no reference to `tab`
|
||||
or to any tab property.
|
||||
|
||||
The two callers diverge because of the chrome permission model:
|
||||
|
||||
* `chrome.action.onClicked` (toolbar) — Chrome grants `activeTab` on
|
||||
the click gesture; `tab.url` is readable; the validation at line 517
|
||||
passes; the dead log line executes; the function proceeds normally.
|
||||
* `chrome.notifications.onClicked` — NOT a user gesture toward a tab.
|
||||
`activeTab` is not granted, and the extension manifest declares no
|
||||
`tabs` permission (per `.planning/intel/01-11-SUMMARY.md` follow-up
|
||||
backlog: "tabs permission gap"). `chrome.tabs.query({ active: true,
|
||||
currentWindow: true })` resolves to a tab whose `url` is undefined
|
||||
(or `[]` depending on Chrome version + window state); `!tab.url`
|
||||
evaluates true; the function throws "No active tab found" before
|
||||
`ensureOffscreen()` is reached.
|
||||
|
||||
The bug was latent because Plan 01-09's smoke validation only
|
||||
exercised toolbar.onClicked. The new Plan 01-10 + debug-2-of-3
|
||||
notification CTA text (commit `4bba679`) explicitly invites operators
|
||||
down the previously-untested notification.onClicked path. Operators
|
||||
hit this on every browser-restart-after-install.
|
||||
|
||||
## Fix
|
||||
|
||||
Surgical removal of the dead tab query + validation + tab-dependent
|
||||
log from `startVideoCapture` (`src/background/index.ts:514-521`).
|
||||
Replace with a tab-independent log message that documents WHY the
|
||||
removal happened (D-01 cleanup gap + this debug session).
|
||||
|
||||
```typescript
|
||||
// Plan 01-09 D-01 cleanup gap (debug session
|
||||
// `01-09-notification-start-no-active-tab`, 2026-05-20):
|
||||
// The legacy chrome.tabs.query({ active: true, currentWindow: true })
|
||||
// here was load-bearing in the pre-D-01 chrome.tabCapture era but
|
||||
// is functionally dead post-D-01 — capture is whole-desktop via
|
||||
// getDisplayMedia in offscreen and the SW-side start path needs
|
||||
// no tab reference. The query also failed for chrome.notifications
|
||||
// .onClicked callers (no activeTab grant + no `tabs` permission →
|
||||
// tab.url undefined → "No active tab found" throw) so the onStartup
|
||||
// notification CTA was silently broken. captureScreenshot +
|
||||
// saveArchive retain their own genuine tab queries (tab.windowId
|
||||
// for captureVisibleTab, tab.id for content-script sendMessage).
|
||||
logger.log('Starting video capture (whole-desktop via getDisplayMedia in offscreen per D-01)');
|
||||
```
|
||||
|
||||
### Out of scope (NOT touched — genuine tab dependencies)
|
||||
|
||||
* `captureScreenshot()` `src/background/index.ts:568-591` — passes
|
||||
`tab.windowId` to `chrome.tabs.captureVisibleTab(tab.windowId, ...)`.
|
||||
Genuine dependency on the active tab's window.
|
||||
* `saveArchive()` `src/background/index.ts:741+` — uses `tab.id` for
|
||||
`chrome.tabs.sendMessage(tab.id, ...)` to query the rrweb content
|
||||
script for events, and uses the tab's window for the embedded
|
||||
screenshot. Genuine dependency.
|
||||
|
||||
These remain because (a) they only fire under operator gestures that
|
||||
DO grant activeTab (toolbar click → popup → SAVE), and (b) they have
|
||||
real downstream API consumers of tab fields. A separate Phase 5
|
||||
"adopt `tabs` permission" decision is the appropriate forum for
|
||||
hardening; not in scope for this surgical bug fix.
|
||||
|
||||
## TDD evidence
|
||||
|
||||
### RED test (new file)
|
||||
|
||||
`tests/background/start-video-capture-no-tab.test.ts` — 3 tests:
|
||||
|
||||
* **A:** `tabs.query` mocked to `[]` (no-activeTab notifications.onClicked
|
||||
context) → assert `chrome.runtime.sendMessage({type:'START_RECORDING'})`
|
||||
was dispatched (was failing pre-fix with "expected 0 to be greater than
|
||||
or equal to 1" — startVideoCapture threw before reaching sendMessage).
|
||||
* **B:** `tabs.query` mocked to `[{id: 1}]` (url-less tab) → same
|
||||
assertion. Was failing pre-fix for the identical reason.
|
||||
* **C:** REGRESSION GUARD — `tabs.query` mocked to a fully-populated
|
||||
tab (action.onClicked / activeTab-granted context) → same assertion.
|
||||
Was passing pre-fix; MUST stay passing after the fix so we don't
|
||||
over-trim and break the toolbar path.
|
||||
|
||||
Pre-fix run:
|
||||
```
|
||||
Test Files 1 failed (1)
|
||||
Tests 2 failed | 1 passed (3)
|
||||
```
|
||||
Post-fix run:
|
||||
```
|
||||
Test Files 1 passed (1)
|
||||
Tests 3 passed (3)
|
||||
```
|
||||
|
||||
### Acceptance gates (all GREEN)
|
||||
|
||||
| Gate | Pre-fix baseline | Post-fix | Notes |
|
||||
|------|------------------|----------|-------|
|
||||
| `npm test` | 150/150 | **153/153** | +3 from new test file; 27 → 28 test files |
|
||||
| `npm run test:uat` | 24/24 | **24/24** | No UAT changes; harness unaffected |
|
||||
| `npx tsc --noEmit` | exit 0 | **exit 0** | No type-system surface change |
|
||||
| `npm run build` | clean | **clean** | Built in 23.36s |
|
||||
|
||||
Note: A single ffprobe-driven test (`tests/background/webm-remux.test.ts
|
||||
> ffprobe -count_frames`) showed an intermittent 5000ms timeout in one
|
||||
of the multi-suite runs but passed in isolation (5/5) and on the second
|
||||
full-suite run (153/153). This is a pre-existing flake under load
|
||||
(ffprobe child-process spawn time), entirely unrelated to this fix —
|
||||
no source files this fix touches are consumed by webm-remux tests.
|
||||
|
||||
### Pre-checkpoint bundle gates (per saved memory `feedback-pre-checkpoint-bundle-gates.md`)
|
||||
|
||||
Inspected the production SW chunk `dist/assets/index.ts-DBpA3-1k.js`
|
||||
(referenced by `dist/service-worker-loader.js`):
|
||||
|
||||
* **Hook-string Tier-1 grep:** 0 matches (TEST_HOOK / MOKOSH_TEST_HOOK
|
||||
/ `__test__` / `window.mokosh` / `globalThis.mokosh` etc).
|
||||
* **SW CSP safety (eval / new Function):** 1 match — vendor library
|
||||
internal templater (rrweb-adjacent), guarded by `typeof` checks;
|
||||
pre-existing in baseline build; unrelated to this fix (no new
|
||||
vendor imports).
|
||||
* **Node-globals (require / process / Buffer / __dirname / global):** 2
|
||||
matches — all inside vendor library `ArrayBuffer`/`Buffer` runtime
|
||||
feature-detect paths; pre-existing; not introduced by this fix.
|
||||
* **DOM-globals (window / document / localStorage / alert / navigator):**
|
||||
3 matches — all inside vendor library `typeof window<"u"&&...`
|
||||
feature-detect patterns (firebug-shim, ArrayBuffer detect, JSDOM
|
||||
detect); evaluate false in SW context at runtime; pre-existing;
|
||||
not introduced by this fix.
|
||||
* **Manifest validation:** unchanged — same permissions, same
|
||||
service_worker entry, same web_accessible_resources.
|
||||
|
||||
The diff is purely subtractive in the SW source (8 lines removed, 14
|
||||
lines of code-comment + 1 new logger.log line added; no new imports,
|
||||
no new APIs, no new vendor dependencies). It cannot have introduced
|
||||
any of the pre-existing vendor matches.
|
||||
|
||||
## Why this was latent until 2026-05-20
|
||||
|
||||
* D-01 (`chrome.tabCapture` → `getDisplayMedia` in offscreen) shipped
|
||||
in Plan 01-09; the tab dependency was orphaned at that point.
|
||||
* Plan 01-09 smoke validation only exercised the toolbar
|
||||
`action.onClicked` path, which grants activeTab and made the dead
|
||||
code transparent.
|
||||
* The original `notifStartup` CTA copy wording was conservative
|
||||
("Mokosh is ready"), not action-inviting; operators tended to
|
||||
dismiss the notification rather than click it.
|
||||
* Commit `4bba679` (2026-05-20) split `notifStartup` into
|
||||
`notifStartupCta` ("Mokosh ready. Click to start a recording.")
|
||||
+ `notifRecordingStarted`. The new CTA copy explicitly invites
|
||||
the click — operators began exercising the path on browser
|
||||
restarts. The latent bug surfaced immediately.
|
||||
|
||||
## Files modified
|
||||
|
||||
* `src/background/index.ts` — startVideoCapture: removed lines
|
||||
514-521 dead tab query + validation + tab-dependent log; replaced
|
||||
with a multi-line WHY comment + a tab-independent
|
||||
`logger.log('Starting video capture (whole-desktop via
|
||||
getDisplayMedia in offscreen per D-01)')`.
|
||||
* `tests/background/start-video-capture-no-tab.test.ts` (NEW) —
|
||||
3 tests pinning the new contract.
|
||||
|
||||
## Forward-looking notes
|
||||
|
||||
* `captureScreenshot()` and `saveArchive()` still query the active
|
||||
tab; this is correct because they are reached only through gestures
|
||||
that grant activeTab (toolbar → popup → SAVE button). Do NOT
|
||||
generalise this fix to those functions — they have real downstream
|
||||
consumers of tab fields.
|
||||
* The "add `tabs` permission to manifest" question is a separate
|
||||
scope-expansion decision (Phase 5 hardening candidate). It would
|
||||
let notifications.onClicked + future no-gesture paths read tab
|
||||
metadata, but it widens the permission surface. Not required for
|
||||
the current bug because the start-recording path doesn't need tab
|
||||
data.
|
||||
* Defensive UX feedback (e.g., showing an error notification when
|
||||
startVideoCapture fails) is out of scope here. After this fix
|
||||
the failure mode that motivated such UX is gone; if a future
|
||||
failure path emerges, address it then.
|
||||
|
||||
## Operator UAT closure
|
||||
|
||||
Orchestrator re-spawns the operator UAT post-merge to confirm:
|
||||
|
||||
1. Install built extension to fresh Chrome profile.
|
||||
2. Restart Chrome → `onStartup` fires → mokosh-startup-* notification
|
||||
appears with CTA "Mokosh ready. Click to start a recording."
|
||||
3. Click the notification → badge transitions to REC → Chrome shows
|
||||
"Sharing your screen" banner → SW devtools console shows
|
||||
`Starting video capture (whole-desktop via getDisplayMedia in
|
||||
offscreen per D-01)` + `START_RECORDING sent successfully` (NO
|
||||
"No active tab found" anywhere).
|
||||
4. Toolbar click while not recording still starts a new session
|
||||
(regression guard against over-trimming).
|
||||
Reference in New Issue
Block a user