fix(gateway): handle Windows OpenClaw process exit error during in-process restarts (#794)
Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Haze <hazeone@users.noreply.github.com>
This commit is contained in:
146
tests/e2e/gateway-lifecycle.spec.ts
Normal file
146
tests/e2e/gateway-lifecycle.spec.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { completeSetup, expect, installIpcMocks, test } from './fixtures/electron';
|
||||
|
||||
test.describe('ClawX gateway lifecycle resilience', () => {
|
||||
test('app remains fully navigable while gateway is disconnected', async ({ page }) => {
|
||||
// In E2E mode, gateway auto-start is skipped, so the app starts
|
||||
// with gateway in "stopped" state — simulating the disconnected scenario.
|
||||
await completeSetup(page);
|
||||
|
||||
// Navigate through all major pages to verify nothing crashes
|
||||
// when the gateway is not running.
|
||||
await page.getByTestId('sidebar-nav-models').click();
|
||||
await expect(page.getByTestId('models-page')).toBeVisible();
|
||||
|
||||
await page.getByTestId('sidebar-nav-agents').click();
|
||||
await expect(page.getByTestId('agents-page')).toBeVisible();
|
||||
|
||||
await page.getByTestId('sidebar-nav-channels').click();
|
||||
await expect(page.getByTestId('channels-page')).toBeVisible();
|
||||
|
||||
await page.getByTestId('sidebar-nav-settings').click();
|
||||
await expect(page.getByTestId('settings-page')).toBeVisible();
|
||||
|
||||
// Navigate back to chat — the gateway status indicator should be visible
|
||||
await page.getByTestId('sidebar-new-chat').click();
|
||||
// Verify the page didn't crash; main layout should still be stable
|
||||
await expect(page.getByTestId('main-layout')).toBeVisible();
|
||||
});
|
||||
|
||||
test('gateway status indicator updates when status transitions occur', async ({ electronApp, page }) => {
|
||||
await completeSetup(page);
|
||||
|
||||
// Mock the initial gateway status as "stopped"
|
||||
await installIpcMocks(electronApp, {
|
||||
gatewayStatus: { state: 'stopped', port: 18789 },
|
||||
});
|
||||
|
||||
// Simulate gateway status transitions by sending IPC events to the renderer.
|
||||
// This mimics the main process emitting gateway:status-changed events.
|
||||
|
||||
// Transition 1: stopped → starting
|
||||
await electronApp.evaluate(({ BrowserWindow }) => {
|
||||
const win = BrowserWindow.getAllWindows()[0];
|
||||
win?.webContents.send('gateway:status-changed', {
|
||||
state: 'starting',
|
||||
port: 18789,
|
||||
});
|
||||
});
|
||||
// Wait briefly for the renderer to process the IPC event
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Transition 2: starting → running
|
||||
await electronApp.evaluate(({ BrowserWindow }) => {
|
||||
const win = BrowserWindow.getAllWindows()[0];
|
||||
win?.webContents.send('gateway:status-changed', {
|
||||
state: 'running',
|
||||
port: 18789,
|
||||
pid: 12345,
|
||||
connectedAt: Date.now(),
|
||||
});
|
||||
});
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Verify navigation still works after status transitions
|
||||
await page.getByTestId('sidebar-nav-models').click();
|
||||
await expect(page.getByTestId('models-page')).toBeVisible();
|
||||
|
||||
// Transition 3: running → error (simulates the bug scenario where
|
||||
// gateway becomes unreachable after in-process restart)
|
||||
await electronApp.evaluate(({ BrowserWindow }) => {
|
||||
const win = BrowserWindow.getAllWindows()[0];
|
||||
win?.webContents.send('gateway:status-changed', {
|
||||
state: 'error',
|
||||
port: 18789,
|
||||
error: 'WebSocket closed before handshake',
|
||||
});
|
||||
});
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// App should still be functional in error state
|
||||
await page.getByTestId('sidebar-nav-agents').click();
|
||||
await expect(page.getByTestId('agents-page')).toBeVisible();
|
||||
|
||||
// Transition 4: error → reconnecting → running (the recovery path)
|
||||
await electronApp.evaluate(({ BrowserWindow }) => {
|
||||
const win = BrowserWindow.getAllWindows()[0];
|
||||
win?.webContents.send('gateway:status-changed', {
|
||||
state: 'reconnecting',
|
||||
port: 18789,
|
||||
reconnectAttempts: 1,
|
||||
});
|
||||
});
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
await electronApp.evaluate(({ BrowserWindow }) => {
|
||||
const win = BrowserWindow.getAllWindows()[0];
|
||||
win?.webContents.send('gateway:status-changed', {
|
||||
state: 'running',
|
||||
port: 18789,
|
||||
pid: 23456,
|
||||
connectedAt: Date.now(),
|
||||
});
|
||||
});
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Final navigation check to confirm app is still healthy after full lifecycle
|
||||
await page.getByTestId('sidebar-nav-settings').click();
|
||||
await expect(page.getByTestId('settings-page')).toBeVisible();
|
||||
await page.getByTestId('sidebar-new-chat').click();
|
||||
await expect(page.getByTestId('main-layout')).toBeVisible();
|
||||
});
|
||||
|
||||
test('app handles rapid gateway status transitions without crashing', async ({ electronApp, page }) => {
|
||||
await completeSetup(page);
|
||||
|
||||
// Simulate rapid status transitions like those seen in the bug log:
|
||||
// running → stopped → starting → error → reconnecting → running
|
||||
const states = [
|
||||
{ state: 'running', port: 18789, pid: 100 },
|
||||
{ state: 'stopped', port: 18789 },
|
||||
{ state: 'starting', port: 18789 },
|
||||
{ state: 'error', port: 18789, error: 'Port 18789 still occupied after 30000ms' },
|
||||
{ state: 'reconnecting', port: 18789, reconnectAttempts: 1 },
|
||||
{ state: 'starting', port: 18789 },
|
||||
{ state: 'running', port: 18789, pid: 200, connectedAt: Date.now() },
|
||||
];
|
||||
|
||||
for (const status of states) {
|
||||
await electronApp.evaluate(({ BrowserWindow }, s) => {
|
||||
const win = BrowserWindow.getAllWindows()[0];
|
||||
win?.webContents.send('gateway:status-changed', s);
|
||||
}, status);
|
||||
// Small delay between transitions to be more realistic
|
||||
await page.waitForTimeout(100);
|
||||
}
|
||||
|
||||
// Verify the app is still stable after rapid transitions
|
||||
await expect(page.getByTestId('main-layout')).toBeVisible();
|
||||
|
||||
// Navigate to verify no page is in a broken state
|
||||
await page.getByTestId('sidebar-nav-models').click();
|
||||
await expect(page.getByTestId('models-page')).toBeVisible();
|
||||
|
||||
await page.getByTestId('sidebar-nav-channels').click();
|
||||
await expect(page.getByTestId('channels-page')).toBeVisible();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user