fix(desktop): gate CLI test-notification observers to local dev builds

Merged
AR
2mo ago
+115-1

Summary

ProactiveAssistantsPlugin.setupTestNotificationListeners() registered the com.omi.test.notification and com.omi.test.insight DistributedNotificationCenter observers unconditionally, so shipped Omi.app and Omi Beta.app accepted them too.

DistributedNotificationCenter is a machine-wide bus with no sender authentication. Any local process could therefore deliver an arbitrary proactive notification — floating-bar card, optionally a system banner via deliverSystemBanner=true — and have it journaled into the signed-in user's real backend chat as an Omi message. That is how #11943 was found: a dev-bundle test run drove the running production Omi Beta and wrote 13 test notifications into a real account.

Fixes #11943.

Why allowsLocalAutomation, not isNonProduction

The issue suggested AppBuild.isNonProduction. allowsLocalAutomation is the correct predicate: external preview bundles (com.omi.preview.*) ship to users outside the team and are non-production, so isNonProduction would have left them injectable. allowsLocalAutomation is also the predicate DesktopAutomationBridge already uses for its debug surface, so the CLI trigger surface now matches the automation surface it belongs to.

The guard sits inside setupTestNotificationListeners() rather than at the call site, so any future caller inherits it.

Verification

Observer registration, counted from each bundle's own log at plugin init:

Bundle identityCodeRegistered
com.omi.computer-macos (real shipped Omi.app)unpatched2 ← the defect
com.omi.preview.p11943 (external preview)unpatched2
com.omi.preview.p11943 (external preview)patched0
com.omi.omi-11943-dev (local development)patched2 ← CLI still works

End-to-end delivery against the patched preview build, with a real trigger posted from a separate process. An independent observer process on the same bus witnessed the notification, so "the app logged no receipt" cannot be an unnoticed post failure:

witness process            -> received com.omi.test.insight (count=1)
patched app receipt count  -> 0
patched app plugin init    -> 1

The patched run still logs ProactiveAssistantsPlugin initialized, so the same code path is reached and the guard — not an unrelated early exit — is what suppresses registration.

Reaching a preview identity locally required a temporary uncommitted edit to scripts/app-config.sh, which otherwise forces every local bundle to an omi- development id. That edit is reverted and is not in this diff.

Tests

  • desktop/macos/scripts/swift-test-suites.sh — 660 suites in isolation. ChatTranscriptGestureHarnessTests failed only under 4-worker parallel load on a timing threshold (154.0 pt drift against a 120.0 pt bound); it passes alone (dev-feedback.py --once swift 'ChatTranscriptGestureHarnessTests' → 17 tests, 0 failures) and touches no code in this diff.
  • dev-feedback.py --once swift 'ProactiveTestTriggerGateTests' → 5 tests, 0 failures.
  • python3 desktop/macos/scripts/check_desktop_test_quality.py → at baseline (54 source-reading files / 147 sites).

The three predicate tests carry the behavioral half — production family, external preview, and local development identities. testRegistrationSiteKeepsTheGate is a labelled source-inspection static contract: the defect was an absent guard in a private method on a MainActor plugin that cannot be instantiated headlessly, and AppBuild reads Bundle.main, so no behavioral seam observes the registration decision. testGateDetectorRejectsUngatedSource proves that scan still discriminates, using fixtures rather than removing the real guard to test it.

Line-Count-Exception: desktop/macos/Desktop/Sources/ProactiveAssistants/ProactiveAssistantsPlugin.swift | 1798 -> 1808 | +1 guard line and a comment recording why this is allowsLocalAutomation rather than isNonProduction, and why an unauthenticated machine-wide bus makes the distinction load-bearing; splitting a 1798-line plugin is out of scope for a security fix

Failure-Class: none