add electron e2e harness and regression coverage (#697)

This commit is contained in:
Lingxuan Zuo
2026-03-28 15:34:20 +08:00
committed by GitHub
Unverified
parent 514a6c4112
commit 2668082809
22 changed files with 535 additions and 78 deletions

View File

@@ -1,6 +1,6 @@
import { randomBytes } from 'node:crypto';
import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';
import { PORTS } from '../utils/config';
import { getPort } from '../utils/config';
import { logger } from '../utils/logger';
import type { HostApiContext } from './context';
import { handleAppRoutes } from './routes/app';
@@ -53,7 +53,7 @@ export function getHostApiToken(): string {
return hostApiToken;
}
export function startHostApiServer(ctx: HostApiContext, port = PORTS.CLAWX_HOST_API): Server {
export function startHostApiServer(ctx: HostApiContext, port = getPort('CLAWX_HOST_API')): Server {
// Generate a cryptographically random token for this session.
hostApiToken = randomBytes(32).toString('hex');

View File

@@ -45,6 +45,12 @@ import { whatsAppLoginManager } from '../utils/whatsapp-login';
import { syncAllProviderAuthToRuntime } from '../services/providers/provider-runtime-sync';
const WINDOWS_APP_USER_MODEL_ID = 'app.clawx.desktop';
const isE2EMode = process.env.CLAWX_E2E === '1';
const requestedUserDataDir = process.env.CLAWX_USER_DATA_DIR?.trim();
if (isE2EMode && requestedUserDataDir) {
app.setPath('userData', requestedUserDataDir);
}
// Disable GPU hardware acceleration globally for maximum stability across
// all GPU configurations (no GPU, integrated, discrete).
@@ -75,14 +81,14 @@ if (process.platform === 'linux') {
// same port, then each treats the other's gateway as "orphaned" and kills
// it — creating an infinite kill/restart loop on Windows.
// The losing process must exit immediately so it never reaches Gateway startup.
const gotElectronLock = app.requestSingleInstanceLock();
const gotElectronLock = isE2EMode ? true : app.requestSingleInstanceLock();
if (!gotElectronLock) {
console.info('[ClawX] Another instance already holds the single-instance lock; exiting duplicate process');
app.exit(0);
}
let releaseProcessInstanceFileLock: () => void = () => {};
let gotFileLock = true;
if (gotElectronLock) {
if (gotElectronLock && !isE2EMode) {
try {
const fileLock = acquireProcessInstanceFileLock({
userDataDir: app.getPath('userData'),
@@ -190,7 +196,9 @@ function createWindow(): BrowserWindow {
// Load the app
if (process.env.VITE_DEV_SERVER_URL) {
win.loadURL(process.env.VITE_DEV_SERVER_URL);
win.webContents.openDevTools();
if (!isE2EMode) {
win.webContents.openDevTools();
}
} else {
win.loadFile(join(__dirname, '../../dist/index.html'));
}
@@ -265,15 +273,19 @@ async function initialize(): Promise<void> {
`Runtime: platform=${process.platform}/${process.arch}, electron=${process.versions.electron}, node=${process.versions.node}, packaged=${app.isPackaged}, pid=${process.pid}, ppid=${process.ppid}`
);
// Warm up network optimization (non-blocking)
void warmupNetworkOptimization();
if (!isE2EMode) {
// Warm up network optimization (non-blocking)
void warmupNetworkOptimization();
// Initialize Telemetry early
await initTelemetry();
// Initialize Telemetry early
await initTelemetry();
// Apply persisted proxy settings before creating windows or network requests.
await applyProxySettings();
await syncLaunchAtStartupSettingFromStore();
// Apply persisted proxy settings before creating windows or network requests.
await applyProxySettings();
await syncLaunchAtStartupSettingFromStore();
} else {
logger.info('Running in E2E mode: startup side effects minimized');
}
// Set application menu
createMenu();
@@ -282,7 +294,9 @@ async function initialize(): Promise<void> {
const window = createMainWindow();
// Create system tray
createTray(window);
if (!isE2EMode) {
createTray(window);
}
// Override security headers ONLY for the OpenClaw Gateway Control UI.
// The URL filter ensures this callback only fires for gateway requests,
@@ -326,34 +340,42 @@ async function initialize(): Promise<void> {
// Repair any bootstrap files that only contain ClawX markers (no OpenClaw
// template content). This fixes a race condition where ensureClawXContext()
// previously created the file before the gateway could seed the full template.
void repairClawXOnlyBootstrapFiles().catch((error) => {
logger.warn('Failed to repair bootstrap files:', error);
});
if (!isE2EMode) {
void repairClawXOnlyBootstrapFiles().catch((error) => {
logger.warn('Failed to repair bootstrap files:', error);
});
}
// Pre-deploy built-in skills (feishu-doc, feishu-drive, feishu-perm, feishu-wiki)
// to ~/.openclaw/skills/ so they are immediately available without manual install.
void ensureBuiltinSkillsInstalled().catch((error) => {
logger.warn('Failed to install built-in skills:', error);
});
if (!isE2EMode) {
void ensureBuiltinSkillsInstalled().catch((error) => {
logger.warn('Failed to install built-in skills:', error);
});
}
// Pre-deploy bundled third-party skills from resources/preinstalled-skills.
// This installs full skill directories (not only SKILL.md) in an idempotent,
// non-destructive way and never blocks startup.
void ensurePreinstalledSkillsInstalled().catch((error) => {
logger.warn('Failed to install preinstalled skills:', error);
});
if (!isE2EMode) {
void ensurePreinstalledSkillsInstalled().catch((error) => {
logger.warn('Failed to install preinstalled skills:', error);
});
}
// Pre-deploy/upgrade bundled OpenClaw plugins (dingtalk, wecom, qqbot, feishu, wechat)
// to ~/.openclaw/extensions/ so they are always up-to-date after an app update.
void ensureAllBundledPluginsInstalled().catch((error) => {
logger.warn('Failed to install/upgrade bundled plugins:', error);
});
if (!isE2EMode) {
void ensureAllBundledPluginsInstalled().catch((error) => {
logger.warn('Failed to install/upgrade bundled plugins:', error);
});
}
// Bridge gateway and host-side events before any auto-start logic runs, so
// renderer subscribers observe the full startup lifecycle.
gatewayManager.on('status', (status: { state: string }) => {
hostEventBus.emit('gateway:status', status);
if (status.state === 'running') {
if (status.state === 'running' && !isE2EMode) {
void ensureClawXContext().catch((error) => {
logger.warn('Failed to re-merge ClawX context after gateway reconnect:', error);
});
@@ -426,7 +448,7 @@ async function initialize(): Promise<void> {
// Start Gateway automatically (this seeds missing bootstrap files with full templates)
const gatewayAutoStart = await getSetting('gatewayAutoStart');
if (gatewayAutoStart) {
if (!isE2EMode && gatewayAutoStart) {
try {
await syncAllProviderAuthToRuntime();
logger.debug('Auto-starting Gateway...');
@@ -436,6 +458,8 @@ async function initialize(): Promise<void> {
logger.error('Gateway auto-start failed:', error);
mainWindow?.webContents.send('gateway:error', String(error));
}
} else if (isE2EMode) {
logger.info('Gateway auto-start skipped in E2E mode');
} else {
logger.info('Gateway auto-start disabled in settings');
}
@@ -443,19 +467,23 @@ async function initialize(): Promise<void> {
// Merge ClawX context snippets into the workspace bootstrap files.
// The gateway seeds workspace files asynchronously after its HTTP server
// is ready, so ensureClawXContext will retry until the target files appear.
void ensureClawXContext().catch((error) => {
logger.warn('Failed to merge ClawX context into workspace:', error);
});
if (!isE2EMode) {
void ensureClawXContext().catch((error) => {
logger.warn('Failed to merge ClawX context into workspace:', error);
});
}
// Auto-install openclaw CLI and shell completions (non-blocking).
void autoInstallCliIfNeeded((installedPath) => {
mainWindow?.webContents.send('openclaw:cli-installed', installedPath);
}).then(() => {
generateCompletionCache();
installCompletionToProfile();
}).catch((error) => {
logger.warn('CLI auto-install failed:', error);
});
if (!isE2EMode) {
void autoInstallCliIfNeeded((installedPath) => {
mainWindow?.webContents.send('openclaw:cli-installed', installedPath);
}).then(() => {
generateCompletionCache();
installCompletionToProfile();
}).catch((error) => {
logger.warn('CLI auto-install failed:', error);
});
}
}
if (gotTheLock) {

View File

@@ -1,6 +1,6 @@
import { ipcMain } from 'electron';
import { proxyAwareFetch } from '../../utils/proxy-fetch';
import { PORTS } from '../../utils/config';
import { getPort } from '../../utils/config';
import { getHostApiToken } from '../../api/server';
type HostApiFetchRequest = {
@@ -11,6 +11,8 @@ type HostApiFetchRequest = {
};
export function registerHostApiProxyHandlers(): void {
const hostApiPort = getPort('CLAWX_HOST_API');
// Expose the per-session auth token to the renderer so the browser-fallback
// path in host-api.ts can authenticate against the Host API server.
ipcMain.handle('hostapi:token', () => getHostApiToken());
@@ -41,7 +43,7 @@ export function registerHostApiProxyHandlers(): void {
}
}
const response = await proxyAwareFetch(`http://127.0.0.1:${PORTS.CLAWX_HOST_API}${path}`, {
const response = await proxyAwareFetch(`http://127.0.0.1:${hostApiPort}${path}`, {
method,
headers,
body,