mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
147 lines
6.2 KiB
TypeScript
147 lines
6.2 KiB
TypeScript
// @ts-nocheck
|
|
/**
|
|
* E2E spec: core port conflict recovery
|
|
*
|
|
* Covers:
|
|
* - When port 7788 (default OPENHUMAN_CORE_PORT) is already bound by an
|
|
* unrelated process before the desktop app starts, the embedded in-process
|
|
* core either binds a fallback port and continues normally, OR surfaces a
|
|
* clear conflict message so the user can diagnose the issue.
|
|
* - A second app instance while the first already owns port 7788 must not
|
|
* silently produce 401s or version drift — it should either attach to the
|
|
* running core or surface a clear error.
|
|
*
|
|
* Gap note (port fallback path):
|
|
* The desktop app's CoreProcessHandle selects a fallback port when the
|
|
* preferred port is occupied by a non-OpenHuman listener
|
|
* (see app/src-tauri/src/core_process.rs, `identify_listener` +
|
|
* `is_expected_port_clash`). The fallback port is communicated back via
|
|
* `EmbeddedReadySignal.fallback_from`. The UI does not currently render a
|
|
* user-visible "port conflict" dialog — the app continues working on the
|
|
* fallback port. As a result, this spec cannot assert a specific conflict
|
|
* dialog text; instead it asserts that the app reaches a usable state (home
|
|
* screen or onboarding) even under a port conflict, which proves the fallback
|
|
* path engaged.
|
|
*
|
|
* TODO (tracked gap):
|
|
* A visible port-conflict banner / dialog for the end-user has not been
|
|
* implemented (feature gap). When it ships, remove the `.skip` from
|
|
* '4.2.2 — second instance surfaces clear conflict dialog' below and add
|
|
* an assertion for the specific UI text.
|
|
*/
|
|
import net from 'node:net';
|
|
|
|
import { waitForApp } from '../helpers/app-helpers';
|
|
import { textExists, waitForText } from '../helpers/element-helpers';
|
|
import { startMockServer, stopMockServer } from '../mock-server';
|
|
|
|
const DEFAULT_CORE_PORT = Number(process.env.OPENHUMAN_CORE_PORT ?? 7788);
|
|
|
|
function stepLog(message: string, context?: unknown): void {
|
|
const stamp = new Date().toISOString();
|
|
if (context === undefined) {
|
|
console.log(`[CorePortConflictE2E][${stamp}] ${message}`);
|
|
return;
|
|
}
|
|
console.log(`[CorePortConflictE2E][${stamp}] ${message}`, JSON.stringify(context, null, 2));
|
|
}
|
|
|
|
async function waitForHome(timeout = 25_000): Promise<boolean> {
|
|
const deadline = Date.now() + timeout;
|
|
while (Date.now() < deadline) {
|
|
if (await textExists('Ask your assistant anything')) return true;
|
|
if (await textExists('Your device is connected')) return true;
|
|
if (await textExists('Welcome')) return true;
|
|
if (await textExists('Get Started')) return true;
|
|
await browser.pause(700);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Create a TCP listener on the given port to simulate an unrelated process
|
|
* occupying that port. Returns a cleanup function that closes the server.
|
|
*
|
|
* Note: this helper runs in the Node test process, not inside the Tauri
|
|
* WebView, so `net` from Node stdlib is available.
|
|
*/
|
|
async function bindPort(port: number): Promise<() => Promise<void>> {
|
|
return new Promise((resolve, reject) => {
|
|
const server = net.createServer();
|
|
server.listen(port, '127.0.0.1', () => {
|
|
stepLog(`pre-bound port ${port} to simulate conflict`);
|
|
resolve(() => new Promise<void>((res, rej) => server.close(err => (err ? rej(err) : res()))));
|
|
});
|
|
server.on('error', reject);
|
|
});
|
|
}
|
|
|
|
describe('Core port conflict recovery', () => {
|
|
before(async () => {
|
|
stepLog('starting mock server');
|
|
await startMockServer();
|
|
});
|
|
|
|
after(async () => {
|
|
stepLog('stopping mock server');
|
|
await stopMockServer();
|
|
});
|
|
|
|
// NOTE on scope: the Tauri harness boots the app before any spec runs, so
|
|
// we cannot pre-bind DEFAULT_CORE_PORT before the embedded core attempts to
|
|
// listen. This case therefore validates startup integrity (core started and
|
|
// app reached a usable screen) rather than the port-conflict fallback branch.
|
|
// The conflict path (bind port → trigger restart → assert fallback) is
|
|
// exercised in 4.2.2 once the UI dialog for that scenario is implemented.
|
|
it('4.2.1 — app reaches usable state on normal startup (startup-integrity check)', async () => {
|
|
stepLog('app is already running — verify it reached usable state', {
|
|
defaultCorePort: DEFAULT_CORE_PORT,
|
|
});
|
|
|
|
// The Tauri app has already been launched by the test harness before
|
|
// this spec runs. We cannot pre-bind the port before app launch from
|
|
// within a spec (the app boots earlier). This case therefore validates
|
|
// the app's normal startup: if the app reached the home/onboarding
|
|
// screen without crashing, the embedded core started cleanly.
|
|
await waitForApp();
|
|
|
|
const onHome = await waitForHome(25_000);
|
|
stepLog('app reached usable state', { onHome });
|
|
expect(onHome).toBe(true);
|
|
});
|
|
|
|
// TODO: Remove .skip when a user-visible port-conflict dialog is implemented.
|
|
// The embedded core currently falls back to a higher port silently (no UI
|
|
// dialog). Once a conflict dialog is added, assert its text here.
|
|
it.skip('4.2.2 — second instance surfaces clear conflict dialog', async () => {
|
|
// Placeholder: bind port 7788 from Node, then trigger a core restart via
|
|
// the Tauri `restart_core_process` command, and assert the UI shows a
|
|
// "port conflict" or "core unavailable" dialog.
|
|
//
|
|
// Gap: the dialog does not yet exist. Filed as a product gap in
|
|
// app/src-tauri/src/core_process.rs — the `ListenerKind::Unknown` branch
|
|
// logs the conflict but does not emit a Tauri event that the frontend
|
|
// renders.
|
|
let release: (() => Promise<void>) | undefined;
|
|
try {
|
|
release = await bindPort(DEFAULT_CORE_PORT);
|
|
await browser.execute(() => {
|
|
// Trigger a core restart to exercise the port-conflict path.
|
|
// @ts-ignore — invoke is set by the Tauri runtime
|
|
if (typeof window.__TAURI_INTERNALS__?.invoke === 'function') {
|
|
window.__TAURI_INTERNALS__.invoke('restart_core_process');
|
|
}
|
|
});
|
|
await browser.pause(5_000);
|
|
const hasConflictUI = await waitForText('port conflict', 10_000)
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
// Assert the gap explicitly so CI flags this as a known TODO, not a
|
|
// silent pass.
|
|
expect(hasConflictUI).toBe(true);
|
|
} finally {
|
|
await release?.();
|
|
}
|
|
});
|
|
});
|