Skip to content

Commit 09c19aa

Browse files
committed
fix: prevent cascading e2e test failures after tauri-driver crash
The root cause of the Linux CI failure was that when server-lifecycle.wdio.ts (worker 0-7) crashed, ALL subsequent workers (0-8 through 0-12) failed with "Failed to create a session" due to cascading process cleanup issues. The problem was in the afterSession/beforeSession lifecycle: - afterSession called pkill -9 tauri-driver while WebdriverIO's deleteSession was still in-flight, causing connection errors - afterSession called stopMockServers() which could kill mock servers needed by subsequent specs - No readiness check existed, so new tauri-driver instances started before the previous one fully released resources (ports, locks) Changes: - Move aggressive cleanup (pkill -9, killMcpmuxProcesses, fuser) from afterSession to beforeSession, executed BEFORE spawning new tauri-driver - afterSession now only sends graceful SIGTERM via closeTauriDriver() - Remove stopMockServers() from closeTauriDriver() (mock servers are started once in onPrepare and should live across all specs) - Add waitForTauriDriverReady() health check that polls GET /status before letting WebdriverIO create a session - Free gateway port 45818 between specs to prevent binding conflicts - Add stopMockServers() to onShutdown handler as safety net Result: 12/13 specs pass (up from 7/13). Only the actually crashing spec (server-lifecycle.wdio.ts) fails; all subsequent specs recover cleanly. Signed-off-by: Claude <noreply@anthropic.com> https://claude.ai/code/session_01N5WUgi8V1zVQBHVw96eVzM Signed-off-by: Claude <noreply@anthropic.com>
1 parent c71bee5 commit 09c19aa

1 file changed

Lines changed: 59 additions & 15 deletions

File tree

tests/e2e/wdio.conf.ts

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -204,13 +204,32 @@ function closeTauriDriver() {
204204
shouldExit = true;
205205
if (tauriDriver) {
206206
tauriDriver.kill();
207+
tauriDriver = null;
207208
}
208-
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' });
209+
// NOTE: Do NOT pkill tauri-driver or stop mock servers here.
210+
// This function is called during afterSession while WebdriverIO's own
211+
// deleteSession is still in-flight. Killing tauri-driver at this point
212+
// causes connection errors that cascade to all subsequent workers.
213+
// Aggressive cleanup is done in beforeSession instead, before spawning
214+
// a fresh tauri-driver.
215+
}
216+
217+
// Wait for tauri-driver to accept WebDriver connections on port 4444.
218+
// Polls GET /status until tauri-driver responds (any HTTP response means it's ready).
219+
async function waitForTauriDriverReady(timeout = 30000): Promise<boolean> {
220+
const start = Date.now();
221+
while (Date.now() - start < timeout) {
222+
try {
223+
await fetch('http://localhost:4444/status');
224+
console.log('[e2e] tauri-driver is ready on port 4444');
225+
return true;
226+
} catch {
227+
// Not ready yet (ECONNREFUSED)
228+
}
229+
await new Promise((resolve) => setTimeout(resolve, 500));
213230
}
231+
console.error(`[e2e] tauri-driver not ready after ${timeout}ms`);
232+
return false;
214233
}
215234

216235
// Kill any processes listening on our mock server ports (leftover from previous runs)
@@ -277,9 +296,10 @@ function onShutdown(fn: () => void) {
277296
process.on('SIGTERM', cleanup);
278297
}
279298

280-
// Ensure tauri-driver is closed when test process exits
299+
// Ensure tauri-driver and mock servers are closed when test process exits
281300
onShutdown(() => {
282301
closeTauriDriver();
302+
stopMockServers();
283303
});
284304

285305
export const config: Options.Testrunner = {
@@ -399,11 +419,32 @@ export const config: Options.Testrunner = {
399419
await startMockServers();
400420
},
401421

402-
// Start tauri-driver before the session starts
403-
beforeSession: function () {
422+
// Start tauri-driver before the session starts.
423+
// Performs aggressive cleanup of leftover processes from the previous spec
424+
// before spawning a fresh tauri-driver, then waits for it to be ready.
425+
beforeSession: async function () {
404426
shouldExit = false;
405427
tauriDriverCrashed = false;
406428

429+
// --- Aggressive cleanup from previous spec ---
430+
// Kill any leftover tauri-driver processes (may remain if previous spec crashed).
431+
// This is safe to do here because no tauri-driver should be running between specs.
432+
if (process.platform !== 'win32') {
433+
spawnSync('pkill', ['-9', 'tauri-driver'], { stdio: 'ignore' });
434+
}
435+
// Kill any leftover mcpmux app processes and clear single-instance lock
436+
killMcpmuxProcesses();
437+
clearSingleInstanceLock();
438+
439+
// Free the gateway port (45818) in case mcpmux didn't release it
440+
if (process.platform !== 'win32') {
441+
spawnSync('fuser', ['-k', '-9', '45818/tcp'], { stdio: 'ignore' });
442+
}
443+
444+
// Wait for OS to fully reclaim process resources (ports, file locks, etc.)
445+
await new Promise((resolve) => setTimeout(resolve, 2000));
446+
447+
// --- Spawn fresh tauri-driver ---
407448
const tauriDriverPath = path.resolve(
408449
os.homedir(),
409450
'.cargo',
@@ -437,18 +478,21 @@ export const config: Options.Testrunner = {
437478
tauriDriverCrashed = true;
438479
}
439480
});
481+
482+
// Wait for tauri-driver to be ready before letting WebdriverIO create a session.
483+
// Without this, WebdriverIO may send POST /session before tauri-driver is listening,
484+
// causing a 2-minute timeout (connectionRetryTimeout) and cascading failures.
485+
await waitForTauriDriverReady(30000);
440486
},
441487

442-
// Stop tauri-driver after the session
488+
// Stop tauri-driver after the session.
489+
// Uses graceful SIGTERM only — aggressive cleanup (pkill -9) is deferred to
490+
// the next spec's beforeSession to avoid racing with WebdriverIO's own
491+
// deleteSession call, which would cause cascading failures in subsequent specs.
443492
afterSession: async function () {
444493
closeTauriDriver();
445494

446-
// Kill the mcpmux app process and clear lock to prevent conflicts between test workers
447-
killMcpmuxProcesses();
448-
clearSingleInstanceLock();
449-
450-
// Brief pause to let processes fully exit before the next spec's beforeSession
451-
// starts a fresh tauri-driver. On Linux CI, process teardown can be slower.
495+
// Brief pause to let tauri-driver/mcpmux handle SIGTERM gracefully
452496
await new Promise((resolve) => setTimeout(resolve, 1000));
453497
},
454498

0 commit comments

Comments
 (0)