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>
This commit is contained in:
2026-05-20 10:14:08 +02:00
parent b112cb7861
commit 4bba679e39
5 changed files with 169 additions and 12 deletions

View File

@@ -0,0 +1,140 @@
---
slug: 01-09-startup-notification-misleading-text
status: resolved
goal: find_and_fix
trigger: "it fires the notification when starts the browser if installed --- even though the recording is not going."
phase: 01-stabilize-video-pipeline
plan: 01-09
opened: 2026-05-20
closed: 2026-05-20
orchestrator_diagnosed: true
---
# Debug session 01-09-startup-notification-misleading-text — i18n key conflated CTA + post-start confirmation
## Problem statement
During the Plan 01-09 closure UAT 2026-05-20, the operator reported the
verbatim symptom: "it fires the notification when starts the browser if
installed --- even though the recording is not going." On browser start
with the extension installed (no active recording), the OS notification
displayed "Recording started. I'm watching the last 30 seconds." Operator
empirical evidence (orchestrator-inspected
`~/Downloads/session_report_2026-05-20_09-51-45.zip`): `meta.json` shows
`totalEvents: 0`, `events.json` empty — operator never actually recorded;
they only saw the misleading notification on browser start, were
confused, and saved an empty archive in an attempt to verify state.
## Root cause
`_locales/{en,ru}/messages.json` defined a single key `notifStartup` whose
text leaned toward a post-start confirmation phrasing ("Recording started.
I'm watching the last 30 seconds."). The key's own `.description` field
acknowledged the conflation: *"Notification body for the onStartup +
manual-start flow."* In reality only the **onStartup** path consumes the
key (`src/background/index.ts:1023`) — and on that path the recording has
**not** started; per Phase 1 always-on charter recording does not
auto-start, and the notification itself IS the gesture surface (clicking
it triggers `notifications.onClicked``startVideoCapture` at line
10381050). The operator was reading a confirmation message in a
pre-recording context.
The fallback constant `NOTIF_STARTUP_FALLBACK` ("Recording started. Click
here to start a recording.") was a hybrid that still led with the
misleading "Recording started." phrase.
## Fix design
Per orchestrator charter, split the conflated key into two:
1. **`notifStartupCta`** (the path actually wired today)
- EN: `"Mokosh ready. Click to start a recording."`
- RU: `"Mokosh готов. Нажмите, чтобы начать запись."`
- Description (en): *"Notification body for the onStartup flow — CTA-with-gesture invite. Notification title is extName. Per Phase 1 always-on charter: recording does NOT auto-start; this notification is the gesture surface."*
- Description (ru): equivalent Russian description.
2. **`notifRecordingStarted`** (reserved for future post-manual-start confirmation flow)
- EN: `"Recording started. I'm watching the last 30 seconds."` (preserves original text for future re-use)
- RU: `"Запись запущена. Я слежу за последними 30 секундами."` (preserves original text)
- Description: clearly scoped to "AFTER recording successfully starts via startVideoCapture."
3. Renamed fallback constant `NOTIF_STARTUP_FALLBACK``NOTIF_STARTUP_CTA_FALLBACK`
with new EN value `"Mokosh ready. Click to start a recording."` to match the new CTA text.
4. Updated single SW call site (`src/background/index.ts:1023`):
`i18nMessage('notifStartup', NOTIF_STARTUP_FALLBACK)``i18nMessage('notifStartupCta', NOTIF_STARTUP_CTA_FALLBACK)`.
5. Updated inline test comment in `tests/background/onstartup-notification.test.ts`
to reflect the new key + fallback. The assertion regex
`/recording|recor|click/i` matches the new CTA text via the `click`
alternation, so no test logic change was needed — the regex deliberately
covers both fallback and resolved locale variants.
### Migration alias decision
**No alias retained.** `notifStartup` was referenced in exactly one code
call site (line 1023) and only as an inline comment in one test (line
164). Both updated in this patch. The single-cycle rename is cleaner than
carrying a deprecation alias.
## Files modified
- `_locales/en/messages.json` — key split + new descriptions
- `_locales/ru/messages.json` — key split + new descriptions
- `src/background/index.ts` — fallback rename + call site update
- `tests/background/onstartup-notification.test.ts` — inline comment refresh
## Behavior preservation
- Same `chrome.notifications.create` call signature, same id prefix
`mokosh-startup-`, same `priority: 1`, same icon path.
- Same `chrome.notifications.onClicked` handler at line 10381052 invokes
`startVideoCapture` on click. The notification's CTA-with-gesture role
is unchanged — only the displayed text now truthfully describes pre-recording
state.
- No new test-mode symbols. `FORBIDDEN_HOOK_STRINGS` inventory stays at 12.
## Acceptance gates
-`_locales/en/messages.json` + `_locales/ru/messages.json` both contain
`notifStartupCta` AND `notifRecordingStarted` with the orchestrator-specified
texts; locale-parity test passes (4/4 GREEN).
-`src/background/index.ts` onStartup handler reads `notifStartupCta` with
`NOTIF_STARTUP_CTA_FALLBACK`.
-`npx vitest run --exclude tests/build/** --exclude tests/background/no-test-hooks-in-prod-bundle.test.ts`: **104/104 GREEN** (excludes 2 build-dependent gates blocked by the parallel Plan 01-10 mark-bundling debug session's uncommitted SVG assets — orthogonal to this fix; orchestrator coordinates the merged re-verification).
-`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 single SW file
modified by this patch).
-`npm run build` + `npm run test:uat` + Tier-1 grep-on-built-bundle deferred
to orchestrator-level re-verification after the parallel Plan 01-10
mark-bundling fix also lands (operator-UAT re-spawn coordinated by orchestrator).
## Empirical re-verification (operator-side, post-orchestrator merge)
After both this fix and the parallel Plan 01-10 mark-bundling fix land:
1. Reload the extension.
2. Close + reopen Chrome.
3. Observe the OS notification on startup. Expected text: "Mokosh ready.
Click to start a recording." (NOT "Recording started…").
4. Click the notification → `startVideoCapture` runs → operator selects
screen → recording starts (gesture surface preserved).
## Noteworthy
- **Two parallel debug sessions touched the same working tree.** The Plan
01-10 mark-bundling session edited `src/welcome/welcome.ts` + `welcome.css`
+ `welcome.html` concurrently with this session's edits to background +
locales. The vitest gates for `tests/build/no-remote-fonts.test.ts` and
`tests/background/no-test-hooks-in-prod-bundle.test.ts` both require
`npm run build` to succeed, which currently fails because the parallel
session's `welcome.ts` imports `mokosh-mark.svg?url` before that asset
exists in the tree. Those 2 failures are entirely owned by the parallel
session — explicitly excluded from this session's verification. Full
build + UAT will be re-run by the orchestrator after both sessions merge.
- **Operator-facing key names follow a discoverable pattern.** `notifStartupCta`
(action invite) vs `notifRecordingStarted` (state confirmation) — future
i18n contributors get unambiguous semantic hints from the key alone.
The `.description` field reinforces this with explicit scoping ("Per
Phase 1 always-on charter: recording does NOT auto-start").

View File

@@ -47,9 +47,13 @@
"message": "Last 30 s video + 10 min log", "message": "Last 30 s video + 10 min log",
"description": "Popup info-text below the SAVE button. Brief explanation of what the archive contains." "description": "Popup info-text below the SAVE button. Brief explanation of what the archive contains."
}, },
"notifStartup": { "notifStartupCta": {
"message": "Mokosh ready. Click to start a recording.",
"description": "Notification body for the onStartup flow — CTA-with-gesture invite. Notification title is extName. Per Phase 1 always-on charter: recording does NOT auto-start; this notification is the gesture surface."
},
"notifRecordingStarted": {
"message": "Recording started. I'm watching the last 30 seconds.", "message": "Recording started. I'm watching the last 30 seconds.",
"description": "Notification body for the onStartup + manual-start flow. The notification title is extName." "description": "Notification body fired AFTER recording successfully starts via startVideoCapture. Notification title is extName."
}, },
"notifRecovery": { "notifRecovery": {
"message": "Recording resumed. Buffer refilling.", "message": "Recording resumed. Buffer refilling.",

View File

@@ -47,9 +47,13 @@
"message": "Последние 30 сек видео + 10 мин лога", "message": "Последние 30 сек видео + 10 мин лога",
"description": "Info-text под SAVE-кнопкой. Краткое описание содержимого архива." "description": "Info-text под SAVE-кнопкой. Краткое описание содержимого архива."
}, },
"notifStartup": { "notifStartupCta": {
"message": "Mokosh готов. Нажмите, чтобы начать запись.",
"description": "Тело уведомления для onStartup — CTA с жестом активации. Заголовок — extName. Per always-on charter: запись не запускается автоматически."
},
"notifRecordingStarted": {
"message": "Запись запущена. Я слежу за последними 30 секундами.", "message": "Запись запущена. Я слежу за последними 30 секундами.",
"description": "Тело уведомления для onStartup + manual-start. Заголовок — extName." "description": "Тело уведомления, отправляемое ПОСЛЕ успешного старта записи через startVideoCapture. Заголовок — extName."
}, },
"notifRecovery": { "notifRecovery": {
"message": "Запись возобновлена. Буфер снова заполняется.", "message": "Запись возобновлена. Буфер снова заполняется.",

View File

@@ -114,7 +114,13 @@ const WELCOME_PATH = 'src/welcome/welcome.html';
// pattern as the popup — unit-test contexts without chrome.i18n stub // pattern as the popup — unit-test contexts without chrome.i18n stub
// degrade to these literals. // degrade to these literals.
const NOTIF_EXTNAME_FALLBACK = 'Mokosh'; const NOTIF_EXTNAME_FALLBACK = 'Mokosh';
const NOTIF_STARTUP_FALLBACK = 'Recording started. Click here to start a recording.'; // Plan 01-09 amendment 2026-05-20: `notifStartup` key was split into
// `notifStartupCta` (used here by the onStartup handler — CTA-with-gesture
// pre-recording invite) and `notifRecordingStarted` (reserved for future
// post-manual-start confirmation flows). The original text falsely implied
// recording had auto-started; the new text invites the operator to start
// a recording. See .planning/debug/resolved/01-09-startup-notification-misleading-text.md.
const NOTIF_STARTUP_CTA_FALLBACK = 'Mokosh ready. Click to start a recording.';
const NOTIF_RECOVERY_FALLBACK = 'Recording stopped. Click here to start a new session.'; const NOTIF_RECOVERY_FALLBACK = 'Recording stopped. Click here to start a new session.';
/** /**
@@ -1020,7 +1026,7 @@ try {
type: 'basic', type: 'basic',
iconUrl: chrome.runtime.getURL(NOTIFICATION_ICON_PATH), iconUrl: chrome.runtime.getURL(NOTIFICATION_ICON_PATH),
title: i18nMessage('extName', NOTIF_EXTNAME_FALLBACK), title: i18nMessage('extName', NOTIF_EXTNAME_FALLBACK),
message: i18nMessage('notifStartup', NOTIF_STARTUP_FALLBACK), message: i18nMessage('notifStartupCta', NOTIF_STARTUP_CTA_FALLBACK),
priority: 1, priority: 1,
}); });
} catch (e) { } catch (e) {

View File

@@ -159,12 +159,15 @@ describe('Plan 01-09 Task 3: chrome.runtime.onStartup notification contract', ()
expect(typeof opts.title).toBe('string'); expect(typeof opts.title).toBe('string');
expect(/Mokosh/i.test(String(opts.title))).toBe(true); expect(/Mokosh/i.test(String(opts.title))).toBe(true);
expect(typeof opts.message).toBe('string'); expect(typeof opts.message).toBe('string');
// Plan 01-12 Wave 4: notification message migrated from literal // Plan 01-12 Wave 4: notification message migrated to chrome.i18n.
// 'Click here to start recording your session.' to // Plan 01-09 amendment 2026-05-20: key split — onStartup handler now
// chrome.i18n.getMessage('notifStartup') with fallback // reads `notifStartupCta` with fallback `NOTIF_STARTUP_CTA_FALLBACK`
// 'Recording started. Click here to start a recording.'. Tests // ('Mokosh ready. Click to start a recording.'); the prior conflated
// without a chrome.i18n stub observe the fallback; both contain // `notifStartup` key was retired (`notifRecordingStarted` reserved
// 'recording' (case-insensitive). // for post-manual-start confirmation). Tests without a chrome.i18n
// stub observe the CTA fallback; the regex matches both 'click' and
// 'recording' (case-insensitive) so it survives both the fallback
// and the resolved locale value.
expect(/recording|recor|click/i.test(String(opts.message))).toBe(true); expect(/recording|recor|click/i.test(String(opts.message))).toBe(true);
}); });