Skip to content

Commit fb90baa

Browse files
committed
fix: harden Linux e2e desktop tests against transient failures
- Remove process.exit(1) from tauri-driver crash handlers so JUnit XML reports are properly finalized (prevents malformed wdio-junit-*.xml) - Use pkill without -f on Linux (match process name, not full cmdline) to avoid accidentally killing unrelated processes - Force-kill lingering tauri-driver processes between specs to free port 4444 on Linux - Add specFileRetries=1 on CI to retry transient WebKit2GTK crashes - Add 1s pause in afterSession for Linux process teardown - Wrap screenshot capture in try/catch for tauri-driver crash scenarios https://claude.ai/code/session_01N5WUgi8V1zVQBHVw96eVzM Signed-off-by: Claude <noreply@anthropic.com>
1 parent 1eb9eef commit fb90baa

1 file changed

Lines changed: 48 additions & 12 deletions

File tree

tests/e2e/wdio.conf.ts

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ let mockBundleApi: ChildProcess | null = null;
2828
let stubMcpHttp: ChildProcess | null = null;
2929
let stubMcpOauth: ChildProcess | null = null;
3030
let shouldExit = false;
31+
let tauriDriverCrashed = false;
3132

3233
// Mock server ports
3334
// Use 8787 for bundle API because that's the app's default MCPMUX_REGISTRY_URL
@@ -201,8 +202,15 @@ function stopMockServers(): void {
201202

202203
function closeTauriDriver() {
203204
shouldExit = true;
204-
tauriDriver?.kill();
205+
if (tauriDriver) {
206+
tauriDriver.kill();
207+
}
205208
stopMockServers();
209+
// Kill any lingering tauri-driver processes on Linux to free port 4444.
210+
// The tauriDriver.kill() above sends SIGTERM which may not exit promptly.
211+
if (process.platform !== 'win32') {
212+
spawnSync('pkill', ['-9', 'tauri-driver'], { stdio: 'ignore' });
213+
}
206214
}
207215

208216
// Kill any processes listening on our mock server ports (leftover from previous runs)
@@ -229,11 +237,12 @@ function killPortProcesses(): void {
229237
function killMcpmuxProcesses(): void {
230238
try {
231239
if (process.platform === 'win32') {
232-
// On Windows, use taskkill
240+
// On Windows, use taskkill (matches exact process name)
233241
spawnSync('taskkill', ['/F', '/IM', 'mcpmux.exe'], { stdio: 'ignore' });
234242
} else {
235-
// On Linux, use pkill
236-
spawnSync('pkill', ['-9', '-f', 'mcpmux'], { stdio: 'ignore' });
243+
// On Linux, kill only the exact mcpmux binary (not anything with "mcpmux" in its cmdline).
244+
// pkill without -f matches the process name only, which is safer than -f (full cmdline).
245+
spawnSync('pkill', ['-9', 'mcpmux'], { stdio: 'ignore' });
237246
}
238247
console.log('[e2e] Killed any existing mcpmux processes');
239248
} catch (error) {
@@ -289,6 +298,10 @@ export const config: Options.Testrunner = {
289298
specs: ['./specs/**/*.wdio.ts'],
290299
exclude: [],
291300

301+
// Retry failed spec files once to handle transient CI failures
302+
// (e.g., WebKit2GTK driver crashes, timing issues on slow CI runners)
303+
specFileRetries: process.env.CI ? 1 : 0,
304+
292305
maxInstances: 1, // Tauri only supports one instance
293306

294307
capabilities: [
@@ -334,8 +347,15 @@ export const config: Options.Testrunner = {
334347
// Sanitize test title: replace invalid filename chars (NTFS: " : < > | * ? \r \n) and spaces
335348
const safeTitle = test.title.replace(/[":*?<>|\r\n\\\/]+/g, '-').replace(/\s+/g, '_');
336349
const filename = `./tests/e2e/screenshots/FAIL-${safeTitle}-${timestamp}.png`;
337-
await browser.saveScreenshot(filename);
338-
console.log(`[e2e] Screenshot saved: ${filename}`);
350+
try {
351+
await browser.saveScreenshot(filename);
352+
console.log(`[e2e] Screenshot saved: ${filename}`);
353+
} catch {
354+
// Screenshot may fail if tauri-driver crashed
355+
if (tauriDriverCrashed) {
356+
console.error(`[e2e] Cannot save screenshot - tauri-driver crashed`);
357+
}
358+
}
339359
}
340360
},
341361

@@ -360,10 +380,13 @@ export const config: Options.Testrunner = {
360380
// Verify app is built
361381
checkAppBuilt();
362382

363-
// Kill any leftover mcpmux processes and clear all app data BEFORE
383+
// Kill any leftover mcpmux and tauri-driver processes and clear all app data BEFORE
364384
// tauri-driver starts the app. This avoids EBUSY errors from trying
365385
// to delete the SQLite DB while the app still holds a lock on it.
366386
killMcpmuxProcesses();
387+
if (process.platform !== 'win32') {
388+
spawnSync('pkill', ['-9', 'tauri-driver'], { stdio: 'ignore' });
389+
}
367390
// Brief pause to let processes fully exit
368391
await new Promise((resolve) => setTimeout(resolve, 2000));
369392
clearSingleInstanceLock();
@@ -382,6 +405,9 @@ export const config: Options.Testrunner = {
382405

383406
// Start tauri-driver before the session starts
384407
beforeSession: function () {
408+
shouldExit = false;
409+
tauriDriverCrashed = false;
410+
385411
const tauriDriverPath = path.resolve(
386412
os.homedir(),
387413
'.cargo',
@@ -400,24 +426,34 @@ export const config: Options.Testrunner = {
400426

401427
tauriDriver.on('error', (error) => {
402428
console.error('[tauri-driver] Error:', error);
403-
process.exit(1);
429+
// Don't call process.exit(1) - it kills the worker before JUnit XML
430+
// reports are finalized, resulting in malformed/empty XML files.
431+
// Let WebdriverIO handle the failure naturally via connection errors.
432+
tauriDriverCrashed = true;
404433
});
405434

406435
tauriDriver.on('exit', (code) => {
407436
if (!shouldExit) {
408-
console.error('[tauri-driver] Exited with code:', code);
409-
process.exit(1);
437+
console.error('[tauri-driver] Exited unexpectedly with code:', code);
438+
// Don't call process.exit(1) - let the test fail gracefully so that
439+
// JUnit XML reports are properly written. WebdriverIO will detect
440+
// the broken connection and fail the affected tests.
441+
tauriDriverCrashed = true;
410442
}
411443
});
412444
},
413445

414446
// Stop tauri-driver after the session
415-
afterSession: function () {
447+
afterSession: async function () {
416448
closeTauriDriver();
417-
449+
418450
// Kill the mcpmux app process and clear lock to prevent conflicts between test workers
419451
killMcpmuxProcesses();
420452
clearSingleInstanceLock();
453+
454+
// Brief pause to let processes fully exit before the next spec's beforeSession
455+
// starts a fresh tauri-driver. On Linux CI, process teardown can be slower.
456+
await new Promise((resolve) => setTimeout(resolve, 1000));
421457
},
422458

423459
// Clean up mock servers after all tests complete

0 commit comments

Comments
 (0)