diff --git a/browse/src/server.ts b/browse/src/server.ts index 767a6f274..9b190b6ae 100644 --- a/browse/src/server.ts +++ b/browse/src/server.ts @@ -696,6 +696,13 @@ const BROWSE_PARENT_PID = parseInt(process.env.BROWSE_PARENT_PID || '0', 10); // the closure every 15s. The CLI's connect path sets BROWSE_HEADED=1 + PID=0, // so this branch is the normal path for /open-gstack-browser. const IS_HEADED_WATCHDOG = process.env.BROWSE_HEADED === '1'; +// Poll interval is env-tunable so the watchdog E2E test can use a ~250ms tick +// instead of waiting out the production 15s interval (was a 20s blind sleep). +// Floor of 50ms guards against a typo'd 0 busy-looping the server. +const WATCHDOG_INTERVAL_MS = (() => { + const raw = parseInt(process.env.BROWSE_WATCHDOG_INTERVAL_MS || '', 10); + return Number.isFinite(raw) && raw >= 50 ? raw : 15_000; +})(); if (BROWSE_PARENT_PID > 0 && !IS_HEADED_WATCHDOG) { let parentGone = false; setInterval(() => { @@ -723,7 +730,7 @@ if (BROWSE_PARENT_PID > 0 && !IS_HEADED_WATCHDOG) { console.log(`[browse] Parent process ${BROWSE_PARENT_PID} exited (server stays alive, idle timeout will clean up)`); } } - }, 15_000); + }, WATCHDOG_INTERVAL_MS); } else if (IS_HEADED_WATCHDOG) { console.log('[browse] Parent-process watchdog disabled (headed mode)'); } else if (BROWSE_PARENT_PID === 0) { diff --git a/browse/test/watchdog.test.ts b/browse/test/watchdog.test.ts index 42faa262a..53467f469 100644 --- a/browse/test/watchdog.test.ts +++ b/browse/test/watchdog.test.ts @@ -25,8 +25,9 @@ import * as os from 'os'; // /pair-agent users (server lingers after disconnect). // // Each test spawns the real server.ts. Tests 1 and 2 verify behavior via -// stdout log line (fast). Test 3 waits for the watchdog poll cycle to confirm -// the server REMAINS alive after parent death (slow — ~20s observation window). +// stdout log line (fast). Test 3 shrinks the watchdog tick to 250ms via +// BROWSE_WATCHDOG_INTERVAL_MS and waits for the stay-alive log line, then +// confirms the server survived parent death (~1-2s instead of a 20s sleep). const ROOT = path.resolve(import.meta.dir, '..'); const SERVER_SCRIPT = path.join(ROOT, 'src', 'server.ts'); @@ -137,21 +138,26 @@ describe('parent-process watchdog (v0.18.1.0)', () => { const parentPid = parentProc.pid!; // Default headless: no BROWSE_HEADED, real parent PID — watchdog active. - serverProc = spawnServer({ BROWSE_PARENT_PID: String(parentPid) }, 34903); + // 250ms tick (test-only knob) so this test doesn't wait out the + // production 15s interval; the old version blind-slept 22s. + serverProc = spawnServer( + { BROWSE_PARENT_PID: String(parentPid), BROWSE_WATCHDOG_INTERVAL_MS: '250' }, + 34903, + ); const serverPid = serverProc.pid!; - // Give the server a moment to start and register the watchdog interval. - await Bun.sleep(2000); + // Give the server a beat to register the watchdog interval. + await Bun.sleep(500); expect(isProcessAlive(serverPid)).toBe(true); - // Kill the parent. The watchdog polls every 15s, so first tick after - // parent death lands within ~15s. Pre-#994 the server would shutdown - // here. Post-#994 the server logs the parent exit and stays alive. + // Kill the parent. Pre-#994 the server would shut down on the next tick. + // Post-#994 it logs the parent exit and stays alive — wait for that log + // line instead of sleeping past a fixed interval. parentProc.kill('SIGKILL'); - - // Wait long enough for at least one watchdog tick (15s) plus margin. - // Server should still be alive — that's the whole point of #994. - await Bun.sleep(20_000); + const out = await readStdoutUntil(serverProc, 'server stays alive', 10_000); + expect(out).toContain( + `Parent process ${parentPid} exited (server stays alive, idle timeout will clean up)`, + ); expect(isProcessAlive(serverPid)).toBe(true); - }, 45_000); + }, 30_000); });