@@ -28,6 +28,7 @@ let mockBundleApi: ChildProcess | null = null;
2828let stubMcpHttp : ChildProcess | null = null ;
2929let stubMcpOauth : ChildProcess | null = null ;
3030let 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
202203function 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 {
229237function 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