Files
Jarvis/packages/Shell/Tests
james.maes cd3b62f672 fix(01-CR-03): detect silent nil global hotkey monitor + wire bind-failed banner
Phase 1 REVIEW.md CR-03 (SHELL-06): `HotkeyBinder.bind` never checked
whether `store.installGlobalMonitor` returned nil — yet
`NSEvent.addGlobalMonitorForEvents` silently returns nil when Input
Monitoring was revoked between launches (common after a codesign
identity change). The result was the exact silent-no-op SHELL-06
exists to prevent: hotkey appears bound, global monitor is dead, no
banner surfaces. Related WR-06: `BannerContent.hotkeyBindFailed`
existed but was never enqueued from production code.

packages/Shell/Sources/Shell/HotkeyBinder.swift
- Add `HotkeyBinder.BannerSink` protocol (mirrors
  `InputMonitoringProbe.BannerSink` pattern).
- `bind(_:inputMonitoringGranted:bindFailedSink:onPress:)` now checks
  the returned token: if nil, sets `isDegraded = true` AND invokes
  `sink.enqueueHotkeyBindFailed()`. Local monitor install is still
  unconditional so the hotkey works while Jarvis is frontmost.

packages/Shell/Tests/ShellTests/HotkeyBindingTests.swift
- Add `DeniedGlobalMonitorStore` fake (returns nil for global install).
- Add `RecordingBindFailedSink` that counts enqueue calls.
- New tests:
  - `test_nilGlobalTokenMarksDegradedAndNotifiesSink` — SHELL-06
    regression guard for the silent-revoke path.
  - `test_deniedInputMonitoringDoesNotEnqueueBindFailed` — asserts the
    priority-2 inputMonitoringDenied banner is not mixed with the
    priority-3 hotkeyBindFailed banner.
  - `test_successfulBindDoesNotEnqueueBindFailed` — no spurious banner
    on the happy path.

App/AppDelegate.swift:233-260
- Add `HotkeyBindFailedSink` adapter (mirrors `AdHocBannerSink`
  pattern) so Shell can enqueue `BannerContent.hotkeyBindFailed` onto
  the `HUDBannerCoordinator` without importing App types.
- `bindHotkeyFromWizard()` now passes the sink into `hotkeyBinder.bind`
  — fixes WR-06 (hotkeyBindFailed preset no longer orphaned).
2026-04-23 16:13:11 -05:00
..