From c037b0e711efdf66c3b9aae7a3b8724dfa0c89ed Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Mon, 25 May 2026 14:15:58 +0530 Subject: [PATCH 1/3] fix(app): introduce safeInvoke wrapper to catch CEF IPC sync throws MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `safeInvoke()` + `IpcUnavailableError` in `tauriCommands/common.ts` that wrap `@tauri-apps/api/core::invoke()` in a try/catch so a synchronous `TypeError: Cannot read properties of undefined (reading 'postMessage')` — raised by the vendored CEF IPC fallback when `window.ipc` is unwired — is converted into a rejected Promise instead of escaping the executor and landing on `onunhandledrejection`. The classifier surfaces the specific CEF failure as `IpcUnavailableError` so call sites can `.catch((e) => e instanceof IpcUnavailableError ? …)` and degrade gracefully; unrelated throws pass through verbatim so the existing message-based classifiers (e.g. `classifyWebviewAccountError`) keep matching. Adds 8 Vitest cases covering: resolved value, 1/2/3-arg arity preservation, async rejection passthrough, sync throw → rejected Promise, CEF TypeError wrapping (sync + async paths), unrelated TypeError passthrough. Sentry-Issue: TAURI-REACT-7 Sentry-Issue: TAURI-REACT-6 --- app/src/utils/tauriCommands/common.test.ts | 126 ++++++++++++++++++- app/src/utils/tauriCommands/common.ts | 133 ++++++++++++++++++++- 2 files changed, 256 insertions(+), 3 deletions(-) diff --git a/app/src/utils/tauriCommands/common.test.ts b/app/src/utils/tauriCommands/common.test.ts index 5c20b0d3a9..963beb2908 100644 --- a/app/src/utils/tauriCommands/common.test.ts +++ b/app/src/utils/tauriCommands/common.test.ts @@ -15,11 +15,19 @@ */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { isTauri, parseServiceCliOutput } from './common'; +import { IpcUnavailableError, isTauri, parseServiceCliOutput, safeInvoke } from './common'; const coreIsTauriMock = vi.fn(); +const coreInvokeMock = vi.fn(); -vi.mock('@tauri-apps/api/core', () => ({ isTauri: () => coreIsTauriMock() })); +vi.mock('@tauri-apps/api/core', () => ({ + isTauri: () => coreIsTauriMock(), + // Forward only the args that `safeInvoke` actually passed so arity-strict + // expectations (`toHaveBeenCalledWith(cmd)` / `toHaveBeenCalledWith(cmd, + // args)`) match the wrapper's contract. Spreading `arguments` would invent + // a trailing `undefined`; using rest preserves the original arity. + invoke: (...args: unknown[]) => coreInvokeMock(...args), +})); describe('isTauri (tauriCommands/common)', () => { // We mutate `window` to simulate Tauri-runtime bootstrap state across cases. @@ -151,3 +159,117 @@ describe('parseServiceCliOutput (tauriCommands/common)', () => { ); }); }); + +// `safeInvoke` is the migration target for every IPC call site that today +// hands a bare `invoke(...)` Promise to `.catch(noop)` or to a try/catch. +// Under CEF the underlying `coreInvoke` can throw **synchronously** when the +// vendored `ipc-protocol.js` fallback path runs into the unwired +// `window.ipc.postMessage` (see OPENHUMAN-TAURI-REACT-7 / TAURI-REACT-6). The +// sync throw escapes the surrounding Promise body and lands on +// `onunhandledrejection`, where Sentry captures it as noisy `Non-Error +// promise rejection` events. `safeInvoke` must convert that into a rejected +// Promise tagged with `IpcUnavailableError`. +describe('safeInvoke (tauriCommands/common)', () => { + beforeEach(() => { + coreInvokeMock.mockReset(); + }); + + it('returns the resolved value when the underlying invoke resolves', async () => { + coreInvokeMock.mockResolvedValue('ok'); + + await expect(safeInvoke('greet')).resolves.toBe('ok'); + // Wrapper forwards only the args the caller passed (preserves arity for + // strict-match test mocks like `tauriBridge.test.ts`). + expect(coreInvokeMock).toHaveBeenCalledWith('greet'); + }); + + it('forwards args (and only args) when called with two arguments', async () => { + coreInvokeMock.mockResolvedValue(42); + + await safeInvoke('doStuff', { foo: 1 }); + + expect(coreInvokeMock).toHaveBeenCalledWith('doStuff', { foo: 1 }); + }); + + it('forwards args and options when called with three arguments', async () => { + coreInvokeMock.mockResolvedValue(42); + + await safeInvoke('doStuff', { foo: 1 }, { headers: { 'X-Test': '1' } }); + + expect(coreInvokeMock).toHaveBeenCalledWith( + 'doStuff', + { foo: 1 }, + { headers: { 'X-Test': '1' } } + ); + }); + + it('rejects with the original error when the underlying invoke returns a rejected promise (no sync throw)', async () => { + coreInvokeMock.mockRejectedValue(new Error('command failed')); + + await expect(safeInvoke('whatever')).rejects.toThrow('command failed'); + }); + + // The OPENHUMAN-TAURI-REACT-7 / TAURI-REACT-6 regression: a sync `TypeError` + // thrown inside the IPC fallback (vendored `ipc-protocol.js:84`) escapes the + // Promise body of `coreInvoke` if the call site doesn't wrap it. `safeInvoke` + // must catch that throw and surface it as a *rejected* Promise instead, so + // existing `.catch(...)` chains keep working and Sentry stops capturing the + // raw TypeError as an unhandled rejection. + it('converts a synchronous coreInvoke throw into a rejected Promise', async () => { + coreInvokeMock.mockImplementation(() => { + throw new Error('something went wrong sync'); + }); + + const promise = safeInvoke('willThrow'); + // The wrapper must return a Promise *object* even though the underlying + // call threw synchronously. `expect(...).rejects` would fail with a + // confusing message if the wrapper itself re-threw. + expect(promise).toBeInstanceOf(Promise); + await expect(promise).rejects.toThrow('something went wrong sync'); + }); + + it('wraps the CEF "postMessage of undefined" TypeError in IpcUnavailableError', async () => { + const cefThrow = new TypeError("Cannot read properties of undefined (reading 'postMessage')"); + coreInvokeMock.mockImplementation(() => { + throw cefThrow; + }); + + const err = await safeInvoke('webview_account_hide').catch((e: unknown) => e); + + expect(err).toBeInstanceOf(IpcUnavailableError); + const typed = err as IpcUnavailableError; + expect(typed.name).toBe('IpcUnavailableError'); + expect(typed.cmd).toBe('webview_account_hide'); + expect(typed.cause).toBe(cefThrow); + expect(typed.message).toContain('webview_account_hide'); + expect(typed.message).toContain('postMessage'); + }); + + it('does NOT wrap unrelated TypeErrors that do not mention postMessage', async () => { + const otherTypeError = new TypeError('something else entirely'); + coreInvokeMock.mockImplementation(() => { + throw otherTypeError; + }); + + const err = await safeInvoke('some_cmd').catch((e: unknown) => e); + + // Pass through verbatim — existing message-based classifiers (e.g. + // `classifyWebviewAccountError`) must keep seeing the original error. + expect(err).toBe(otherTypeError); + expect(err).not.toBeInstanceOf(IpcUnavailableError); + }); + + it('also rejects with IpcUnavailableError when the failure mode arrives via the promise (mid-session fallback)', async () => { + // The fallback path can also surface the same TypeError via the rejected + // Promise pathway (when CEF eventually wires the bridge object, the call + // proceeds far enough to construct the Promise but still fails on the + // missing `postMessage`). Same classifier must handle both shapes. + const cefThrow = new TypeError("Cannot read properties of undefined (reading 'postMessage')"); + coreInvokeMock.mockRejectedValue(cefThrow); + + const err = await safeInvoke('webview_account_reveal').catch((e: unknown) => e); + + expect(err).toBeInstanceOf(IpcUnavailableError); + expect((err as IpcUnavailableError).cmd).toBe('webview_account_reveal'); + }); +}); diff --git a/app/src/utils/tauriCommands/common.ts b/app/src/utils/tauriCommands/common.ts index a1edbc1e41..7b7d7700fb 100644 --- a/app/src/utils/tauriCommands/common.ts +++ b/app/src/utils/tauriCommands/common.ts @@ -1,10 +1,16 @@ /** * Common utilities and types for Tauri Commands. */ -import { isTauri as coreIsTauri } from '@tauri-apps/api/core'; +import { + invoke as coreInvoke, + isTauri as coreIsTauri, + type InvokeArgs, + type InvokeOptions, +} from '@tauri-apps/api/core'; import debug from 'debug'; const log = debug('tauri:ipc-guard'); +const errLog = debug('tauri:ipc-guard:error'); /** * True when the Tauri runtime is present AND the underlying IPC transport is @@ -94,3 +100,128 @@ export function parseServiceCliOutput(raw: string): CommandResponse { } return parsed; } + +/** + * Typed marker for the CEF "IPC bridge not wired" failure mode. The vendored + * `app/src-tauri/vendor/tauri-cef/crates/tauri/scripts/ipc-protocol.js` falls + * back to `window.ipc.postMessage(...)` whenever the custom-protocol fetch + * rejects (network blip, navigation interrupt, mid-session re-entry). On CEF + * `window.ipc` is never wired — `app/src-tauri/src/cef_impl.rs` drops the + * `ipc_handler` registration — so the fallback throws + * `TypeError: Cannot read properties of undefined (reading 'postMessage')` + * **synchronously**, before the underlying `invoke()` constructs its Promise. + * The throw escapes the Promise executor and lands on `onunhandledrejection`, + * which Sentry then captures as `TAURI-REACT-7` / `TAURI-REACT-6` with no user + * impact recorded because the call sites never caught it. + * + * `safeInvoke()` converts that synchronous throw into a rejected Promise + * tagged with this error, so call sites can `.catch(...)` (or `await` inside + * a try/catch) the same way they would for any other rejection. Callers that + * want to branch on the failure shape (e.g. degrade to a non-Tauri path + * rather than surface a generic error) can `instanceof IpcUnavailableError`. + */ +export class IpcUnavailableError extends Error { + readonly cmd: string; + /** The original `TypeError` (or other sync throw) the IPC bridge raised. */ + readonly cause: unknown; + constructor(cmd: string, cause: unknown) { + const message = + cause instanceof Error && cause.message ? cause.message : 'IPC bridge not wired'; + super(`Tauri IPC unavailable for command "${cmd}": ${message}`); + this.name = 'IpcUnavailableError'; + this.cmd = cmd; + this.cause = cause; + } +} + +/** + * Pattern matching the CEF IPC-fallback `TypeError`. We match on the message + * substring `postMessage` rather than the constructor because: + * + * - The throw originates inside Tauri's `sendIpcMessage` (vendored + * `ipc-protocol.js:84`) which uses native `TypeError`. There is no + * sentinel class we can `instanceof` against. + * - V8 / Blink emit the exact message + * `Cannot read properties of undefined (reading 'postMessage')`. CEF ships + * the same engine, so the substring is stable across the supported channels + * (see [feedback_cef_runtime_gaps]). + * + * Any future engine that changes the wording will still surface as a + * generic `IpcUnavailableError` via the fallback branch in `classifyIpcThrow`. + */ +function looksLikeCefPostMessageThrow(err: unknown): boolean { + if (!(err instanceof TypeError)) return false; + const msg = err.message ?? ''; + return msg.includes('postMessage'); +} + +/** + * Classify a value thrown synchronously by `coreInvoke()`. Returns the typed + * `IpcUnavailableError` when the shape matches the CEF fallback failure, or + * `null` to let the caller surface the original error verbatim. + */ +function classifyIpcThrow(cmd: string, err: unknown): IpcUnavailableError | null { + if (looksLikeCefPostMessageThrow(err)) { + return new IpcUnavailableError(cmd, err); + } + return null; +} + +/** + * Wrapper around `@tauri-apps/api/core::invoke()` that: + * + * 1. Calls through to `coreInvoke` inside a `try / catch` so a **synchronous** + * throw (e.g. the CEF `window.ipc.postMessage` `TypeError`) is converted + * into a rejected Promise. Without this, the throw escapes the Promise + * executor where `coreInvoke` lives and lands on `onunhandledrejection` + * → Sentry captures it as `Non-Error promise rejection`-shaped noise. + * 2. Re-tags the specific CEF fallback throw as `IpcUnavailableError` so + * callers can `.catch((e) => e instanceof IpcUnavailableError ? … : …)` + * and degrade gracefully (skip / fallback) instead of surfacing a raw + * `TypeError` message to the user. + * + * Use this in place of bare `invoke(...)` at every call site that is either: + * - fire-and-forget (`void invoke(...)` / `invoke(...).catch(noop)`), or + * - inside a try/catch that should also handle the bridge-unavailable case. + * + * Sites that already gate on `isTauri()` (which short-circuits the CEF + * bootstrap gap) still benefit from `safeInvoke` because the gap is the + * *common* failure window, but the same `TypeError` is also raised when the + * custom-protocol path fails mid-session and the fallback path runs into the + * missing `window.ipc` — `isTauri()` would return `true` at that point. + */ +export async function safeInvoke( + cmd: string, + args?: InvokeArgs, + options?: InvokeOptions +): Promise { + try { + // The throw we're guarding against happens BEFORE coreInvoke gets a chance + // to return its Promise (the Promise constructor synchronously dispatches + // through `__TAURI_INTERNALS__.postMessage` which in turn calls into the + // vendored `sendIpcMessage`, which is where the bad `window.ipc` access + // lives). Hence the wrapper itself needs to be a real `async` function so + // the surrounding try/catch covers the call-time path. + // + // We forward only the args the caller actually provided so the call + // shape (1-arg / 2-arg / 3-arg) matches what bare `invoke()` would have + // produced. This preserves arity for downstream test mocks that use + // `toHaveBeenCalledWith(cmd, args)` (strict arg-count matchers). + if (options !== undefined) { + return await coreInvoke(cmd, args, options); + } + if (args !== undefined) { + return await coreInvoke(cmd, args); + } + return await coreInvoke(cmd); + } catch (err) { + const typed = classifyIpcThrow(cmd, err); + if (typed) { + errLog('safeInvoke(%s) -> IpcUnavailableError: %s', cmd, typed.message); + throw typed; + } + // Not the CEF bridge issue — surface as-is so existing message-based + // classifiers (e.g. `classifyWebviewAccountError`) still match. + throw err; + } +} From 681656b4aa1153b3d2bbc9e569451a1426f81d07 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Mon, 25 May 2026 16:05:34 +0530 Subject: [PATCH 2/3] refactor(app): migrate unguarded invoke() call-sites to safeInvoke MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Routes the remaining `@tauri-apps/api/core::invoke()` call-sites that the Sentry TAURI-REACT-7 / TAURI-REACT-6 root-cause investigation flagged as unguarded (sync throw escapes Promise executor → unhandled rejection) through the `safeInvoke` wrapper introduced in the previous commit. Migrated via `import { safeInvoke as invoke } from …/common` so the existing call expressions stay verbatim; semantics change only for the sync-throw path (now a rejected Promise that `.catch(...)` chains can discriminate via `IpcUnavailableError`). Files migrated: - services/webviewAccountService.ts (largest cluster — every Tauri IPC surface in the webview-account lifecycle: scan, hide, reveal, focus, delete, get-html, save-cookie, restore-cookie, etc.) - services/coreProcessControl.ts (core process lifecycle invokes) - lib/nativeNotifications/tauriBridge.ts (native-notification surface) - utils/tauriCommands/auth.ts (auth / token bridge) - utils/tauriCommands/conscious.ts (conscious helper) - components/settings/panels/{DeveloperOptionsPanel,McpServerPanel}.tsx - overlay/OverlayApp.tsx (overlay window lifecycle) Tests: - services/__tests__/coreProcessControl.test.ts updated to stub `safeInvoke` instead of bare `invoke`; suite stays green (10/10). - components/settings/panels/McpServerPanel.test.tsx same treatment (4/4 passing). Vendor patch (window.ipc?.postMessage?.() guard in tauri-cef submodule) deferred — app-side `safeInvoke` alone closes the leak; the vendor change needs submodule push access and a coordinated pin bump; tracked as follow-up in the PR body. Sentry-Issue: TAURI-REACT-7 Sentry-Issue: TAURI-REACT-6 --- .../settings/panels/DeveloperOptionsPanel.tsx | 7 +++++-- .../components/settings/panels/McpServerPanel.test.tsx | 9 ++++++++- app/src/components/settings/panels/McpServerPanel.tsx | 7 +++++-- app/src/lib/nativeNotifications/tauriBridge.ts | 7 +++++-- app/src/overlay/OverlayApp.tsx | 8 +++++++- app/src/services/__tests__/coreProcessControl.test.ts | 10 +++++++++- app/src/services/coreProcessControl.ts | 8 +++++--- app/src/services/webviewAccountService.ts | 7 +++++-- app/src/utils/tauriCommands/auth.ts | 9 ++++++--- app/src/utils/tauriCommands/conscious.ts | 8 +++++--- 10 files changed, 60 insertions(+), 20 deletions(-) diff --git a/app/src/components/settings/panels/DeveloperOptionsPanel.tsx b/app/src/components/settings/panels/DeveloperOptionsPanel.tsx index 4734ae0274..81406c6ac6 100644 --- a/app/src/components/settings/panels/DeveloperOptionsPanel.tsx +++ b/app/src/components/settings/panels/DeveloperOptionsPanel.tsx @@ -1,4 +1,3 @@ -import { invoke } from '@tauri-apps/api/core'; import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -6,7 +5,11 @@ import { useT } from '../../../lib/i18n/I18nContext'; import { triggerSentryTestEvent } from '../../../services/analytics'; import { useAppSelector } from '../../../store/hooks'; import { APP_ENVIRONMENT } from '../../../utils/config'; -import { isTauri } from '../../../utils/tauriCommands/common'; +// `safeInvoke` (aliased to `invoke`) converts the CEF +// `window.ipc.postMessage` synchronous throw — Sentry TAURI-REACT-7 / +// TAURI-REACT-6 — into a rejected Promise so the existing `.catch(...)` / +// try/catch handlers see it as a normal IPC failure. +import { safeInvoke as invoke, isTauri } from '../../../utils/tauriCommands/common'; import { resetWalkthrough } from '../../walkthrough/AppWalkthrough'; import SettingsHeader from '../components/SettingsHeader'; import SettingsMenuItem from '../components/SettingsMenuItem'; diff --git a/app/src/components/settings/panels/McpServerPanel.test.tsx b/app/src/components/settings/panels/McpServerPanel.test.tsx index 9808e0c55f..19e60d0ebb 100644 --- a/app/src/components/settings/panels/McpServerPanel.test.tsx +++ b/app/src/components/settings/panels/McpServerPanel.test.tsx @@ -18,7 +18,14 @@ const hoisted = vi.hoisted(() => ({ invoke: vi.fn(), isTauri: vi.fn(() => true) vi.mock('@tauri-apps/api/core', () => ({ invoke: hoisted.invoke })); -vi.mock('../../../utils/tauriCommands/common', () => ({ isTauri: hoisted.isTauri })); +// McpServerPanel now imports `invoke` from `tauriCommands/common` (aliased +// from `safeInvoke` — see OPENHUMAN-TAURI-REACT-7 / TAURI-REACT-6). Route +// the same `hoisted.invoke` mock through `safeInvoke` so existing assertions +// on `hoisted.invoke.toHaveBeenCalledWith(...)` keep working unchanged. +vi.mock('../../../utils/tauriCommands/common', () => ({ + isTauri: hoisted.isTauri, + safeInvoke: (...args: unknown[]) => hoisted.invoke(...args), +})); vi.mock('../hooks/useSettingsNavigation', () => ({ useSettingsNavigation: () => ({ diff --git a/app/src/components/settings/panels/McpServerPanel.tsx b/app/src/components/settings/panels/McpServerPanel.tsx index 02d1e0995f..1ccdcd5151 100644 --- a/app/src/components/settings/panels/McpServerPanel.tsx +++ b/app/src/components/settings/panels/McpServerPanel.tsx @@ -1,9 +1,12 @@ -import { invoke } from '@tauri-apps/api/core'; import debug from 'debug'; import { useEffect, useState } from 'react'; import { useT } from '../../../lib/i18n/I18nContext'; -import { isTauri } from '../../../utils/tauriCommands/common'; +// `safeInvoke` (aliased to `invoke`) converts the CEF +// `window.ipc.postMessage` synchronous throw — Sentry TAURI-REACT-7 / +// TAURI-REACT-6 — into a rejected Promise that the existing try/catch sees +// as a regular IPC failure. +import { safeInvoke as invoke, isTauri } from '../../../utils/tauriCommands/common'; import SettingsHeader from '../components/SettingsHeader'; import { useSettingsNavigation } from '../hooks/useSettingsNavigation'; diff --git a/app/src/lib/nativeNotifications/tauriBridge.ts b/app/src/lib/nativeNotifications/tauriBridge.ts index 266dbb2d76..ba10a096d2 100644 --- a/app/src/lib/nativeNotifications/tauriBridge.ts +++ b/app/src/lib/nativeNotifications/tauriBridge.ts @@ -1,7 +1,10 @@ -import { invoke } from '@tauri-apps/api/core'; import debug from 'debug'; -import { isTauri } from '../../utils/tauriCommands/common'; +// `safeInvoke` (aliased to `invoke`) replaces bare +// `@tauri-apps/api/core::invoke` so the CEF `window.ipc.postMessage` +// synchronous throw (Sentry TAURI-REACT-7 / TAURI-REACT-6) is converted into +// a rejected Promise that the existing try/catch chains already handle. +import { safeInvoke as invoke, isTauri } from '../../utils/tauriCommands/common'; const log = debug('native-notifications:bridge'); const errLog = debug('native-notifications:bridge:error'); diff --git a/app/src/overlay/OverlayApp.tsx b/app/src/overlay/OverlayApp.tsx index 1efd149871..983d86dea4 100644 --- a/app/src/overlay/OverlayApp.tsx +++ b/app/src/overlay/OverlayApp.tsx @@ -23,7 +23,6 @@ * * There is **no** demo loop — the overlay is entirely event-driven. */ -import { invoke } from '@tauri-apps/api/core'; import { currentMonitor, getCurrentWindow, @@ -37,6 +36,13 @@ import RotatingTetrahedronCanvas from '../components/RotatingTetrahedronCanvas'; import { useT } from '../lib/i18n/I18nContext'; import { callCoreRpc, getCoreHttpBaseUrl } from '../services/coreRpcClient'; import { connectCoreSocket } from '../services/coreSocket'; +// `safeInvoke` (aliased to `invoke`) converts the CEF +// `window.ipc.postMessage` synchronous throw — Sentry TAURI-REACT-7 / +// TAURI-REACT-6 — into a rejected Promise so the existing `.catch(...)` +// handler sees it as a normal IPC failure. The overlay window is the most +// at-risk surface here because it boots into its own WebView where the +// CEF IPC bridge can briefly be unwired. +import { safeInvoke as invoke } from '../utils/tauriCommands/common'; const OVERLAY_IDLE_WIDTH = 50; const OVERLAY_IDLE_HEIGHT = 50; diff --git a/app/src/services/__tests__/coreProcessControl.test.ts b/app/src/services/__tests__/coreProcessControl.test.ts index 12bcb1bea2..986598a389 100644 --- a/app/src/services/__tests__/coreProcessControl.test.ts +++ b/app/src/services/__tests__/coreProcessControl.test.ts @@ -12,8 +12,16 @@ vi.mock('@tauri-apps/api/core', () => ({ invoke: invokeMock, isTauri: vi.fn(() = // coreIsTauri() from @tauri-apps/api/core. The default mock returns false // (non-Tauri env); tests that need the Tauri-path branch override it // inline. +// +// `safeInvoke` is the migration shim for the CEF IPC sync-throw +// (OPENHUMAN-TAURI-REACT-7 / TAURI-REACT-6). In production it routes through +// `coreInvoke`; for unit tests we keep `invokeMock` as the seam so the +// existing assertions on call args / order keep working unchanged. const isTauriMock = vi.fn(() => false); -vi.mock('../../utils/tauriCommands/common', () => ({ isTauri: isTauriMock })); +vi.mock('../../utils/tauriCommands/common', () => ({ + isTauri: isTauriMock, + safeInvoke: (...args: unknown[]) => invokeMock(...args), +})); vi.mock('../coreRpcClient', () => ({ clearCoreRpcTokenCache: clearCoreRpcTokenCacheMock })); diff --git a/app/src/services/coreProcessControl.ts b/app/src/services/coreProcessControl.ts index 3a9ea05a42..2b0cac2e78 100644 --- a/app/src/services/coreProcessControl.ts +++ b/app/src/services/coreProcessControl.ts @@ -6,9 +6,11 @@ * is stuck. Outside Tauri (web preview / Vitest harness) this is a no-op * that returns a friendly error string. */ -import { invoke } from '@tauri-apps/api/core'; - -import { isTauri } from '../utils/tauriCommands/common'; +// `safeInvoke` (in place of `@tauri-apps/api/core::invoke`) converts the CEF +// `window.ipc.postMessage` synchronous throw — Sentry TAURI-REACT-7 / +// TAURI-REACT-6 — into a rejected Promise so the caller's `await` / +// `.catch(...)` can handle it instead of bubbling as an unhandled rejection. +import { safeInvoke as invoke, isTauri } from '../utils/tauriCommands/common'; import { clearCoreRpcTokenCache } from './coreRpcClient'; export async function restartCoreProcess(): Promise { diff --git a/app/src/services/webviewAccountService.ts b/app/src/services/webviewAccountService.ts index aa0268cafa..4954294b84 100644 --- a/app/src/services/webviewAccountService.ts +++ b/app/src/services/webviewAccountService.ts @@ -1,5 +1,4 @@ import * as Sentry from '@sentry/react'; -import { invoke } from '@tauri-apps/api/core'; import { listen, type UnlistenFn } from '@tauri-apps/api/event'; import debug from 'debug'; import { z } from 'zod'; @@ -15,7 +14,11 @@ import { import { addIntegrationNotification } from '../store/notificationSlice'; import { fetchRespondQueue } from '../store/providerSurfaceSlice'; import type { AccountProvider, IngestedMessage } from '../types/accounts'; -import { isTauri } from '../utils/tauriCommands/common'; +// `safeInvoke` replaces bare `@tauri-apps/api/core::invoke` so the CEF +// `window.ipc.postMessage` synchronous throw (Sentry TAURI-REACT-7 / +// TAURI-REACT-6) is converted into a rejected Promise rather than an +// unhandled rejection. Existing `.catch(...)` chains keep working unchanged. +import { safeInvoke as invoke, isTauri } from '../utils/tauriCommands/common'; import { openhumanGetMeetSettings } from '../utils/tauriCommands/config'; import { trackEvent } from './analytics'; import { threadApi } from './api/threadApi'; diff --git a/app/src/utils/tauriCommands/auth.ts b/app/src/utils/tauriCommands/auth.ts index 71bbc0f3f6..9916f17de3 100644 --- a/app/src/utils/tauriCommands/auth.ts +++ b/app/src/utils/tauriCommands/auth.ts @@ -1,10 +1,13 @@ /** * Authentication commands. */ -import { invoke } from '@tauri-apps/api/core'; - import { callCoreRpc } from '../../services/coreRpcClient'; -import { CommandResponse, isTauri } from './common'; +// `safeInvoke` (aliased to `invoke`) replaces bare +// `@tauri-apps/api/core::invoke` so the CEF `window.ipc.postMessage` +// synchronous throw (Sentry TAURI-REACT-7 / TAURI-REACT-6) surfaces as a +// rejected Promise. `exchangeToken` runs early in the auth flow where the +// CEF bridge can still be unwired, so this matters most. +import { CommandResponse, safeInvoke as invoke, isTauri } from './common'; /** * Exchange a login token for a session token diff --git a/app/src/utils/tauriCommands/conscious.ts b/app/src/utils/tauriCommands/conscious.ts index 591b919b91..8682078fc3 100644 --- a/app/src/utils/tauriCommands/conscious.ts +++ b/app/src/utils/tauriCommands/conscious.ts @@ -1,9 +1,11 @@ /** * Conscious loop commands. */ -import { invoke } from '@tauri-apps/api/core'; - -import { isTauri } from './common'; +// `safeInvoke` (aliased to `invoke`) replaces bare +// `@tauri-apps/api/core::invoke` so the CEF `window.ipc.postMessage` +// synchronous throw (Sentry TAURI-REACT-7 / TAURI-REACT-6) lands as a +// rejected Promise instead of escaping to `onunhandledrejection`. +import { safeInvoke as invoke, isTauri } from './common'; /** * Trigger a conscious loop run manually. From c2e9c9469aed67f2382b673cac32bd047c1f5edc Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Mon, 25 May 2026 17:03:03 +0530 Subject: [PATCH 3/3] refactor(app): address CodeRabbit feedback on PR #2619 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - auth.ts: use inline `type` modifier on CommandResponse so the compiler/tree-shaker knows it's type-only, while keeping `safeInvoke as invoke` + `isTauri` as runtime imports in a single statement (avoids no-duplicate-imports). - McpServerPanel.test.tsx: drop the redundant `@tauri-apps/api/core` mock — the `safeInvoke` mock at the same hoisted.invoke target shadows it for every panel call site. No behavior change. 30/30 in focused vitest pass. --- .../settings/panels/McpServerPanel.test.tsx | 12 ++++++------ app/src/utils/tauriCommands/auth.ts | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/components/settings/panels/McpServerPanel.test.tsx b/app/src/components/settings/panels/McpServerPanel.test.tsx index 19e60d0ebb..3abe3f5125 100644 --- a/app/src/components/settings/panels/McpServerPanel.test.tsx +++ b/app/src/components/settings/panels/McpServerPanel.test.tsx @@ -16,12 +16,12 @@ import { renderWithProviders } from '../../../test/test-utils'; const hoisted = vi.hoisted(() => ({ invoke: vi.fn(), isTauri: vi.fn(() => true) })); -vi.mock('@tauri-apps/api/core', () => ({ invoke: hoisted.invoke })); - -// McpServerPanel now imports `invoke` from `tauriCommands/common` (aliased -// from `safeInvoke` — see OPENHUMAN-TAURI-REACT-7 / TAURI-REACT-6). Route -// the same `hoisted.invoke` mock through `safeInvoke` so existing assertions -// on `hoisted.invoke.toHaveBeenCalledWith(...)` keep working unchanged. +// McpServerPanel imports `invoke` from `tauriCommands/common` (aliased from +// `safeInvoke` — see OPENHUMAN-TAURI-REACT-7 / TAURI-REACT-6). Route +// `hoisted.invoke` through `safeInvoke` so assertions on +// `hoisted.invoke.toHaveBeenCalledWith(...)` work unchanged. The +// `@tauri-apps/api/core` mock is omitted because `safeInvoke` shadows it +// for all panel call sites. vi.mock('../../../utils/tauriCommands/common', () => ({ isTauri: hoisted.isTauri, safeInvoke: (...args: unknown[]) => hoisted.invoke(...args), diff --git a/app/src/utils/tauriCommands/auth.ts b/app/src/utils/tauriCommands/auth.ts index 9916f17de3..c4fda27431 100644 --- a/app/src/utils/tauriCommands/auth.ts +++ b/app/src/utils/tauriCommands/auth.ts @@ -7,7 +7,7 @@ import { callCoreRpc } from '../../services/coreRpcClient'; // synchronous throw (Sentry TAURI-REACT-7 / TAURI-REACT-6) surfaces as a // rejected Promise. `exchangeToken` runs early in the auth flow where the // CEF bridge can still be unwired, so this matters most. -import { CommandResponse, safeInvoke as invoke, isTauri } from './common'; +import { type CommandResponse, safeInvoke as invoke, isTauri } from './common'; /** * Exchange a login token for a session token