add electron e2e harness and regression coverage (#697)
This commit is contained in:
committed by
GitHub
Unverified
parent
514a6c4112
commit
2668082809
@@ -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');
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user