Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/src/components/BootCheckGate/BootCheckGate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
* Visual language follows ServiceBlockingGate.tsx (bg-stone-950/80 overlay,
* bg-stone-900 panel, ocean-500 / coral-500 semantics).
*/
import { isTauri } from '@tauri-apps/api/core';
import debug from 'debug';
import { useCallback, useEffect, useRef, useState } from 'react';

Expand All @@ -29,6 +28,7 @@ import {
storeCoreToken,
storeRpcUrl,
} from '../../utils/configPersistence';
import { isTauri } from '../../utils/tauriCommands/common';

const log = debug('boot-check');
const logError = debug('boot-check:error');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { invoke, isTauri } from '@tauri-apps/api/core';
import { invoke } from '@tauri-apps/api/core';
import { useEffect, useState } from 'react';

import { triggerSentryTestEvent } from '../../../services/analytics';
import { useAppSelector } from '../../../store/hooks';
import { APP_ENVIRONMENT } from '../../../utils/config';
import { isTauri } from '../../../utils/tauriCommands/common';
import SettingsHeader from '../components/SettingsHeader';
import SettingsMenuItem from '../components/SettingsMenuItem';
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
Expand Down
4 changes: 3 additions & 1 deletion app/src/lib/nativeNotifications/tauriBridge.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { invoke, isTauri } from '@tauri-apps/api/core';
import { invoke } from '@tauri-apps/api/core';
import debug from 'debug';

import { isTauri } from '../../utils/tauriCommands/common';

const log = debug('native-notifications:bridge');
const errLog = debug('native-notifications:bridge:error');

Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/webviewNotifications/service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { isTauri } from '@tauri-apps/api/core';
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
import debug from 'debug';

Expand All @@ -9,6 +8,7 @@ import {
noteWebviewNotificationFired,
} from '../../store/accountsSlice';
import { addIntegrationNotification } from '../../store/notificationSlice';
import { isTauri } from '../../utils/tauriCommands/common';
import { WEBVIEW_NOTIFICATION_FIRED_EVENT, type WebviewNotificationFired } from './types';

const log = debug('webview-notifications');
Expand Down
7 changes: 5 additions & 2 deletions app/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// IMPORTANT: Polyfills must be imported FIRST
import { isTauri as tauriRuntimeAvailable } from '@tauri-apps/api/core';
import { getCurrentWindow } from '@tauri-apps/api/window';
import React from 'react';
import ReactDOM from 'react-dom/client';
Expand All @@ -16,14 +15,18 @@ import { primeActiveUserId } from './store/userScopedStorage';
import { APP_VERSION } from './utils/config';
import { setupDesktopDeepLinkListener } from './utils/desktopDeepLinkListener';
import { getActiveUserIdFromCore } from './utils/tauriCommands';
import { isTauri as tauriRuntimeAvailable } from './utils/tauriCommands/common';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] Stale comment about "pre-Tauri detection".

The existing comment near the currentWindowLabel block says something like "Detect it before we touch any Tauri APIs" — but tauriRuntimeAvailable() now IS the hardened isTauri() which itself reads window.__TAURI_INTERNALS__. Worth a quick update to reflect reality so a future engineer doesn't wonder why we're touching internals in the "pre-Tauri" detection block. Behavior is correct; just the comment is misleading.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rewrote the stale comment block above the urlWindowParam IIFE in app/src/main.tsx to reflect that tauriRuntimeAvailable() is the hardened isTauri() which itself reads window.__TAURI_INTERNALS__. Landed in 169f1fc.


setStoreForApiClient(() => getCoreStateSnapshot().snapshot.sessionToken);

