import { describe, test, expect } from 'bun:test'; import * as fs from 'node:fs'; import * as path from 'node:path'; import * as os from 'node:os'; import { shouldSpawnXvfb, isOurXvfb, readPidStartTime, readPidCmdline, cleanupXvfb, pickFreeDisplay, isDisplayFree, } from '../src/xvfb'; const HAS_XVFB = (() => { if (process.platform !== 'linux') return false; const result = Bun.spawnSync(['which', 'Xvfb'], { stdout: 'pipe', stderr: 'pipe', timeout: 30_000 }); return result.exitCode === 0; })(); describe('shouldSpawnXvfb', () => { test('skips when not headed', () => { const d = shouldSpawnXvfb({}, 'linux'); expect(d.spawn).toBe(false); expect(d.reason).toContain('not headed'); }); test('skips on macOS even when headed', () => { const d = shouldSpawnXvfb({ BROWSE_HEADED: '1' }, 'darwin'); expect(d.spawn).toBe(false); expect(d.reason).toContain('darwin'); }); test('skips on Windows even when headed', () => { const d = shouldSpawnXvfb({ BROWSE_HEADED: '1' }, 'win32'); expect(d.spawn).toBe(false); expect(d.reason).toContain('win32'); }); test('skips on Linux when DISPLAY already set', () => { const d = shouldSpawnXvfb({ BROWSE_HEADED: '1', DISPLAY: ':0' }, 'linux'); expect(d.spawn).toBe(false); expect(d.reason).toContain('DISPLAY=:0'); }); test('skips on Linux when WAYLAND_DISPLAY set (codex F2)', () => { const d = shouldSpawnXvfb({ BROWSE_HEADED: '1', WAYLAND_DISPLAY: 'wayland-0' }, 'linux'); expect(d.spawn).toBe(false); expect(d.reason).toContain('Wayland'); }); test('spawns on Linux + headed + no DISPLAY/WAYLAND_DISPLAY', () => { const d = shouldSpawnXvfb({ BROWSE_HEADED: '1' }, 'linux'); expect(d.spawn).toBe(true); }); }); describe('isOurXvfb (PID validation)', () => { test('returns false when pid is 0', () => { expect(isOurXvfb(0, 'whatever')).toBe(false); }); test('returns false when startTime is empty', () => { expect(isOurXvfb(process.pid, '')).toBe(false); }); test('returns false when cmdline does not contain Xvfb', () => { // Current bun process is not Xvfb. PID-correct, cmdline-wrong → reject. // NOTE: this very suite's argv CONTAINS "xvfb.test.ts" — a substring // match over the whole cmdline identified the test runner as our Xvfb // on the first Linux CI run. Identity rests on argv[0]'s basename. const myStart = readPidStartTime(process.pid); expect(isOurXvfb(process.pid, myStart)).toBe(false); }); test('a process whose ARGUMENTS mention xvfb is not ours (argv0 identity)', async () => { // sh's $0 trick plants "xvfb" in the child's args while argv[0] stays sh. // Killing this process because its arguments mention xvfb is the exact // sibling-kill class the identity check exists to prevent. const child = Bun.spawn(['/bin/sh', '-c', 'sleep 2', 'xvfb-lookalike-arg']); try { const start = readPidStartTime(child.pid); // On non-Linux, /proc is absent and both reads return '' → false either way. expect(isOurXvfb(child.pid, start || 'recorded')).toBe(false); } finally { child.kill(); await child.exited; } }); test('returns false when start-time differs (PID reuse defense)', () => { // Even if we somehow had the right PID, a stale start-time means it's a // different process. We never fake the cmdline test, so this assertion // is structural: the function must not pass on stale start-time alone. expect(isOurXvfb(process.pid, 'Mon Jan 1 00:00:00 1970')).toBe(false); }); }); describe('readPidStartTime', () => { test('returns non-empty for current process', () => { if (process.platform === 'win32') return; // ps not available const t = readPidStartTime(process.pid); expect(t.length).toBeGreaterThan(0); }); test('returns empty string for nonexistent PID', () => { expect(readPidStartTime(99999999)).toBe(''); }); }); describe('readPidCmdline', () => { test('returns non-empty for current process on Linux', () => { if (process.platform !== 'linux') return; // /proc unavailable const c = readPidCmdline(process.pid); expect(c.length).toBeGreaterThan(0); }); test('returns empty for nonexistent PID', () => { expect(readPidCmdline(99999999)).toBe(''); }); }); describe('cleanupXvfb', () => { test('no-op when pid is 0', () => { expect(() => cleanupXvfb({ pid: 0, startTime: '', display: ':99' })).not.toThrow(); }); test('no-op when not our Xvfb (won\'t kill unrelated process)', () => { // Pass the current bun process's PID + a stale start-time. cleanupXvfb // should refuse to send signals because cmdline doesn't match Xvfb. expect(() => cleanupXvfb({ pid: process.pid, startTime: 'Mon Jan 1 00:00:00 1970', display: ':99', })).not.toThrow(); // The current process is still alive after the no-op cleanup attempt. expect(process.kill(process.pid, 0)).toBe(true); }); }); describe('pickFreeDisplay (Xvfb installed)', () => { test.skipIf(!HAS_XVFB)('returns a number in the requested range', () => { const n = pickFreeDisplay(99, 105); if (n != null) { expect(n).toBeGreaterThanOrEqual(99); expect(n).toBeLessThanOrEqual(105); } // null means all displays in range are busy — also valid. }); test.skipIf(!HAS_XVFB)('isDisplayFree returns boolean', () => { const result = isDisplayFree(99); expect(typeof result).toBe('boolean'); }); }); describe('xvfb spawn → cleanup round trip (Linux + Xvfb only)', () => { test.skipIf(!HAS_XVFB)('spawn, validate ownership, cleanup', async () => { const { spawnXvfb } = await import('../src/xvfb'); const display = pickFreeDisplay(99, 110); if (display == null) { // No free display in range — skip. return; } const handle = await spawnXvfb(display); try { expect(handle.pid).toBeGreaterThan(0); expect(handle.display).toBe(`:${display}`); expect(handle.startTime.length).toBeGreaterThan(0); // Validation should pass. expect(isOurXvfb(handle.pid, handle.startTime)).toBe(true); const lockPath = `/tmp/.X${display}-lock`; const lock = fs.readFileSync(lockPath, 'utf8'); cleanupXvfb({ ...handle, startTime: 'stale-start-time' }); expect(isOurXvfb(handle.pid, handle.startTime)).toBe(true); await expect(spawnXvfb(display)).rejects.toThrow('already reserved'); expect(fs.readFileSync(lockPath, 'utf8')).toBe(lock); expect(isOurXvfb(handle.pid, handle.startTime)).toBe(true); } finally { handle.close(); // After cleanup, our Xvfb should be gone. await new Promise((r) => setTimeout(r, 200)); expect(isOurXvfb(handle.pid, handle.startTime)).toBe(false); } }); }); describe.skipIf(process.platform !== 'linux')('display allocation failure controls', () => { test('missing ownership tooling fails before spawning Xvfb', () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-xvfb-no-ps-')); const marker = path.join(root, 'spawned'); fs.writeFileSync(path.join(root, 'Xvfb'), `#!/bin/sh\nprintf started > ${JSON.stringify(marker)}\nexit 0\n`, { mode: 0o755 }); try { const display = pickFreeDisplay(); expect(display).not.toBeNull(); const child = Bun.spawnSync([process.execPath, '-e', ` import { spawnXvfb } from ${JSON.stringify(path.resolve(import.meta.dir, '../src/xvfb.ts'))}; try { const handle = await spawnXvfb(${display}); handle.close(); process.exitCode = 1; } catch (err) { console.log(err.message); } `], { env: { ...process.env, PATH: root }, stdout: 'pipe', stderr: 'pipe', timeout: 10000 }); expect(child.exitCode).toBe(0); expect(child.stdout.toString()).toContain('without process start-time ownership checks'); expect(fs.existsSync(marker)).toBe(false); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('an unreachable reserved display is not free and its lock is not removed', async () => { const display = pickFreeDisplay(20000, 20100); expect(display).not.toBeNull(); const lockPath = `/tmp/.X${display}-lock`; fs.writeFileSync(lockPath, `${process.pid}\n`, { flag: 'wx' }); const inode = fs.statSync(lockPath).ino; try { expect(isDisplayFree(display!)).toBe(false); expect(pickFreeDisplay(display!, display!)).toBeNull(); const { spawnXvfb } = await import('../src/xvfb'); await expect(spawnXvfb(display!)).rejects.toThrow('already reserved'); expect(fs.statSync(lockPath).ino).toBe(inode); expect(fs.readFileSync(lockPath, 'utf8')).toBe(`${process.pid}\n`); } finally { if (fs.statSync(lockPath).ino === inode) fs.unlinkSync(lockPath); } }); test('a dangling display lock remains reserved and is not replaced', async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-display-link-')); const display = pickFreeDisplay(21000, 21100); expect(display).not.toBeNull(); const lockPath = `/tmp/.X${display}-lock`; const target = path.join(root, 'missing-owner'); fs.symlinkSync(target, lockPath); try { expect(isDisplayFree(display!)).toBe(false); const { spawnXvfb } = await import('../src/xvfb'); await expect(spawnXvfb(display!)).rejects.toThrow('already reserved'); expect(fs.readlinkSync(lockPath)).toBe(target); } finally { if (fs.readlinkSync(lockPath) === target) fs.unlinkSync(lockPath); fs.rmSync(root, { recursive: true, force: true }); } }); test('a failed Xvfb process reports startup failure without claiming a display', async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-xvfb-failure-')); fs.writeFileSync(path.join(root, 'Xvfb'), '#!/bin/sh\nexit 42\n', { mode: 0o755 }); try { const display = pickFreeDisplay(); expect(display).not.toBeNull(); const child = Bun.spawnSync([process.execPath, '-e', ` import { spawnXvfb } from ${JSON.stringify(path.resolve(import.meta.dir, '../src/xvfb.ts'))}; try { const handle = await spawnXvfb(${display}); handle.close(); process.exitCode = 1; } catch (err) { console.log(err.message); } `], { env: { ...process.env, PATH: `${root}:${process.env.PATH}` }, stdout: 'pipe', stderr: 'pipe', timeout: 10000 }); expect(child.exitCode).toBe(0); expect(child.stdout.toString()).toContain('exited during startup (code 42)'); expect(isDisplayFree(display!)).toBe(true); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); }); describe.skipIf(process.platform !== 'linux')('daemon-owned display lifecycle', () => { test('registered shutdown waits for owned display allocation before process exit', async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-display-exit-')); const marker = path.join(root, 'xvfb.pid'); const realXvfb = Bun.which('Xvfb'); expect(realXvfb).not.toBeNull(); fs.writeFileSync(path.join(root, 'Xvfb'), `#!/bin/sh\nprintf '%s' "$$" > ${JSON.stringify(marker)}\n/bin/sleep 0.8\nexec ${JSON.stringify(realXvfb)} "$@"\n`, { mode: 0o755 }); const script = path.join(root, 'shutdown.ts'); fs.writeFileSync(script, ` import { BrowserManager } from ${JSON.stringify(path.resolve(import.meta.dir, '../src/browser-manager.ts'))}; import { buildFetchHandler, resolveConfigFromEnv } from ${JSON.stringify(path.resolve(import.meta.dir, '../src/server.ts'))}; const manager = new BrowserManager(); await manager.launch(); manager.closeRaceMs = 50; const handler = buildFetchHandler({ ...resolveConfigFromEnv(), browserManager: manager }); void manager.ensureHeadedDisplay().catch(() => {}); await handler.shutdown(); `); const child = Bun.spawn([process.execPath, script], { cwd: root, env: { ...process.env, HOME: root, PATH: `${root}:${process.env.PATH}`, DISPLAY: '', WAYLAND_DISPLAY: '', BROWSE_HEADED: '', BROWSE_PARENT_PID: '0', GSTACK_HOME: path.join(root, 'home-state'), BROWSE_STATE_FILE: path.join(root, 'state', 'browse.json'), CHROMIUM_PROFILE: path.join(root, 'profile'), GSTACK_CHROMIUM_NO_SANDBOX: '1', PLAYWRIGHT_BROWSERS_PATH: process.env.PLAYWRIGHT_BROWSERS_PATH || path.join(os.homedir(), '.cache', 'ms-playwright'), }, stdin: 'ignore', stdout: 'ignore', stderr: 'ignore', }); let pid = 0; let startTime = ''; try { const deadline = Date.now() + 10000; while (!fs.existsSync(marker) && Date.now() < deadline) await Bun.sleep(20); expect(fs.existsSync(marker)).toBe(true); pid = Number(fs.readFileSync(marker, 'utf8')); startTime = readPidStartTime(pid); expect(startTime).not.toBe(''); expect(await Promise.race([child.exited, Bun.sleep(10000).then(() => 'timeout')])).toBe(0); await Bun.sleep(1000); expect(isOurXvfb(pid, startTime)).toBe(false); } finally { if (child.exitCode === null) { child.kill('SIGKILL'); await child.exited; } if (pid && startTime) cleanupXvfb({ pid, startTime, display: '' }); fs.rmSync(root, { recursive: true, force: true }); } }, 30000); for (const mode of ['welcome', 'welcome failure', 'shutdown', 'restored page', 'headless'] as const) { test(`${mode}: startup settles welcome before publishing daemon readiness`, async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-welcome-ready-')); const stateFile = path.join(root, 'state', 'browse.json'); const entered = path.join(root, 'entered'); const release = path.join(root, 'release'); const fixture = Bun.serve({ hostname: '127.0.0.1', port: 0, fetch: () => new Response('