mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
Co-authored-by: Test User <test@example.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
154 lines
6.1 KiB
TypeScript
154 lines
6.1 KiB
TypeScript
/**
|
|
* 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, parseServiceCliOutput } 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);
|
|
});
|
|
});
|
|
|
|
// `parseServiceCliOutput` runs against raw CLI stdout from the `openhuman`
|
|
// sidecar. The core process can crash mid-write, return a partial response, or
|
|
// drift from the expected JSON schema across versions — any of which produces
|
|
// malformed input. The guard must reject those cases with a descriptive error
|
|
// rather than handing typed garbage back to callers.
|
|
describe('parseServiceCliOutput (tauriCommands/common)', () => {
|
|
it('returns the parsed response when shape matches CommandResponse', () => {
|
|
const raw = JSON.stringify({ result: { value: 42 }, logs: ['ok'] });
|
|
|
|
const parsed = parseServiceCliOutput<{ value: number }>(raw);
|
|
|
|
expect(parsed.result).toEqual({ value: 42 });
|
|
expect(parsed.logs).toEqual(['ok']);
|
|
});
|
|
|
|
it('accepts null result and empty logs (valid CommandResponse shape)', () => {
|
|
const raw = JSON.stringify({ result: null, logs: [] });
|
|
|
|
const parsed = parseServiceCliOutput<null>(raw);
|
|
|
|
expect(parsed.result).toBeNull();
|
|
expect(parsed.logs).toEqual([]);
|
|
});
|
|
|
|
it('throws a descriptive error when the input is not valid JSON', () => {
|
|
expect(() => parseServiceCliOutput('not-json')).toThrow(/Failed to parse service CLI output/);
|
|
});
|
|
|
|
it('throws when the parsed value is null', () => {
|
|
expect(() => parseServiceCliOutput('null')).toThrow(/CommandResponse shape/);
|
|
});
|
|
|
|
it('throws when the parsed value is an array (not an object)', () => {
|
|
expect(() => parseServiceCliOutput('[]')).toThrow(/CommandResponse shape/);
|
|
});
|
|
|
|
it('throws when required `logs` field is missing', () => {
|
|
expect(() => parseServiceCliOutput(JSON.stringify({ result: 1 }))).toThrow(
|
|
/CommandResponse shape/
|
|
);
|
|
});
|
|
|
|
it('throws when required `result` field is missing', () => {
|
|
expect(() => parseServiceCliOutput(JSON.stringify({ logs: [] }))).toThrow(
|
|
/CommandResponse shape/
|
|
);
|
|
});
|
|
|
|
it('throws when `logs` is not an array', () => {
|
|
expect(() => parseServiceCliOutput(JSON.stringify({ result: 1, logs: 'oops' }))).toThrow(
|
|
/CommandResponse shape/
|
|
);
|
|
});
|
|
|
|
it('throws when `logs` contains non-string entries', () => {
|
|
expect(() => parseServiceCliOutput(JSON.stringify({ result: 1, logs: [1, 2] }))).toThrow(
|
|
/CommandResponse shape/
|
|
);
|
|
});
|
|
});
|