// The floating mascot is hosted in a native macOS NSPanel + WKWebView
// that lives OUTSIDE Tauri's runtime (the vendored tauri-cef can't render
// transparent windowed-mode browsers). That webview can't read a Tauri
// window label, so the Rust shell appends `?window=mascot` to the URL it
// loads. Detect it before we touch any Tauri APIs.
// loads. Detect it via the URL param so we can skip `getCurrentWindow()`
// — which would either throw or trigger the CEF IPC-bootstrap gap that
// `tauriRuntimeAvailable()` (= the hardened `isTauri()`) now guards
// against by reading `window.__TAURI_INTERNALS__.invoke`.
const urlWindowParam = (() => {
try {
return new URLSearchParams(window.location.search).get('window');
Expand Down
3 changes: 1 addition & 2 deletions app/src/services/backendUrl.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { isTauri as coreIsTauri } from '@tauri-apps/api/core';

import { BACKEND_URL } from '../utils/config';
import { isTauri as coreIsTauri } from '../utils/tauriCommands/common';
import { callCoreRpc } from './coreRpcClient';

let resolvedBackendUrl: string | null = null;
Expand Down
3 changes: 2 additions & 1 deletion app/src/services/coreRpcClient.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { isTauri as coreIsTauri, invoke } from '@tauri-apps/api/core';
import { invoke } from '@tauri-apps/api/core';
import debug from 'debug';

import { dispatchLocalAiMethod } from '../lib/ai/localCoreAiMemory';
import { CORE_RPC_TIMEOUT_MS, CORE_RPC_URL } from '../utils/config';
import { getStoredCoreToken, peekStoredRpcUrl } from '../utils/configPersistence';
import { sanitizeError } from '../utils/sanitize';
import { isTauri as coreIsTauri } from '../utils/tauriCommands/common';
import { normalizeRpcMethod } from './rpcMethods';

interface CoreRpcRelayRequest {
Expand Down
3 changes: 2 additions & 1 deletion app/src/services/meetCallService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
//
// Splitting it this way keeps platform-specific window code in the shell
// while the validation rules live (and are tested) in the core.
import { invoke, isTauri } from '@tauri-apps/api/core';
import { invoke } from '@tauri-apps/api/core';

import { isTauri } from '../utils/tauriCommands/common';
import { callCoreRpc } from './coreRpcClient';

export type MeetJoinCallInput = { meetUrl: string; displayName: string };
Expand Down
7 changes: 6 additions & 1 deletion app/src/services/webviewAccountService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as Sentry from '@sentry/react';
import { invoke, isTauri } from '@tauri-apps/api/core';
import { invoke } from '@tauri-apps/api/core';
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
import debug from 'debug';

Expand All @@ -13,6 +13,7 @@ import {
import { addIntegrationNotification } from '../store/notificationSlice';
import { fetchRespondQueue } from '../store/providerSurfaceSlice';
import type { AccountProvider, IngestedMessage } from '../types/accounts';
import { isTauri } from '../utils/tauriCommands/common';
import { openhumanGetMeetSettings } from '../utils/tauriCommands/config';
import { trackEvent } from './analytics';
import { threadApi } from './api/threadApi';
Expand All @@ -25,6 +26,10 @@ const MEET_ORCHESTRATOR_MODEL = 'reasoning-v1';
const log = debug('webview-accounts');
const errLog = debug('webview-accounts:error');

// Re-export the canonical Tauri guard so existing imports
// `import { isTauri } from '.../webviewAccountService'` keep working.
// The implementation lives in `utils/tauriCommands/common.ts` and accounts
// for the CEF IPC injection race (see comment there).
export { isTauri };

/**
Expand Down
18 changes: 18 additions & 0 deletions app/src/test/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,17 @@ if (typeof Element !== 'undefined' && !Element.prototype.scrollIntoView) {
Element.prototype.scrollIntoView = function () {};
}

// The hardened `isTauri()` (in `utils/tauriCommands/common.ts`) checks both
// `coreIsTauri()` and `window.__TAURI_INTERNALS__.invoke`. Many existing test
// files mock `@tauri-apps/api/core::isTauri` to `true` to exercise the
// Tauri branch; without a matching IPC handle on `window` they would now
// regress to the non-Tauri path. Seed a no-op handle once globally so the
// IPC-readiness check passes by default. Tests that *want* the CEF gap
// behaviour can `delete window.__TAURI_INTERNALS__` in a `beforeEach`.
(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[major] __TAURI_INTERNALS__ seed is not reset between tests.

The seed is set once globally at module load but not restored in afterEach. Tests that delete window.__TAURI_INTERNALS__ (as the comment suggests) could contaminate subsequent tests sharing the same jsdom worker if they don't individually restore it. common.test.ts handles this correctly with its own save/restore, but any other test file that deletes the global without restoring it will silently cause subsequent tests to take the non-Tauri branch.

Suggestion — re-seed in the existing afterEach:

afterEach(() => {
  clearRequestLog();
  cleanup();
  // Re-seed the IPC handle after any test that may have deleted it.
  (window as unknown as { __TAURI_INTERNALS__: { invoke: () => Promise<unknown> } })
    .__TAURI_INTERNALS__ = { invoke: vi.fn(() => Promise.resolve()) };
});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-seeded window.__TAURI_INTERNALS__ inside the existing afterEach in app/src/test/setup.ts so tests that delete the handle can't contaminate sibling tests in the same jsdom worker. Landed in 169f1fc.

window as unknown as { __TAURI_INTERNALS__: { invoke: () => Promise<unknown> } }
).__TAURI_INTERNALS__ = { invoke: vi.fn(() => Promise.resolve()) };

// Mock Tauri APIs (not available in test env)
vi.mock('@tauri-apps/api/core', () => ({ invoke: vi.fn(), isTauri: vi.fn(() => false) }));

Expand Down Expand Up @@ -223,6 +234,13 @@ if (!process.env.DEBUG_TESTS) {
afterEach(() => {
clearRequestLog();
cleanup();
// Re-seed the IPC handle after any test that may have deleted it
// (e.g. tests exercising the CEF-gap branch of `isTauri()`). Without
// this, sibling tests in the same jsdom worker would silently regress
// to the non-Tauri path. Per graycyrus review on PR #1556.
(
window as unknown as { __TAURI_INTERNALS__: { invoke: () => Promise<unknown> } }
).__TAURI_INTERNALS__ = { invoke: vi.fn(() => Promise.resolve()) };
});
afterAll(async () => {
await stopMockServer();
Expand Down
2 changes: 1 addition & 1 deletion app/src/utils/desktopDeepLinkListener.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as Sentry from '@sentry/react';
import { isTauri as coreIsTauri } from '@tauri-apps/api/core';
import { getCurrentWindow } from '@tauri-apps/api/window';
import { getCurrent, onOpenUrl } from '@tauri-apps/plugin-deep-link';

Expand All @@ -14,6 +13,7 @@ import { BILLING_DASHBOARD_URL } from './links';
import { evaluateOAuthAppVersionGate } from './oauthAppVersionGate';
import { openUrl } from './openUrl';
import { storeSession } from './tauriCommands';
import { isTauri as coreIsTauri } from './tauriCommands/common';

const SESSION_TOKEN_UPDATED_EVENT = 'core-state:session-token-updated';

Expand Down
2 changes: 1 addition & 1 deletion app/src/utils/oauthAppVersionGate.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { getVersion } from '@tauri-apps/api/app';
import { isTauri } from '@tauri-apps/api/core';

import { LATEST_APP_DOWNLOAD_URL, MINIMUM_SUPPORTED_APP_VERSION } from './config';
import { isVersionAtLeast, parseSemverParts } from './semver';
import { isTauri } from './tauriCommands/common';

export type OAuthAppVersionGateResult =
| { ok: true }
Expand Down
2 changes: 1 addition & 1 deletion app/src/utils/openUrl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const isTauriMock = vi.fn();
const tauriOpenUrlMock = vi.fn();
const addBreadcrumbMock = vi.fn();

vi.mock('@tauri-apps/api/core', () => ({ isTauri: () => isTauriMock() }));
vi.mock('./tauriCommands/common', () => ({ isTauri: () => isTauriMock() }));

vi.mock('@tauri-apps/plugin-opener', () => ({ openUrl: (url: string) => tauriOpenUrlMock(url) }));

Expand Down
3 changes: 2 additions & 1 deletion app/src/utils/openUrl.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as Sentry from '@sentry/react';
import { isTauri } from '@tauri-apps/api/core';
import { openUrl as tauriOpenUrl } from '@tauri-apps/plugin-opener';

import { isTauri } from './tauriCommands/common';

const isHttpUrl = (url: string): boolean => /^https?:\/\//i.test(url);

/**
Expand Down
92 changes: 92 additions & 0 deletions app/src/utils/tauriCommands/common.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* Unit tests for `isTauri()` — the canonical Tauri-runtime guard used across
* `app/src/`. Beyond delegating to `@tauri-apps/api/core::isTauri()`, this
* wrapper also confirms that the IPC transport (`window.__TAURI_INTERNALS__
* .invoke`) is wired before reporting `true`.
*
* Why it matters: under CEF, `globalThis.isTauri` (which the underlying
* `coreIsTauri()` checks) is injected by the webview bootstrap BEFORE the
* `postMessage` IPC bridge is connected. An `invoke()` landing in that gap
* throws `TypeError: Cannot read properties of undefined (reading
* 'postMessage')` deep inside Tauri's `sendIpcMessage`, which surfaces as
* the OPENHUMAN-REACT-S Sentry issue (#1472 follow-up). All call sites that
* gate on `isTauri()` should now route through the non-Tauri branch during
* the gap instead of bursting into IPC.
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { isTauri } from './common';

const coreIsTauriMock = vi.fn();

vi.mock('@tauri-apps/api/core', () => ({ isTauri: () => coreIsTauriMock() }));

describe('isTauri (tauriCommands/common)', () => {
// We mutate `window` to simulate Tauri-runtime bootstrap state across cases.
// Stash + restore so other tests in the suite (which share the jsdom global)
// see a pristine window.
let originalInternals: unknown;

beforeEach(() => {
coreIsTauriMock.mockReset();
originalInternals = (window as unknown as { __TAURI_INTERNALS__?: unknown })
.__TAURI_INTERNALS__;
});

afterEach(() => {
if (originalInternals === undefined) {
delete (window as unknown as { __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__;
} else {
(window as unknown as { __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__ =
originalInternals;
}
});

it('returns false when not running in Tauri at all (browser/Vitest)', () => {
coreIsTauriMock.mockReturnValue(false);
delete (window as unknown as { __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__;

expect(isTauri()).toBe(false);
});

it('returns true when both the runtime flag and the IPC `invoke` handle are present', () => {
coreIsTauriMock.mockReturnValue(true);
(window as unknown as { __TAURI_INTERNALS__?: { invoke: unknown } }).__TAURI_INTERNALS__ = {
invoke: () => Promise.resolve(),
};

expect(isTauri()).toBe(true);
});

// The OPENHUMAN-REACT-S regression: Tauri sets `globalThis.isTauri = true`
// (so the official check returns true) before CEF wires the IPC postMessage
// bridge. During that gap any unguarded `invoke(...)` blows up inside
// `sendIpcMessage` with the "Cannot read properties of undefined (reading
// 'postMessage')" TypeError. Our guard must short-circuit to `false` so
// call sites skip the IPC path instead of trusting the runtime flag alone.
it('returns false during the CEF gap when runtime flag is set but __TAURI_INTERNALS__ is missing', () => {
coreIsTauriMock.mockReturnValue(true);
delete (window as unknown as { __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__;

expect(isTauri()).toBe(false);
});

it('returns false during the partial-bootstrap gap when __TAURI_INTERNALS__ exists but `invoke` is not yet wired', () => {
coreIsTauriMock.mockReturnValue(true);
// Some CEF bootstrap stages set the object literal before the IPC handle
// is attached. Treat that as "not ready".
(window as unknown as { __TAURI_INTERNALS__?: Record<string, unknown> }).__TAURI_INTERNALS__ =
{};

expect(isTauri()).toBe(false);
});

it('returns false when __TAURI_INTERNALS__.invoke is present but not a function', () => {
coreIsTauriMock.mockReturnValue(true);
(window as unknown as { __TAURI_INTERNALS__?: { invoke: unknown } }).__TAURI_INTERNALS__ = {
invoke: 'not-a-function',
};

expect(isTauri()).toBe(false);
});
});
36 changes: 33 additions & 3 deletions app/src/utils/tauriCommands/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,41 @@
* Common utilities and types for Tauri Commands.
*/
import { isTauri as coreIsTauri } from '@tauri-apps/api/core';
import debug from 'debug';

// Check if we're running in Tauri
const log = debug('tauri:ipc-guard');

/**
* True when the Tauri runtime is present AND the underlying IPC transport is
* wired. The official `coreIsTauri()` check (which reads `globalThis.isTauri`)
* is set early by Tauri's webview bootstrap, but on CEF `__TAURI_INTERNALS__`
* (and the `postMessage` bridge it dispatches through) is injected *after*
* `on_after_created` fires. An `invoke()` landing in that gap throws
* `TypeError: Cannot read properties of undefined (reading 'postMessage')`
* deep inside Tauri's `sendIpcMessage` — see OPENHUMAN-REACT-S / #1472.
*
* Callers that gate on `isTauri()` BEFORE invoking should therefore use this
* function; it returns `false` during the bootstrap gap so the call site
* takes the non-Tauri branch (skip / fallback) instead of synchronously
* throwing into a `new Promise` body where the rejection escapes the local
* try/catch and lands as an unhandled Sentry event.
*/
export const isTauri = (): boolean => {
// Tauri v2: prefer the official runtime check over window globals.
return coreIsTauri();
if (!coreIsTauri()) return false;
if (typeof window === 'undefined') return false;
// Narrow `window` access through a single optional chain so the check is
// resilient to either `__TAURI_INTERNALS__` being absent or `.invoke`
// being missing while the rest of the object is partially populated.
const internals = (window as unknown as { __TAURI_INTERNALS__?: { invoke?: unknown } })
.__TAURI_INTERNALS__;
if (typeof internals?.invoke !== 'function') {
// Bridge-missing branch: distinct from `!coreIsTauri()` (= not in Tauri
// at all). Logging here makes the CEF bootstrap gap observable in dev
// and is a no-op in production (debug namespace disabled by default).
log('isTauri() -> false: IPC bridge not wired (CEF bootstrap gap or non-Tauri)');
return false;
}
return true;
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] No debug logging when the guard short-circuits due to missing IPC bridge.

Per CLAUDE.md's debug logging requirement, new/changed flows should log branches and state transitions. This wrapper is the single choke point for catching the CEF gap at runtime — but when it returns false due to a missing IPC bridge (not just !coreIsTauri()), there's no log entry. A lightweight debug log on the bridge-missing branch would make the fix observable in dev with zero production overhead:

import debug from 'debug';
const log = debug('tauri:ipc-guard');

export const isTauri = (): boolean => {
  if (!coreIsTauri()) return false;
  if (typeof window === 'undefined') return false;
  const internals = (window as unknown as { __TAURI_INTERNALS__?: { invoke?: unknown } })
    .__TAURI_INTERNALS__;
  if (typeof internals?.invoke !== 'function') {
    log('isTauri() → false: IPC bridge not yet wired (CEF gap or non-Tauri)');
    return false;
  }
  return true;
};

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added debug('tauri:ipc-guard') logging in app/src/utils/tauriCommands/common.ts on the branch where internals?.invoke isn't a function, making the CEF bootstrap gap observable in dev with zero prod overhead. Landed in 169f1fc.


export interface CommandResponse<T> {
Expand Down
Loading