mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-28 13:32:23 +00:00
342 lines
12 KiB
TypeScript
342 lines
12 KiB
TypeScript
/**
|
|
* E2E: Linux CEF deb package runtime - core binary resolution and tray gating
|
|
*
|
|
* Tests the cross-process behavior:
|
|
* - UI → Tauri `core_rpc_url` command → sidecar binary resolution
|
|
* - Core binary path probing: env override → packaged Linux locations → fallback
|
|
* - Tray setup on linux+cef (skipped without panicking)
|
|
* - Grep-friendly logging patterns for diagnostics
|
|
*
|
|
* This spec validates that the Linux .deb package can find openhuman-core
|
|
* in system paths like /usr/bin/openhuman-core when installed via .deb.
|
|
*
|
|
* Coverage:
|
|
* - core_process::default_core_bin() resolution paths
|
|
* - setup_tray() conditional compilation for linux+cef
|
|
* - Tauri command: core_rpc_url
|
|
* - Sidecar JSON-RPC connectivity
|
|
*/
|
|
import { waitForApp, waitForAppReady } from '../helpers/app-helpers';
|
|
import { callOpenhumanRpc } from '../helpers/core-rpc';
|
|
import { dumpAccessibilityTree, textExists } from '../helpers/element-helpers';
|
|
import { supportsExecuteScript } from '../helpers/platform';
|
|
import { startMockServer, stopMockServer } from '../mock-server';
|
|
|
|
interface TauriInvokeResult<T> {
|
|
ok: boolean;
|
|
error?: string;
|
|
result?: T;
|
|
}
|
|
|
|
/**
|
|
* Invoke a Tauri command via WebView execute (tauri-driver only).
|
|
* Returns { ok, result } or { ok: false, error }.
|
|
*/
|
|
async function invokeTauriCommand<T>(
|
|
command: string,
|
|
args: Record<string, unknown> = {}
|
|
): Promise<TauriInvokeResult<T>> {
|
|
if (!supportsExecuteScript()) {
|
|
return { ok: false, error: 'Execute script not supported on this platform' };
|
|
}
|
|
|
|
try {
|
|
const result = await browser.executeAsync(
|
|
(cmd: string, a: Record<string, unknown>, done: (r: unknown) => void) => {
|
|
// Use __TAURI_INTERNALS__.invoke — the same path @tauri-apps/api/core uses
|
|
// internally. Under CEF, window.__TAURI__ is not populated but
|
|
// __TAURI_INTERNALS__ is always present (mirrors tauri-commands.spec.ts).
|
|
const internals = (
|
|
window as unknown as {
|
|
__TAURI_INTERNALS__?: { invoke?: (cmd: string, args?: unknown) => Promise<unknown> };
|
|
}
|
|
).__TAURI_INTERNALS__;
|
|
if (typeof internals?.invoke !== 'function') {
|
|
done({ ok: false, error: 'window.__TAURI_INTERNALS__.invoke not available' });
|
|
return;
|
|
}
|
|
internals
|
|
.invoke(cmd, a)
|
|
.then((r: unknown) => done({ ok: true, result: r }))
|
|
.catch((e: unknown) =>
|
|
done({ ok: false, error: e instanceof Error ? e.message : String(e) })
|
|
);
|
|
},
|
|
command,
|
|
args
|
|
);
|
|
return result as TauriInvokeResult<T>;
|
|
} catch (e) {
|
|
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
}
|
|
}
|
|
|
|
function stepLog(message: string, context?: unknown): void {
|
|
const stamp = new Date().toISOString();
|
|
const prefix = '[LinuxCefDebRuntimeE2E]';
|
|
if (context === undefined) {
|
|
console.log(`${prefix}[${stamp}] ${message}`);
|
|
} else {
|
|
console.log(`${prefix}[${stamp}] ${message}`, JSON.stringify(context, null, 2));
|
|
}
|
|
}
|
|
|
|
describe('Linux CEF deb package runtime (UI → Tauri → sidecar)', () => {
|
|
before(async function beforeSuite() {
|
|
this.timeout(90_000);
|
|
stepLog('Starting mock backend');
|
|
await startMockServer();
|
|
await waitForApp();
|
|
await waitForAppReady(20_000);
|
|
stepLog('App ready');
|
|
});
|
|
|
|
after(async () => {
|
|
stepLog('Stopping mock backend');
|
|
await stopMockServer();
|
|
});
|
|
|
|
// ==========================================================================
|
|
// Core Binary Resolution Tests
|
|
// ==========================================================================
|
|
|
|
describe('core binary resolution paths', () => {
|
|
it('core_rpc_url Tauri command returns valid RPC URL', async () => {
|
|
const result = await invokeTauriCommand<string>('core_rpc_url');
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.result).toBeDefined();
|
|
|
|
const url = result.result!;
|
|
stepLog('core_rpc_url returned', { url });
|
|
|
|
// Validate URL format: http://host:port/rpc
|
|
expect(url).toMatch(/^http:\/\/[^/]+:\d+\/rpc$/);
|
|
|
|
// Should be localhost or 127.0.0.1
|
|
expect(url).toMatch(/http:\/\/(127\.0\.0\.1|localhost):/);
|
|
});
|
|
|
|
it('core RPC endpoint responds to ping (sidecar is reachable)', async () => {
|
|
const result = await callOpenhumanRpc('core.ping', {});
|
|
|
|
stepLog('core.ping result', {
|
|
ok: result.ok,
|
|
error: result.error,
|
|
hasResult: result.result !== undefined,
|
|
});
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.error).toBeUndefined();
|
|
});
|
|
|
|
it('core version is accessible via JSON-RPC', async () => {
|
|
const result = await callOpenhumanRpc('core.version', {});
|
|
|
|
stepLog('core.version result', {
|
|
ok: result.ok,
|
|
error: result.error,
|
|
hasResult: result.result !== undefined,
|
|
});
|
|
|
|
// core.version should succeed and return version info
|
|
expect(result.ok).toBe(true);
|
|
|
|
if (result.result && typeof result.result === 'object') {
|
|
const resultObj = result.result as Record<string, unknown>;
|
|
stepLog('core version info', resultObj);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ==========================================================================
|
|
// Sidecar Lifecycle Tests
|
|
// ==========================================================================
|
|
|
|
describe('sidecar lifecycle and health', () => {
|
|
it('sidecar responds to health check via JSON-RPC', async () => {
|
|
// Multiple methods to verify sidecar is healthy
|
|
const methods = ['core.ping', 'core.version', 'core.health'];
|
|
const results: Record<string, boolean> = {};
|
|
|
|
for (const method of methods) {
|
|
try {
|
|
const result = await callOpenhumanRpc(method, {});
|
|
results[method] = result.ok;
|
|
stepLog(`Health check ${method}`, { ok: result.ok });
|
|
} catch {
|
|
results[method] = false;
|
|
}
|
|
}
|
|
|
|
// At least one method should succeed
|
|
const anySuccess = Object.values(results).some(v => v);
|
|
if (!anySuccess) {
|
|
throw new Error(
|
|
`Expected at least one health check to pass. Results: ${JSON.stringify(results)}`
|
|
);
|
|
}
|
|
expect(anySuccess).toBe(true);
|
|
});
|
|
|
|
it('sidecar binary was found and spawned (not self-subcommand fallback)', async () => {
|
|
// Post PR #1061 the core runs in-process (no sidecar binary), but this
|
|
// assertion still has value: a successful core.ping proves the core
|
|
// RPC server bound a port and is reachable. `httpStatus` is only set
|
|
// on failure paths of callOpenhumanRpcNode — so asserting on it for a
|
|
// success case always fails; check `ok` and absence of error instead.
|
|
|
|
const result = await callOpenhumanRpc('core.ping', {});
|
|
|
|
stepLog('Verifying core RPC is running', { ok: result.ok, error: result.error });
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.error).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ==========================================================================
|
|
// Tray Behavior Tests (linux+cef specific)
|
|
// ==========================================================================
|
|
|
|
describe('tray behavior on packaged linux', () => {
|
|
it('app starts without tray-related panics', async () => {
|
|
// The app started successfully in before() - if setup_tray() had panicked
|
|
// on linux+cef, we wouldn't be here. Verify app is healthy.
|
|
|
|
const hasChrome = await textExists('OpenHuman');
|
|
stepLog('App chrome check', { hasChrome });
|
|
|
|
// App should have started without crashing
|
|
expect(hasChrome).toBe(true);
|
|
});
|
|
|
|
it('accessibility tree is accessible for diagnostics', async () => {
|
|
// Get page source for debugging - validates no crash
|
|
const source = await dumpAccessibilityTree();
|
|
|
|
stepLog('Accessibility tree length', { length: source.length });
|
|
|
|
// The dump includes bundled JS source which legitimately contains the
|
|
// tokens "error" / "panic" inside string literals (e.g. Tauri-Error
|
|
// header, IPC error-handling code). Assert on crash-shaped markers
|
|
// instead — those are what we actually care about for diagnostics.
|
|
expect(source.length).toBeGreaterThan(0);
|
|
expect(source.toLowerCase()).not.toContain('uncaught panic');
|
|
expect(source.toLowerCase()).not.toContain('runtime panic at');
|
|
});
|
|
|
|
it('main window is created and visible', async () => {
|
|
if (!supportsExecuteScript()) {
|
|
stepLog('Skipping window visibility check on Mac2');
|
|
return;
|
|
}
|
|
|
|
// Check that the main window exists
|
|
const windowHandle = await browser.getWindowHandle();
|
|
stepLog('Window handle', { handle: windowHandle });
|
|
|
|
expect(windowHandle).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
// ==========================================================================
|
|
// Cross-Process Integration Tests
|
|
// ==========================================================================
|
|
|
|
describe('UI ↔ Tauri ↔ sidecar JSON-RPC integration', () => {
|
|
it('frontend can invoke Tauri commands that reach the sidecar', async () => {
|
|
// Test the full chain: UI invokes Tauri command → Tauri calls sidecar RPC
|
|
// The core_rpc_url command returns the RPC URL, proving the sidecar is managed
|
|
|
|
const rpcUrlResult = await invokeTauriCommand<string>('core_rpc_url');
|
|
expect(rpcUrlResult.ok).toBe(true);
|
|
|
|
const rpcUrl = rpcUrlResult.result!;
|
|
stepLog('Full chain test: core_rpc_url', { rpcUrl });
|
|
|
|
// Now verify that URL is actually reachable
|
|
const pingResult = await callOpenhumanRpc('core.ping', {});
|
|
expect(pingResult.ok).toBe(true);
|
|
});
|
|
|
|
it('sidecar environment inherits from Tauri process', async () => {
|
|
// The sidecar should have access to env vars set by Tauri
|
|
// core_rpc_url should return the same URL that the sidecar is using
|
|
|
|
const result = await invokeTauriCommand<string>('core_rpc_url');
|
|
expect(result.ok).toBe(true);
|
|
|
|
const url = result.result!;
|
|
stepLog('Sidecar RPC URL from Tauri', { url });
|
|
|
|
// Extract port from URL
|
|
const portMatch = url.match(/:(\d+)\/rpc$/);
|
|
expect(portMatch).toBeTruthy();
|
|
|
|
const port = parseInt(portMatch![1], 10);
|
|
expect(port).toBeGreaterThan(0);
|
|
expect(port).toBeLessThan(65536);
|
|
});
|
|
});
|
|
|
|
// ==========================================================================
|
|
// Logging/Diagnostics Tests
|
|
// ==========================================================================
|
|
|
|
describe('grep-friendly diagnostics', () => {
|
|
it('core process logs contain expected diagnostic patterns', async () => {
|
|
// This test documents the expected log patterns from PR #3:
|
|
// - "[core] default_core_bin: using packaged linux core binary"
|
|
// - "[core] default_core_bin: using OPENHUMAN_CORE_BIN override"
|
|
// - "[tray] skipping tray setup on linux+cef"
|
|
// - "[core] core process ready"
|
|
|
|
// We can't directly read logs in E2E, but we verify the sidecar
|
|
// started successfully which means the logging paths executed
|
|
|
|
const result = await callOpenhumanRpc('core.ping', {});
|
|
expect(result.ok).toBe(true);
|
|
|
|
stepLog('Diagnostic patterns verified via successful startup', { pingOk: result.ok });
|
|
});
|
|
});
|
|
|
|
// ==========================================================================
|
|
// Packaged Install Path Tests
|
|
// ==========================================================================
|
|
|
|
describe('packaged linux binary path resolution', () => {
|
|
it('sidecar is running with non-default port when OPENHUMAN_CORE_PORT is set', async () => {
|
|
// When OPENHUMAN_CORE_PORT is set, the sidecar should use that port
|
|
// This verifies env var propagation to the sidecar
|
|
|
|
const result = await invokeTauriCommand<string>('core_rpc_url');
|
|
expect(result.ok).toBe(true);
|
|
|
|
const url = result.result!;
|
|
stepLog('Sidecar RPC URL', { url });
|
|
|
|
// URL should be well-formed
|
|
expect(url).toMatch(/^http:\/\/.+:\d+\/rpc$/);
|
|
});
|
|
|
|
it('core.ping returns consistent response across multiple calls', async () => {
|
|
// Verify the sidecar is stable and responding consistently
|
|
const results: boolean[] = [];
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
const result = await callOpenhumanRpc('core.ping', {});
|
|
results.push(result.ok);
|
|
await browser.pause(100);
|
|
}
|
|
|
|
stepLog('Multiple ping results', { results });
|
|
|
|
// All calls should succeed
|
|
expect(results.every(r => r)).toBe(true);
|
|
});
|
|
});
|
|
});
|