79964e62d2ce4f2c20d29ef16da62bcd71df273c
28 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 79964e62d2 |
feat(02-02): SW — downloadArchive via offscreen-minted Blob URL + revoke lifecycle (D-P2-01 closes P0-6)
Production changes (src/background/index.ts):
- pendingDownloadUrlResolvers Map<requestId, resolver> routes DOWNLOAD_URL
responses back to the in-flight downloadArchive Promise; mirrors the
pendingBufferRequests pattern from the BUFFER round-trip so port
replacement mid-mint does not lose the response.
- pendingRevokes Map<downloadId, url> tracks (downloadId → minted blob:URL)
for the chrome.downloads.onChanged revoke dispatch.
- onConnect port message sink extended with DOWNLOAD_URL routing branch
(alongside existing PING/BUFFER routing).
- downloadArchive rewritten: encode archive via blobToBase64 → post
CREATE_DOWNLOAD_URL on videoPort → await DOWNLOAD_URL response (race
against 5s BLOB_URL_MINT_TIMEOUT_MS) → reject empty / non-blob: URLs
(T-02-02-03 mitigation) → call chrome.downloads.download → register
(downloadId, url) in pendingRevokes. NO data:URL fallback — typed
errors route through saveArchive's catch to RECORDING_ERROR.
- chrome.downloads.onChanged listener registered at module init:
on terminal state ('complete' / 'interrupted'), posts REVOKE_DOWNLOAD_URL
to videoPort and clears the pendingRevokes entry.
Deviation (Rule 3 — auto-fix blocking issue):
- Plan 02-01's test helpers in blob-url-download.test.ts +
meta-json-urls-schema.test.ts + strict-meta-json-validation.test.ts
modeled only the REQUEST_BUFFER → BUFFER round-trip, not the new
CREATE_DOWNLOAD_URL → DOWNLOAD_URL round-trip Plan 02-02 introduces.
Without the test-side mint simulation, the SW's downloadArchive
times out at the offscreen mint step → chrome.downloads.download
never called → ALL existing meta.json tests timeout.
- Each helper extended with a tryFireDownloadUrl block that decodes
the CREATE_DOWNLOAD_URL.dataBase64, mints a Node-native blob:URL via
URL.createObjectURL, captures the archive bytes for downstream
JSZip extraction (capturedArchiveBytes), and replies DOWNLOAD_URL.
Test 3 (revoke lifecycle) additionally shims port.postMessage to
call URL.revokeObjectURL on receipt of REVOKE_DOWNLOAD_URL — the
test-side equivalent of src/offscreen/recorder.ts handleCreateDownloadUrl.
- Pre-existing Plan-02-02-era TODO comments in both test files
explicitly anticipated this extension ("Plan 02-03 implementer will
likely need a different helper, e.g. spy on URL.createObjectURL").
Verification (full §verification block from plan):
- npx tsc --noEmit: clean
- npm run build: clean
- npx vitest run tests/background/blob-url-download.test.ts: 3/3 GREEN (was 3 RED)
- npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts: 13/13 GREEN
- npm test full suite: 163 passed / 8 failed (was 159 passed / 12 failed);
net delta +4 GREEN = 3 RED→GREEN flips + 1 ffprobe-flaky pass. 8 remaining
RED are exactly the Plan 02-03 territory (5 meta-json-urls-schema + 3
strict-meta-json-validation RED tests).
- grep -c "data:application/zip;base64," src/background/index.ts: 0 (gone)
- grep -c "blob:" src/background/index.ts: 8 (new pipeline)
- grep -c "chrome.downloads.onChanged" src/background/index.ts: 5 (listener wired)
- dist/ post-build: 0 "data:application/zip;base64," matches; 1 file with
"chrome.downloads.onChanged" (the SW chunk).
|
|||
| a2dfc8cb9b |
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
|
|||
| 4bba679e39 |
fix(01-09): notifStartup text split — notifStartupCta for onStartup; notifRecordingStarted for manual-start
Operator UAT 2026-05-20 rejected the build because the OS notification fired
on `chrome.runtime.onStartup` ("Recording started. I'm watching the last 30
seconds.") implied recording had auto-started when in fact recording was
not running. Per Phase 1 always-on charter recording does NOT auto-start;
the notification is the gesture surface that invites the operator to start
one (notifications.onClicked → startVideoCapture, src/background/index.ts:1038).
Root cause: a single i18n key `notifStartup` conflated the pre-recording
CTA-with-gesture path (the only path actually wired today) and a future
post-manual-start confirmation path. The key's own `.description` field
acknowledged the conflation. Operator-facing text leaned toward the
confirmation phrasing.
Fix (key split, no behavior change):
- `notifStartupCta` — EN: "Mokosh ready. Click to start a recording." /
RU: "Mokosh готов. Нажмите, чтобы начать запись." — wired into the
onStartup handler.
- `notifRecordingStarted` — preserves the original text ("Recording
started. I'm watching the last 30 seconds." / "Запись запущена…") for
a future post-manual-start confirmation flow.
- Fallback constant renamed `NOTIF_STARTUP_FALLBACK` →
`NOTIF_STARTUP_CTA_FALLBACK`; value updated to match the new CTA text.
- Inline test comment in tests/background/onstartup-notification.test.ts
refreshed to reference the new key + fallback. Assertion regex
/recording|recor|click/i covers both fallback + resolved locale variants,
no logic change.
Notification behavior preserved: same id prefix `mokosh-startup-`, same
priority, same icon, same onClicked → startVideoCapture wiring. No new
test-mode symbols (FORBIDDEN_HOOK_STRINGS inventory stays at 12).
Files modified:
- _locales/en/messages.json
- _locales/ru/messages.json
- src/background/index.ts
- tests/background/onstartup-notification.test.ts
Verification:
- npx vitest run --exclude tests/build/** --exclude tests/background/no-test-hooks-in-prod-bundle.test.ts: 104/104 GREEN
- npx vitest run tests/i18n/ tests/background/onstartup-notification.test.ts: 18/18 GREEN (locale-parity 4/4 + onstartup-notification 14/14)
- npx tsc --noEmit clean on src/background/index.ts
The 2 build-dependent vitest gates (tests/build/no-remote-fonts.test.ts +
tests/background/no-test-hooks-in-prod-bundle.test.ts) and npm run test:uat
are deferred to orchestrator-level re-verification after the parallel
Plan 01-10 mark-bundling fix also lands (operator-UAT re-spawn coordinated
by orchestrator).
Debug record: .planning/debug/resolved/01-09-startup-notification-misleading-text.md
Operator UAT rejection event: 2026-05-20
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 8f329d8b74 |
feat(01-10): wave-2 task-3 — openWelcomeIfFirstInstall helper + onInstalled wiring (D-17-onboarding) — 3 RED → GREEN
Plan 01-10 Wave 2: SW handler extension flips Task 1's 3 RED onboarding
tests GREEN.
src/background/index.ts changes:
1. Three top-level constants added near the badge/notification block:
- ONBOARDING_FLAG = 'onboarding-completed'
- ONBOARDING_INSTALLED_AT = 'installed-at'
- WELCOME_PATH = 'src/welcome/welcome.html'
SCREAMING_SNAKE per project naming standard for true constants.
2. openWelcomeIfFirstInstall helper added below ensureOffscreen
(interfaces §1 placement). JSDoc cites D-17-onboarding (CONTEXT.md
line 537+; SUFFIX disambiguates from D-17-port-lifecycle per
CONTEXT.md lines 540-545). Body:
- Early return on details.reason !== 'install' (subsequent
installs / updates / chrome_update / shared_module_update do
NOT open a welcome tab — Test B's contract).
- chrome.storage.local.get(ONBOARDING_FLAG) read with the EXACT
single-key string (storage-schema cross-version-compat pin;
Test A.3's contract).
- Early return if stored[ONBOARDING_FLAG] === true — Test C's
contract (already-onboarded suppression).
- chrome.tabs.create + chrome.storage.local.set with both the
flag and Date.now() installed-at — Test A.1 + A.2's contract.
- Defense-in-depth try/catch wraps the whole body; any thrown
chrome.* call is logged via logger.warn but does not propagate
(D-16-toolbar start path remains independent).
3. onInstalled listener extended: fire-and-forget call to
openWelcomeIfFirstInstall(details) AFTER initialize(); .catch()
boundary so rejected promises cannot escape the synchronous
listener. The existing IDB cleanup + initialize() call sequence
stays unchanged.
Architectural compliance:
- NO `await import(...)` added (01-11-SUMMARY architectural constraint
preserved; the three matches in lines 14-28 are documentation
comments about Plan 01-11's falsification).
- NO `as any` (chrome.runtime.InstalledDetails ambient typing covers
the parameter).
- NO `continue` (if-else early-return only).
- No new dependencies.
Verify (all GREEN):
- npx vitest run tests/background/onboarding.test.ts: 3 passed (Test
A flipped RED → GREEN; B + C continue passing as load-bearing
guards).
- Full vitest baseline 147 → 150 (137 ex-build-gated + 13 in build-
gated = 150 GREEN total).
- npx tsc --noEmit: clean.
- npm run build: clean; openWelcomeIfFirstInstall + D-17-onboarding
references survive into dist/assets/index.ts-*.js.
- Tier-1 FORBIDDEN_HOOK_STRINGS unchanged at 12 entries; gate GREEN.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 468f16d7e7 |
feat(01-12): wave-4 task-1 — adopt tokens.css + chrome.i18n.getMessage in src/popup/ + src/background/ (loom palette + RU i18n + en fallback)
src/popup/style.css:
- Adds @import "../shared/tokens.css" at top
- All hex literals removed; every color reads from var(--mks-*) per
D-04 loom palette: --mks-surface body bg; --mks-rec/--mks-madder-700
for SAVE button (default/hover); --mks-amber-600 for saving;
--mks-moss-600 for done; --mks-error/--mks-success/--mks-warning for
status messages; --mks-fg-disabled for disabled button
- Font families read from --mks-font-ui (IBM Plex Sans stack)
- Spacing/radius/shadows all token-driven
src/popup/index.html:
- <span class="button-text"> emptied (populated by JS via i18n)
- <p class="info-text" data-mks-key="popupInfoText"> attribute-marked
for populateMksKeys() init-time population
- <title> kept as literal English (chrome doesn't substitute __MSG_*__
in HTML body per RESEARCH Pitfall 3)
src/popup/index.ts:
- New `i18n(key, fallback)` helper: chrome.i18n.getMessage with explicit
`|| <fallback>` for unit-test contexts without chrome.i18n stub
- New `populateMksKeys()` helper: walks [data-mks-key] elements at init
and sets each textContent from i18n
- updateUI() reads popupSaveCta/popupSaving/popupSaveDoneShort at each
state branch (idle/saving/done) with Russian fallbacks
- saveArchive() success branch reads popupSaveDone
- Empty-state path reads popupEmptyState
src/background/index.ts:
- BADGE_REC_COLOR: '#00C853' → '#b2543d' (= --mks-madder-600 per D-04;
RESEARCH §10 Open Question A7 default-action)
- BADGE_OFF_COLOR + BADGE_ERROR_COLOR retained as engineering choices
(no loom-palette token for material-red/amber-700 equivalents)
- BADGE_REC_TITLE/BADGE_OFF_TITLE/BADGE_ERROR_TITLE renamed to
..._FALLBACK and only referenced at the chrome.i18n.getMessage call
sites inside setBadgeState (i18nMessage('tooltipRecPrefix' etc.))
- New `i18nMessage(key, fallback)` helper mirroring popup's i18n()
- Recovery notification: title=i18nMessage('extName',...); message=
i18nMessage('notifRecovery',...)
- Startup notification: title=i18nMessage('extName',...); message=
i18nMessage('notifStartup',...)
- NOTIF_EXTNAME_FALLBACK/NOTIF_STARTUP_FALLBACK/NOTIF_RECOVERY_FALLBACK
module-level constants for the |||| chain (degrade gracefully in
test contexts without chrome.i18n stub)
- NO `await import(...)` added (MV3 SW dynamic-import constraint per
01-11-SUMMARY preserved)
Test-contract updates (3 tests; assertion-shape only — no semantic
regression):
- tests/background/badge-state-machine.test.ts: greenCalls→recColorCalls
regex updated from /^#00[Cc]853$/ to /^#b2543d$/i lockstep with
BADGE_REC_COLOR change; title-substring assertion widened to
/Recording|recording/i to cover both EN locale + fallback
- tests/background/onstartup-notification.test.ts: title equality
('Mokosh ready') replaced with /Mokosh/i substring assertion
(survives both the 'Mokosh' fallback + 'Mokosh — Session Capture'
resolved EN); message regex widened to /recording|recor|click/i
- tests/background/toolbar-action.test.ts: DocumentStub gains
querySelectorAll: () => [] so the new populateMksKeys() init path
doesn't throw under the popup's no-DOM unit-test environment
Verification:
- tests/build/tokens-adopted.test.ts: 4/4 GREEN (was 2 RED + 2 GREEN)
- tests/build/no-remote-fonts.test.ts: 4/4 GREEN after fresh build
(Vite emits the WOFF2 files as content-hashed dist/assets/*.woff2;
tokens.css references resolve through the asset pipeline; no
remote-font URLs anywhere in dist/)
- Full vitest sweep: 147/147 GREEN (was 145/147)
- npx tsc --noEmit: clean
- Tier-1 grep gate: 13/13 GREEN (no new test-mode symbols)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 7645765401 |
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 |
|||
| 4f4c3e2241 |
feat(01-09-save-stops): GREEN — SAVE_ARCHIVE auto-stops recording per SPEC one-shot intent
Operator UAT closure for Plan 01-13 Task 9. Patches saveArchive() in
src/background/index.ts with a `finally` block that dispatches
STOP_RECORDING to offscreen (mirrors the existing START_RECORDING
control-plane channel via chrome.runtime.sendMessage), flips
isRecording=false, and calls setIdleMode() — applied to BOTH the
success and empty-buffer-error paths.
Operator UX contract: SAVE click ALWAYS stops the session, regardless of
internal success/empty-buffer outcome. The badge clears, the popup
empties (re-enabling chrome.action.onClicked for restart), and Chrome's
sharing banner closes via the offscreen recorder's stopRecording()
(which nulls mediaStream + stops all tracks + clears the rotation
timer — line 527 of src/offscreen/recorder.ts, already wired since
Plan 01-05).
Trade-off documented inline: empty-buffer path still surfaces a
recovery notification (the catch branch emits RECORDING_ERROR{
error:'empty-video-buffer'} → SW's own onMessage handler runs
setErrorMode + creates a mokosh-recovery-* notif). The finally block
then setIdleMode()'s, so the FINAL visible state is OFF/empty-popup —
clean restart path. The notification stays visible briefly so the
operator sees that something went wrong, then clicks it to start a
new session.
Test count: 94 GREEN (baseline) → 98 GREEN (+4 from
tests/background/save-archive-stops-recording.test.ts).
Files modified:
- src/background/index.ts (saveArchive + finally block; no
PortMessage/Message type changes — STOP_RECORDING already in
MessageType per src/shared/types.ts:14, offscreen handler at
recorder.ts:848 already wired)
Toolchain:
- npx tsc --noEmit: exit 0 (no type errors)
- npm run build: exit 0 (dist/ clean rebuild)
Debug record: .planning/debug/01-09-save-stops-recording.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| a63066a289 |
chore(01-13): wave-0 — clean broken Approach-A artifacts per 01-11-SUMMARY
Restore a clean baseline before promoting the |
|||
| c647f61553 |
wip(01-11): prototype — A6 via test-page+bridge+synthetic-stream PASSES
Plan 01-11 orchestrator commissioned a research+prototype investigation
into whether full MV3 UAT automation is feasible with the architecture:
extension-internal test page + chrome.runtime.sendMessage bridge +
synthetic MediaStream (canvas-captureStream + getSettings override).
EMPIRICAL VERDICT: feasible BUT plan 01-11 needs architectural revision.
Architectural findings (with proof):
1. DYNAMIC IMPORT BLOCKED IN MV3 SW. Top-of-module
`await import('../test-hooks/sw-hooks')` in src/background/index.ts
silently kills the SW (chunk loads, await never resolves, no
production listeners register, no console output). This is by design
per Chromium docs (es_modules.md) + w3c/webextensions#212. The Plan
01-11 RESEARCH §6 architecture was wrong for the SW side.
Workaround in this prototype: REMOVE the SW-side gated dynamic
import. SW-side test hooks need a different design (see verdict).
2. OFFSCREEN-SIDE DYNAMIC IMPORT WORKS. Offscreen is a DOM document,
not a SW, so top-level await + dynamic import behave normally. The
offscreen-hooks.ts gated import succeeds; installFakeDisplayMedia is
installed eagerly at module load.
3. EXTENSION-INTERNAL PAGE HAS FULL chrome.* SURFACE. Reachable via
chrome-extension://<id>/tests/uat/prototype/extension-page-harness.html
(added as rollup input in vite.test.config.ts). The page can call
chrome.action.getBadgeText, chrome.action.getPopup, chrome.offscreen
.createDocument, chrome.notifications.getAll, chrome.runtime
.sendMessage — everything needed for A6.
4. NO 'tabs' PERMISSION → tab.url IS UNDEFINED. Production
startVideoCapture's `chrome.tabs.query({active:true})` check
(`if (!tab.id || !tab.url) throw`) fails because the manifest lacks
the 'tabs' permission. Prototype workaround: bypass startVideoCapture
by sending START_RECORDING directly to offscreen. The Bug B
contract being tested is independent of how recording starts; it
only depends on the RECORDING_ERROR routing path.
5. SYNTHETIC MEDIASTREAM WORKS. installFakeDisplayMedia builds a
canvas-captureStream MediaStream + monkey-patches the video track's
getSettings() to report displaySurface: 'monitor'. Production code's
post-grant validation passes. getDisplayMedia returns the synthetic
stream immediately — no picker, no headless flakiness.
A6 prototype result (with Bug B fix in place — current HEAD state):
[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)
A6 prototype result (with Bug B fix rewound to `if (false)`):
[PASS] SETUP: badge becomes REC after start
[FAIL] A6.1: badge text is '' (got "ERR")
[FAIL] A6.2: popup is '' (got chrome-extension://.../popup/index.html)
[FAIL] A6.3: notif delta = 0 (got 1)
[PASS] A6.4: isRecording=false ← false-positive (badge='ERR' not 'REC')
The Bug B regression rewind cycle proves the harness CAN catch regression:
4/5 checks turn RED on rewind, 5/5 turn GREEN with the fix restored.
Files in this commit:
- tests/uat/prototype/extension-page-harness.{html,ts} — the harness
page (chrome-extension URL, exposes window.__mokoshHarness.assertA6)
- tests/uat/prototype/a6.test.ts — Puppeteer driver (~270 lines)
- tests/uat/prototype/probe_*.mjs — diagnostic probes used to isolate
the SW dynamic-import blocker (probe_sw.mjs is the key one)
- src/test-hooks/offscreen-hooks.ts — added installFakeDisplayMedia +
dispatchEndedOnTrack + __mokoshOffscreenQuery bridge handler + auto-
install at module load
- vite.test.config.ts — added prototype harness page as rollup input;
added modulePreload.polyfill=false (red herring; harmless)
- src/background/index.ts — removed the broken SW-side gated dynamic
import (this is the BLOCKER unblocker — production 01-11 plan needs
to redesign SW-side test hooks before re-spawning)
Bundle hygiene: prototype runs against dist-test/; production dist/
remains hook-free (Tier-1 grep gate still GREEN, verified via
no-test-hooks-in-prod-bundle.test.ts in the unit test suite).
Vitest baseline: 89/89 GREEN preserved.
Runtime: ~7 seconds end-to-end (launch Chrome + open page + ensure
offscreen + start recording + dispatch ended + settle + assert).
See: research return for VERDICT + recommended next step.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| cb1a729962 |
feat(01-11): wave-1 — gated test hooks for SW + offscreen, dist/ stays hook-free
Task 2 of Plan 01-11 (Puppeteer UAT harness).
Test hook surface:
- src/test-hooks/types.ts: canonical MokoshTestSurface — handlers
(onClicked, onStartup, notificationOnClicked), notificationCount,
lastNotificationOptions<true>, notificationIds, getCurrentStream,
getSegmentCount. globalThis.__mokoshTest ambient declaration.
- src/test-hooks/sw-hooks.ts: SW-side hook. Monkey-patches addListener
on chrome.action.onClicked / chrome.runtime.onStartup / chrome
.notifications.onClicked to capture handler refs while chaining to
the original. Wraps chrome.notifications.create across all four
overload shapes (id+options+cb, options+cb, id+options→Promise,
options→Promise) to increment notificationCount, save
lastNotificationOptions, push resolved id into notificationIds.
- src/test-hooks/offscreen-hooks.ts: offscreen-side hook. Exports
setCurrentStream + setSegmentCountGetter; the recorder calls both
inside startRecording after the mediaStream + segments assignments.
getCurrentStream getter closes over the cell so the harness reads
the live MediaStream for displaySurface inspection + 'ended'
dispatch (Bug B BLOCKER per RESEARCH §7).
- tests/uat/lib/test-hook-contract.d.ts: manual harness-side mirror of
MokoshTestSurface (decoupled from src/ to keep tests/ import-clean
per RESEARCH §11 resolution 5; drift risk documented inline).
Production-side wires (gated by __MOKOSH_UAT__ token):
- src/background/index.ts top-of-module: `if (__MOKOSH_UAT__) { await
import('../test-hooks/sw-hooks'); }`. MUST run before any chrome.*
addListener call below — top-of-module placement satisfies this.
- src/offscreen/recorder.ts top-of-module: symmetric gated dynamic
import + module-scoped testHooks reference.
- src/offscreen/recorder.ts inside startRecording (after mediaStream
assignment): `if (__MOKOSH_UAT__) { testHooks?.setCurrentStream(stream);
testHooks?.setSegmentCountGetter(() => segments.length); }`
- src/offscreen/recorder.ts inside onUserStoppedSharing (after
mediaStream = null): `if (__MOKOSH_UAT__) { testHooks?.setCurrentStream(null); }`
— T-1-11-05 (Repudiation: stale stream ref) mitigation.
Build-time token wiring:
- vite.config.ts: declares `define: { __MOKOSH_UAT__: 'false' }` (prod
default) + bumps `build.target: 'es2022'` so the top-level await in
the gated dynamic imports compiles (MDN: Chrome 89 / Edge 89 /
Firefox 89 / Safari 15 support TLA; MV3 floor Chrome 88 is
effectively Chrome 89+ in field — comfortably inside the envelope).
- vite.test.config.ts: overrides `define: { __MOKOSH_UAT__: 'true' }`
so the test bundle has the hooks active.
- vitest.config.ts: declares `define: { __MOKOSH_UAT__: 'false' }` for
vitest's own source-loading runs. CRITICAL — without this, vitest
would throw `ReferenceError: __MOKOSH_UAT__ is not defined` when
loading src/background/index.ts; OR if we'd used `import.meta.env.MODE
=== 'test'` (RESEARCH §6's initial guidance), vitest's default
MODE='test' would have ACTIVATED the hooks under unit tests +
clobbered every existing vi.fn() chrome.notifications.create mock.
The dedicated `__MOKOSH_UAT__` token sidesteps both failure modes
cleanly — a refinement on RESEARCH §6 documented in the comment
preambles of all three configs.
- globals.d.ts: declares `__MOKOSH_UAT__: boolean` ambient so
`npx tsc --noEmit` passes without per-file annotations.
- tsconfig.json: include adds `globals.d.ts`.
Notification options generic refinement:
- chrome.notifications.NotificationOptions is declared with a
`<true | false>` generic distinguishing "create" (all required —
true) from "update" (all optional — false). Plan 01-11's production
code always uses the create shape; types.ts + sw-hooks.ts pin to
`NotificationOptions<true>` so the harness reads iconUrl etc. as
definitely-present.
Verification:
- npx tsc --noEmit: exit 0
- npm run build: exit 0
- grep -rln '__mokoshTest\|simulateUserStop\|getSegmentCount\|setCurrentStream\|setSegmentCountGetter' dist/:
ZERO matches (Tier-1 gate stays GREEN)
- npm run build:test: exit 0; dist-test/ emits separate sw-hooks-*.js
+ offscreen-hooks-*.js chunks (the gated dynamic imports survive
tree-shaking when __MOKOSH_UAT__ === true)
- grep -rln '__mokoshTest' dist-test/: 2 matches
(assets/sw-hooks-*.js + assets/offscreen-hooks-*.js)
- SKIP_BUILD=1 npx vitest run: 89/89 GREEN
(83 baseline + 6 Tier-1 hook-leak surfaces)
- sw-bundle-import.test.ts: GREEN (the gated dynamic import does not
break production module init — the `if (false)` branch is never
reachable so the await + import are dead code in dist/)
In-flight bugs auto-fixed (Rule 1 + Rule 3):
- Rule 3: original RESEARCH §6 plan called for `import.meta.env.MODE
=== 'test'` as the gate; switched to `__MOKOSH_UAT__` define-token
after observing vitest contamination (vitest defaults MODE='test'
→ hooks activated under unit tests → 8 existing tests broke with
"Cannot read properties of undefined (reading 'calls')" because the
hook wrapper replaced vi.fn() mocks). Documented in the comment
preambles of all three configs as a refinement on RESEARCH §6.
- Rule 3: esbuild rejected TLA against the default ES2020 target;
bumped to es2022 (Chrome 89+ supports TLA per MDN — inside MV3
envelope). Recorded in vite.config.ts preamble.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| b9eeeeb386 |
feat(01-09): GREEN — Bug B route user-stopped-sharing → IDLE; other codes → ERROR (preserved)
Patches the RECORDING_ERROR onMessage handler in src/background/index.ts
(lines 725-744 pre-patch) with conditional routing on the incoming
`message.error` payload:
- 'user-stopped-sharing' → setIdleMode() (popup empties; badge OFF;
isRecording flipped to false). Recovery notification suppressed —
the operator stopped deliberately, surfacing one would be UX noise.
The offscreen recorder's onUserStoppedSharing has already cleared
the buffer (src/offscreen/recorder.ts:457 resetBuffer), so IDLE is
the correct landing state.
- all other codes → setErrorMode() + recovery notification, preserving
the existing operator-facing surface for genuine capture failures
(codec-unsupported, wrong-display-surface, capture-failed, etc.).
Closes the operator-lockout regression observed in Plan 01-09 Task 5
empirical UAT: after Chrome's "Stop sharing" banner click, the badge
stayed yellow and the popup pinned to SAVE-only, gating
chrome.action.onClicked behind the popup forever. Operator had no
restart path. With IDLE routing, the popup empties and the toolbar
click fires startVideoCapture as designed.
Tests: 83/83 GREEN (was 81; +2 from Tests E+F). tsc clean. Build exit 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 06dee246c9 |
feat(01-09): GREEN — toolbar onClicked + badge state machine + onStartup notification + SAVE-only popup
Plan 01-09 Task 4 GREEN — flips all 13 Task 3 RED tests to GREEN:
src/background/index.ts:
• Badge palette + notification id prefix constants (SCREAMING_SNAKE).
• setBadgeState(state) helper: 3-state machine REC/OFF/ERROR with
deterministic setBadgeText + setBadgeBackgroundColor + setTitle.
Each chrome call wrapped in try/catch (defense in depth).
• setIdleMode / setRecordingMode / setErrorMode helpers — drive the
setPopup dance: '' in OFF (so onClicked fires), html path in REC/
ERROR (so popup opens for SAVE).
• startVideoCapture wires setRecordingMode on success, setErrorMode
in catch.
• chrome.action.onClicked.addListener — direct toolbar-to-picker flow
(no popup needed for start). isRecording guard prevents double-start.
• chrome.runtime.onStartup.addListener — fires once per browser
session; creates mokosh-startup- notification inviting recording.
• chrome.notifications.onClicked.addListener — T-1-09-01 spoofing
mitigation via 'mokosh-' prefix gate; clears notification + invokes
startVideoCapture (notification click is a valid activation gesture).
• RECORDING_ERROR onMessage branch — setErrorMode + creates a
mokosh-recovery- notification inviting the operator to restart.
• initialize() calls setIdleMode at SW boot — ensures fresh OFF state
on every (re-)spawn including Chrome's idle-eviction respawn.
• All new listener registrations wrapped in try/catch so unit-test
chrome stubs that don't define action/notifications/onStartup don't
crash SW load (preserves the 5 pre-existing request-id-protocol +
1 port-lifecycle-continuous tests as GREEN).
src/popup/index.ts:
• Removed checkPermissions + requestPermissions functions entirely
(no more REQUEST_PERMISSIONS round-trip on popup open).
• popupState defaults isRecording=true, hasPermissions=true under
SAVE-only charter — the popup ONLY opens when recording is active
(REC/ERROR setPopup html path), so SAVE button is always enabled.
• init() calls updateUI() directly (no async permission probe).
• Empty-state copy updated: 'Откройте запись через иконку расширения'
(Open recording via the extension icon — points operator back to
the toolbar for starting a new session).
• saveArchive() simplified: no permission re-check.
manifest.json:
• Added 'notifications' to permissions array (preserves all existing).
• default_popup retained — popup still opens in REC/ERROR modes.
smoke.sh (W-04 5-sub-step update):
• SHARE_TARGET='Entire screen' (was 'Mokosh Smoke Test').
• Added 14-line locale-fallback comment block citing Chromium
generated_resources.grd as authoritative source + 4 known locale
strings + KEEP_PROFILE=1 fallback path.
• <title> changed to 'Mokosh Smoke Test — monitor mode' to keep tab
title distinct from the screen-source string.
• <ol> instruction updated: picker auto-accepts entire screen, not
the tab. Body intro paragraph also updated.
• T+/wall timer overlay (commit
|
|||
| 35db6c2357 |
feat(01-08): swap mergeVideoSegments -> await remuxSegments at call site
- src/background/index.ts now imports remuxSegments from './webm-remux'
and awaits it in createArchive instead of synchronously calling the
retired file-concat mergeVideoSegments.
- mergeVideoSegments function declaration deleted entirely; only a
retirement comment remains naming Plan 01-08 D-14-remux as the
superseding decision.
- EmptyVideoBufferError throw paths preserved on (a) zero segments
AND (b) zero-byte output. Error message free-text changed from
"merged video blob is zero bytes" to "remuxed video blob is zero
bytes"; pre-flight grep (W-01 fix from plan checker pass)
confirmed no downstream consumer matches on the legacy string —
request-id-protocol.test.ts asserts on error.code ('empty-video-
buffer'), not the free-text message.
- createArchive remains async (was already declared async); saveArchive
already awaits createArchive so no upstream signature changes.
- Stale comment in decodeBufferSegments referencing mergeVideoSegments
updated to reflect the new remux pipeline (Rule 3: keep forward-
references accurate).
- CONTEXT.md amendment provenance verified intact via 4 grep checks
(B-01 fix from plan checker, folded from retired Task 6):
(a) D-14-remux disambiguated marker present (1 match)
(b) original D-13 line preserved (1 match)
(c) D-17-port-lifecycle amendment intact (1 match)
(d) webm-remux.ts replaces citation present (1 match)
No CONTEXT.md mutation by this task — verify-only step.
- npm run build exit 0; main SW bundle 374.56 KB (108.44 KB gzipped,
matches the d13 library survey's ~100 KB estimate for ts-ebml +
webm-muxer combined).
- Full suite: 13 files / 60 GREEN + 2 RED (webm-playback duration
assertions waiting on Task 5 fixture regen). tsc exit 0.
|
|||
| ffd383d2a6 |
feat(option-c-error-surface): createArchive throws on empty video; saveArchive surfaces to popup
Retires the upstream silent-skip defect (bisected to commit
|
|||
| 6ffa242cb9 |
feat(option-c-sw): request-id'd BUFFER routing + retry on port replacement + PONG echo
Implements the SW-side architectural refactor per
.planning/debug/empty-archive-port-race.md "Fix Strategy: Option C":
1. **Request-id'd protocol** — getVideoBufferFromOffscreen generates a
uuid (crypto.randomUUID with Math.random fallback) and sends
{type:'REQUEST_BUFFER', requestId} on the live videoPort. The
per-request listener pattern is GONE; replaced by a module-level
pendingBufferRequests Map<requestId, PendingBufferRequest>. The
onConnect-level message sink routes BUFFER -> resolve by id.
2. **Stale BUFFER routing** — BUFFER messages without a matching
requestId in the Map are silently dropped (no cross-talk). BUFFER
without a valid requestId at all is rejected with a warn (Option C
protocol requires the id).
3. **Retry on port replacement** — every onConnect (post-bootstrap)
scans pendingBufferRequests and re-issues REQUEST_BUFFER on the
fresh port with the SAME requestId. The offscreen posts BUFFER on
the current keepalivePort (see prior offscreen commit), the sink
matches by id, and the request resolves. This retires the H2
silent-drop class architecturally — the BUFFER reaches the SW
regardless of port-replacement timing.
4. **PING -> PONG echo** — the sink replies to every PING with PONG.
Closes the offscreen's health-probe loop (it counts missed PONGs
and reconnects when MAX_MISSED_PONGS exceeded — see prior offscreen
commit). The PONG post is wrapped in try/catch to absorb the same
port-closed-mid-response race the offscreen ping path handles.
5. **Outer hard-timeout bumped 2s -> 10s** — the legacy per-port
BUFFER_FETCH_TIMEOUT_MS = 2000 was too tight to retry across a
reconnect. The new outer budget covers EVERY retry across port
replacements; the inner round-trip is still ~100-200 ms.
6. **decodeBufferSegments extracted** — pulled out of the legacy
inline handler so the new onConnect sink can decode wire segments
without duplicating the logic. Preserves WR-07 (empty wire segment
filter) and base64ToBlob defensive catch behaviour. Closes the
pre-existing implicit-undefined-return path the legacy flatMap
catch had (tsc happy but semantically ambiguous).
Status: 51 GREEN, 1 RED. The remaining RED (createArchive must throw
on empty video, surfacing to operator) is addressed in the next commit.
Pinning contracts (D-12 port-serialization, D-13 segment-rotation,
A3 webm-playback) untouched. tsc --noEmit exit 0; type-safety grep clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 034155bc4e | fix(01-review): sweep #5 surface port-replaced-during-fetch diagnostic on buffer timeout | |||
| 6286957f53 | fix(01-review): IN-01 read extensionVersion from chrome.runtime.getManifest() | |||
| f8a9c10758 | fix(01-review): WR-08 downloadArchive use shared blobToBase64 helper | |||
| e9aae09f6d | fix(01-review): WR-07 base64ToBlob empty-input shortcut + SW-side empty-segment filter | |||
| 2e3f5248ce |
fix(01-review): CR-01+CR-02+CR-03+WR-03+WR-09 critical port + handshake race fixes
What was wrong:
- CR-01 (recorder.ts): encodeAndSendBuffer captured no port identity before
awaiting Promise.all(blobToBase64). If the port disconnected mid-encode
and onDisconnect synchronously reconnected (re-assigning keepalivePort
to a fresh instance), the post-await null-check evaluated false and
the BUFFER was posted on the NEW port — but the SW's per-request
onMessage listener was still bound to the OLD port (captured at
getVideoBufferFromOffscreen line 110). Result: SW timed out after
2 s, SAVE_ARCHIVE produced an empty-segments zip, data-loss path
masquerading as a benign timeout.
- CR-02 (background/index.ts): SW's onConnect handler attached
ONLY onDisconnect — no permanent onMessage sink. PING traffic
had no listener when getVideoBufferFromOffscreen wasn't running
(the normal idle state of the port), and field reports note Chrome's
SW idle-timer reset behaves inconsistently when no listener is
attached. Risk: PINGs silently dropped, SW evicted ~30 s into
recording, port torn down, next SAVE_ARCHIVE fails entirely.
- CR-03 (background/index.ts): offscreenReady is a one-shot Promise
resolved on the FIRST OFFSCREEN_READY message. If the SW is evicted
while the offscreen document persists, the next SW lifetime creates
a fresh Promise and waits on it forever — the offscreen never
re-emits OFFSCREEN_READY. startVideoCapture() hangs at
`await offscreenReady` until Chrome restarts.
- WR-03 (recorder.ts): `baseTimestamp + idx` (Date.now() + idx) used
millisecond resolution + array offset. Two REQUEST_BUFFER calls
within the same millisecond would collide, breaking the sort-by-
timestamp contract in SW-side mergeVideoSegments.
- WR-09 (recorder.ts): encodeAndSendBuffer always appended the
unfinalized in-flight segment to the BUFFER. That segment lacks
the Matroska SegmentSize and Cues that MediaRecorder.stop()
writes — re-introducing the "File ended prematurely" symptom
documented in debug session webm-playback-freeze.
What changed:
- recorder.ts encodeAndSendBuffer:
- Capture `portAtRequest = keepalivePort` BEFORE the encode.
- After the await, refuse to post if `keepalivePort !== portAtRequest`
(port was replaced by reconnect). SW already times out cleanly
after BUFFER_FETCH_TIMEOUT_MS = 2 s; the next SAVE_ARCHIVE
re-issues REQUEST_BUFFER on the fresh port. Stale data
NEVER reaches a stranger port.
- Include the in-flight segment ONLY when finalized.length === 0
(preserve the SAVE-within-first-10-s UX trade-off documented at
the original comment) — otherwise drop the unfinalized tail.
- Replace `baseTimestamp + idx` with module-level monotonic
`++segmentSeq` counter (zero wall-clock dependency).
- Switch from Promise.all/map+filter to a sequential for-loop
because each iteration now mutates the shared `segmentSeq`;
Promise.all timing would interleave assignments. Throughput
impact negligible (3 segments × ~50 ms base64 each ≈ 150 ms
vs ~50 ms parallel — still well under the 2 s SW budget).
- background/index.ts onConnect:
- Install a permanent `port.onMessage.addListener` that
explicitly drains PING and silently drops unknown traffic.
Per-request BUFFER listener still wins because it's attached
LATER in the listener chain when getVideoBufferFromOffscreen
fires; this sink only catches the idle PING stream and
guarantees the SW idle-timer reset is consumed by a real
handler.
- background/index.ts initialize():
- When `chrome.offscreen.hasDocument()` returns true on SW init,
immediately resolve `offscreenReady` AND null
`offscreenReadyResolve`. The offscreen MUST have completed its
bootstrap before it was observable via hasDocument(); waiting
for an OFFSCREEN_READY that will never come is a deadlock.
Why these fixes vs alternatives:
- CR-01: alternatives considered: (a) cancel encoding when port
disconnects (requires AbortController plumbing into blobToBase64);
(b) re-route the BUFFER through the new port via a per-port
request-id correlation. Both add machinery for a case the SW
already handles correctly (2 s timeout → retry). The capture-
identity check is the minimum-mechanism fix and matches REVIEW.md
CR-01 fix guidance exactly.
- CR-02: alternative considered: documenting "rely on kernel-level
port-message side effect for idle-timer reset" — REJECTED, this
is what the existing comment did and the field evidence shows
it's unreliable. Explicit listener is the safe default.
- CR-03: alternative considered: (a) have offscreen re-emit
OFFSCREEN_READY on every inbound SW message — adds noise to
the message bus and races with the original-bootstrap-emit.
Option (b) (resolve-on-hasDocument-true) is simpler, narrower,
and was explicitly recommended by REVIEW.md.
- WR-03: alternative considered: keeping Date.now() and adding a
microsecond-resolution offset via performance.now() — fragile
across SW respawns where performance.now() resets. Module-level
monotonic counter has zero wall-clock dependency.
- WR-09: alternative considered: forcing a synchronous rotation
at REQUEST_BUFFER time. Rejected — adds ~50–200 ms latency to
every save AND races with scheduleRotation()'s timer. The
"exclude unless empty" trade-off matches REVIEW.md option (a)
exactly and preserves the documented first-10-s UX path.
Validation evidence:
- npx tsc --noEmit: exit 0 (no type errors).
- npx vitest run --reporter=dot: 30/30 tests pass in 2.67 s
(8 test files, including port.test.ts which pins the reconnect
invariant and port-serialization.test.ts which pins the wire format).
- grep "as any\|@ts-ignore" src/offscreen/ src/background/index.ts
src/shared/: no matches (type-safety gate stays clean).
|
|||
| f81438d6c8 |
feat(fix-a3): rename TransferredVideoChunk → TransferredVideoSegment
Pure rename pass — zero behavioural change at any call site. The
prior "chunk" naming is a vestige of D-09..D-11's chunk-level
buffer; under D-13 the unit of transfer is a self-contained ~10 s
WebM segment, so the type name now matches the data shape.
Renames propagated atomically:
- src/shared/types.ts
* TransferredVideoChunk → TransferredVideoSegment
(also: the `isFirst?: boolean` field is dropped — D-13 segments
are all implicitly their own header, so the flag is meaningless
and only existed for the retired ring-buffer's pin semantics)
* VideoChunk → VideoSegment (drops `isFirst?` for the same reason)
* PortMessage.chunks? → PortMessage.segments?
* VideoBufferResponse.chunks → VideoBufferResponse.segments
- src/offscreen/recorder.ts
* import type rename
* encodeAndSendBuffer's per-element accumulator + filter type
* the port.postMessage payload field
- src/background/index.ts
* import type rename
* getVideoBufferFromOffscreen reads `(msg as {segments?}).segments`
(matching the new wire field name)
* empty-buffer returns `{ segments: [] }`
* mergeVideoSegments signature takes VideoSegment[]
* createArchive consumes videoBufferResponse.segments
* saveArchive log line says "segments"
Verification:
- npx tsc --noEmit clean.
- npx vitest run → 28 passed / 2 failed (the 2 fixture-empirical
ffmpeg dry-runs; unchanged — they're gated on ./smoke.sh regen).
- npm run build succeeds, all 60 modules transformed.
- grep predicates clean in src/:
* no addChunk / trimAged / firstChunkSaved / isFirst
* no TransferredVideoChunk / VideoChunk (old names)
* 21 occurrences of new names propagated correctly
- Pre-existing port-serialization.test.ts still GREEN: its `isFirst`
references are inside inline-test-scoped objects (not imports
from production types), and its tests assert JSON-roundtrip
behaviour rather than the production type shape.
|
|||
| 670daa3fe8 |
feat(fix-a3): adapt SW receive path to segment semantics
Follow-up to the D-13 activation in src/offscreen/recorder.ts. Each entry on the BUFFER port message is now a self-contained WebM segment (not a partial chunk), so the SW-side concat is trivial: sort by timestamp and Blob-concatenate. The resulting multi-EBML- header file plays natively in Chrome (SPEC §10 #7 scope). Changes in src/background/index.ts: - mergeVideoChunks renamed to mergeVideoSegments; doc comment and log lines say "segment" throughout. No behavioural change beyond removing the now-stale per-chunk `isFirst` logging. - getVideoBufferFromOffscreen no longer reads or carries the `isFirst` field when decoding wire payload into VideoChunk — D-13's segment lifecycle makes the flag meaningless (every segment is a fresh recorder boundary, every segment's first chunk is implicitly the header). The field stays optional on VideoChunk for one more commit; commit 4 sweeps the type rename and drops it. - The single mergeVideoChunks call site in createArchive updated to the new name. Verification: - npx vitest run → 28 passed / 2 failed (same 2 empirical-ffmpeg REDs from webm-playback.test.ts; unchanged from prior commit — the fixture is still the stale single-continuous-recorder one). - npx tsc --noEmit clean. - src/background/index.ts now has zero references to addChunk / trimAged / firstChunkSaved / isFirst. |
|||
| d5bb948d95 |
feat(fix-d12): decode chunks from base64 in SW BUFFER receive
- Read incoming port.chunks as TransferredVideoChunk[] (was VideoChunk[] — but that was a lie because Blob doesn't survive JSON serialization across the port boundary). - Decode each wire chunk via base64ToBlob(wire.data, wire.type) and resolve VideoBufferResponse with the resulting VideoChunk[]. The existing mergeVideoChunks downstream sees real Blobs and produces a real WebM-prefixed merged blob. - Defensive per-chunk decode: log + skip individual decode failures rather than blowing up the whole fetch. Falls back to video/webm;codecs=vp9 if the wire chunk somehow omits the type (defense-in-depth — the offscreen always populates it). - Document the 2 s BUFFER_FETCH_TIMEOUT_MS budget: covers worst-case encode + post-message + JSON parse with > 1.5 s of headroom for the current 15-chunk × 100 KB sizing. Refs: debug session d12-blob-port-transfer-fails, D-17 port lifecycle. |
|||
| 6aeeda495c |
fix(01-06): align ensureOffscreen URL with crxjs emit path
After collapsing vite.config.ts to use rollupOptions.input.offscreen = 'src/offscreen/index.html', crxjs preserves the 'src/' prefix in the bundled output (Outcome A per RESEARCH.md Pitfall 5 dichotomy): dist/src/offscreen/index.html (NOT dist/offscreen/index.html) The pre-amendment leftover string 'offscreen/index.html' at src/background/index.ts:45 would have produced ERR_FILE_NOT_FOUND in chrome.offscreen.createDocument and broken Plan 07's manual smoke load. Updated to match the actual emit path. - npm run build exits 0; 7 dist/assets/*.js bundles produced - dist/manifest.json permissions: desktopCapture present, tabCapture absent - tsc --noEmit clean; 9/9 vitest tests still green - ensureOffscreen URL string now matches dist/src/offscreen/index.html Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 5cd1519858 |
feat(01-05): wire SW-side port host and port-based buffer fetch
Plan 05 Task 2 — make the SW a pure coordinator that talks to the offscreen
via the long-lived 'video-keepalive' port (D-17, RESEARCH.md Pattern 5).
Additions:
- chrome.runtime.onConnect.addListener handler scoped to port name
'video-keepalive' + T-1-04 mitigation (port.sender?.id check). Stores port
in module-level videoPort: chrome.runtime.Port | null.
- getVideoBufferFromOffscreen(): port-based REQUEST_BUFFER round-trip with
a 2s timeout fallback to { chunks: [] }. Replaces the synchronous SW-local
getVideoBuffer() stub from Task 1.
- offscreenReady Promise + OFFSCREEN_READY onMessage case (RESEARCH.md
Pattern 4): startVideoCapture awaits this before sending START_RECORDING,
closing the 'Receiving end does not exist' race window (audit P1 #12).
- onMessage GET_VIDEO_BUFFER + SAVE_ARCHIVE rewritten to fetch the buffer
via the port instead of the deleted local array.
- onMessage sender.id !== chrome.runtime.id guard at handler top
(T-1-NEW-05-01 mitigation).
- chrome.runtime.onInstalled now calls indexedDB.deleteDatabase('VideoRecorderDB')
once to clean up the orphaned database from pre-Phase-01 builds
(T-1-NEW-05-02 / RESEARCH.md Runtime State Inventory).
Rule 2 deviation (orchestrator-flagged robustness):
- initialize() now calls chrome.offscreen.hasDocument() to detect existing
offscreen documents across SW respawns and update offscreenCreated
accordingly (audit P1 #8). Guarded with a typeof check to stay safe under
partial chrome stubs.
Verified: npx tsc --noEmit clean; npx vitest run 9/9 green (Plan 04
offscreen-side tests stay un-touched); no as any / @ts-ignore.
|
|||
| 886376e789 |
refactor(01-05): delete legacy SW buffer, alarms, IndexedDB, tabCapture paths
Plan 05 Task 1 — finish the SW shrink:
- DELETE videoBuffer: VideoChunk[] module state (buffer lives in offscreen per D-16)
- DELETE setupKeepalive + chrome.alarms registration (D-18; alarms never reset SW idle timer — port does)
- DELETE chrome.tabCapture.getMediaStreamId call (D-01: getDisplayMedia now runs inside offscreen)
- DELETE chrome.permissions.contains/request for tabCapture (broken — desktopCapture is the new manifest entry, but getDisplayMedia needs no runtime perm)
- DELETE comment-only references to removed symbols (so grep gates pass)
- REPLACE 'USER_MEDIA' as any → chrome.offscreen.Reason.DISPLAY_MEDIA (D-02; @types/chrome 0.0.268 exposes it)
- REPLACE justification copy to match RESEARCH.md Example C
- FIX (error as any) → instanceof Error pattern (CLAUDE.md rule)
- FIX chrome.tabs.sendMessage cast: explicit response type instead of 'as any'
- COLLAPSE REQUEST_PERMISSIONS handler: under getDisplayMedia, no runtime perm check is meaningful — just call startVideoCapture() (Rule 1 deviation; old code returned granted=false because tabCapture is no longer in manifest)
- Temporary stub: getVideoBuffer() returns { chunks: [] } — Task 2 deletes this and wires the port-based getVideoBufferFromOffscreen()
Verified: npx tsc --noEmit clean, npx vitest run 9/9 green, no as any / @ts-ignore.
|
|||
| c5828d38ef |
feat(01-03): add OffscreenLogger and clean up shared types
- Add PortMessageType and PortMessage interface to src/shared/types.ts for the long-lived port (offscreen ↔ SW; D-17 / Plan 04 wires the ping loop + REQUEST_BUFFER / BUFFER traffic). - Remove 'VIDEO_CHUNK' and 'VIDEO_CHUNK_SAVED' from MessageType union (per D-19 — chunks no longer travel via chrome.runtime.sendMessage; the IndexedDB SW-side plumbing is the audit P0 #2 broken path). - OffscreenLogger class was already added alongside Task 2 because recorder.ts imports it at module top. Inline SW cleanup (Rule 3 — blocking dependency, plan acceptance gates on `npx tsc --noEmit` exit 0): - Remove src/background/index.ts VIDEO_CHUNK + VIDEO_CHUNK_SAVED case branches (refs to deleted union members). - Remove now-unreferenced loadChunkFromIndexedDB / openIndexedDB (only reachable from the deleted VIDEO_CHUNK_SAVED branch). - Remove now-unreferenced addVideoChunkFromBlob / cleanupVideoBuffer / firstChunkSaved / VIDEO_BUFFER_DURATION_MS constant (the SW-side ring buffer now lives in src/offscreen/recorder.ts per D-16). - Keep SW-side `videoBuffer: VideoChunk[] = []` as a placeholder; Plan 04 wires it to fetch from offscreen over the keepalive port. The remaining `getVideoBuffer` + `saveArchive` callers continue to compile against the empty array until Plan 04 lands. - Plan 05 owns the broader SW shell cleanup. Verification (post-commit): - npx vitest run tests/offscreen/ring-buffer.test.ts tests/offscreen/codec-check.test.ts → 6/6 PASS - npx tsc --noEmit → exit 0 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 555eb0543f |
chore: import broken Phase-1 extension as received
Snapshot of /home/parf/Downloads/manifest.zip as delivered, before any
GSD-driven remediation. Contains a partially-broken first attempt at the
Russian SPEC "Тз расширение фаза1.md" (Phase 1 of operator-session-recorder).
Source layout:
- manifest.json — MV3 declaration with tabCapture/activeTab/downloads/etc.
- src/background/index.ts — service worker (video buffer + archive packaging)
- src/content/index.ts — rrweb + user-event logger
- src/popup/{index.html,index.ts,style.css} — Russian popup UI
- offscreen/{index.html,index.ts} — orphaned offscreen (see audit)
- vite.config.ts — inline plugin emitting a separate live offscreen.js
- generate-icons.js, icons/ — minimal PNG icons
- "Тз расширение фаза1.md" — authoritative Russian SPEC
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|