From c78e6bac0355ae4f7a90f0c694b8fc31ef574ce2 Mon Sep 17 00:00:00 2001 From: jesse23 Date: Tue, 21 Apr 2026 09:54:22 -0400 Subject: [PATCH 1/6] debug: add server+client logging to trace Windows disconnect Co-Authored-By: Claude Sonnet 4.6 --- src/client/index.ts | 8 +++++++- src/pty/bun.ts | 13 ++++++++++++- src/server/websocket.ts | 22 +++++++++++++++++++--- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/client/index.ts b/src/client/index.ts index b5d7649..36c607f 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -99,6 +99,7 @@ let ws: WebSocket; function connect(): void { const wsUrl = `${protocol}//${window.location.host}/ws/${sessionId}?cols=${term.cols}&rows=${term.rows}`; + console.log(`[webtty] connecting to ${wsUrl}`); ws = new WebSocket(wsUrl); const DIM = '\x1b[2m', @@ -109,15 +110,19 @@ function connect(): void { const msg = (text: string): string => `\r\n${tag} ${DIM}${ITALIC}${text}${RESET}\r\n`; ws.onopen = () => { + console.log(`[webtty] ws open cols=${term.cols} rows=${term.rows}`); ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows })); }; + let msgCount = 0; ws.onmessage = (event: MessageEvent) => { + if (msgCount++ < 5) console.log(`[webtty] ws message #${msgCount} ${event.data.length}B`); applyDecscusr(term, event.data); term.write(event.data); }; ws.onclose = (event: CloseEvent) => { + console.log(`[webtty] ws close code=${event.code} reason="${event.reason}" wasClean=${event.wasClean}`); if (event.code === 4001) { term.write(msg('Session removed.')); setTimeout(() => window.close(), 500); @@ -132,7 +137,8 @@ function connect(): void { setTimeout(connect, 2000); }; - ws.onerror = () => { + ws.onerror = (event: Event) => { + console.log(`[webtty] ws error`, event); term.write(msg('WebSocket error.')); }; } diff --git a/src/pty/bun.ts b/src/pty/bun.ts index cf5ced7..81cc711 100644 --- a/src/pty/bun.ts +++ b/src/pty/bun.ts @@ -20,20 +20,28 @@ export function spawn( ): PtyProcess { let onDataCb: ((data: string) => void) | undefined; let onExitCb: ((e: { exitCode: number }) => void) | undefined; + let dataCount = 0; + + console.log(`[pty:bun] spawn shell=${shell} cols=${cols} rows=${rows} cwd=${homedir()}`); const proc = Bun.spawn([shell], { terminal: { cols, rows, data(_term: unknown, data: Uint8Array) { - onDataCb?.(Buffer.from(data).toString('utf8')); + const str = Buffer.from(data).toString('utf8'); + if (dataCount++ < 5) console.log(`[pty:bun] data #${dataCount} ${str.length}B`); + onDataCb?.(str); }, }, cwd: homedir(), env: { ...process.env, TERM: term, COLORTERM: colorTerm }, }); + console.log(`[pty:bun] spawned pid=${proc.pid} terminal=${proc.terminal != null ? 'ok' : 'NULL'}`); + proc.exited.then((exitCode) => { + console.log(`[pty:bun] pid=${proc.pid} exited exitCode=${exitCode}`); onExitCb?.({ exitCode: exitCode ?? 0 }); }); @@ -46,12 +54,15 @@ export function spawn( onExitCb = cb; }, write(data) { + console.log(`[pty:bun] write ${JSON.stringify(data.slice(0, 40))}`); proc.terminal?.write(data); }, resize(cols, rows) { + console.log(`[pty:bun] resize cols=${cols} rows=${rows}`); proc.terminal?.resize(cols, rows); }, kill() { + console.log(`[pty:bun] kill pid=${proc.pid}`); proc.kill(); }, }; diff --git a/src/server/websocket.ts b/src/server/websocket.ts index b787c25..ee06dd4 100644 --- a/src/server/websocket.ts +++ b/src/server/websocket.ts @@ -99,17 +99,21 @@ export function createWebSocketServer(httpServer: http.Server): WebSocketServer httpServer.on('upgrade', (req, socket, head) => { const url = new URL(req.url ?? '/', `http://${req.headers.host ?? '127.0.0.1'}`); + console.log(`[ws] upgrade request: ${req.url}`); if (url.pathname.match(/^\/ws\/([^/]+)$/)) { wss.handleUpgrade(req, socket, head, (ws) => wss.emit('connection', ws, req)); } else { + console.log(`[ws] upgrade rejected (bad path): ${url.pathname}`); socket.destroy(); } }); wss.on('connection', (ws: WS, req: http.IncomingMessage) => { const url = new URL(req.url ?? '/', `http://${req.headers.host ?? '127.0.0.1'}`); + console.log(`[ws] connection: ${req.url}`); const wsMatch = url.pathname.match(/^\/ws\/([^/]+)$/); if (!wsMatch) { + console.log(`[ws] closing: no path match`); ws.close(); return; } @@ -118,6 +122,7 @@ export function createWebSocketServer(httpServer: http.Server): WebSocketServer try { id = decodeURIComponent(wsMatch[1]); } catch { + console.log(`[ws] closing: bad session id`); ws.close(WS_CLOSE.BAD_REQUEST, 'Bad Request'); return; } @@ -130,22 +135,28 @@ export function createWebSocketServer(httpServer: http.Server): WebSocketServer Math.min(500, Number.parseInt(url.searchParams.get('rows') ?? '24', 10) || 24), ); + console.log(`[ws] session id="${id}" cols=${cols} rows=${rows} registry_has=${sessionRegistry.has(id)}`); if (!sessionRegistry.has(id)) { + console.log(`[ws] closing 4001: session not in registry`); ws.close(WS_CLOSE.SESSION_GONE, 'session deleted'); return; } const session = sessionRegistry.get(id); if (!session) { + console.log(`[ws] closing 4001: session get returned undefined`); ws.close(WS_CLOSE.SESSION_GONE, 'session deleted'); return; } session.clients.add(ws); setLastUsedId(id); + console.log(`[ws] session found hasPty=${session.pty != null} clients=${session.clients.size}`); if (!session.pty) { const config = loadConfig(); + console.log(`[ws] spawning PTY shell=${config.shell}`); session.pty = spawnForSession(cols, rows, config.shell, config.term, config.colorTerm); + console.log(`[ws] PTY spawned pid=${session.pty.pid}`); session.pty.onData((data: string) => { session.scrollback = (session.scrollback + data).slice(-config.scrollback); @@ -156,10 +167,12 @@ export function createWebSocketServer(httpServer: http.Server): WebSocketServer } }); - session.pty.onExit(() => { + session.pty.onExit(({ exitCode }) => { + console.log(`[ws] PTY exited exitCode=${exitCode} session="${session.id}" clients=${session.clients.size}`); sessionRegistry.delete(session.id); for (const client of session.clients) { if (client.readyState === client.OPEN) { + console.log(`[ws] closing client 4001 (shell exited)`); client.close(WS_CLOSE.SESSION_GONE, 'shell exited'); } } @@ -196,11 +209,14 @@ export function createWebSocketServer(httpServer: http.Server): WebSocketServer session.pty?.write(message); }); - ws.on('close', () => { + ws.on('close', (code: number, reason: Buffer) => { + console.log(`[ws] client disconnected code=${code} reason="${reason.toString()}" session="${id}"`); session.clients.delete(ws); }); - ws.on('error', () => {}); + ws.on('error', (err: Error) => { + console.log(`[ws] client error: ${err.message}`); + }); }); return wss; From d66eff1148206049a42808545baa41f49f33d20b Mon Sep 17 00:00:00 2001 From: jesse23 Date: Tue, 21 Apr 2026 15:26:03 -0400 Subject: [PATCH 2/6] fix: spawn server with node on Windows to avoid Bun PTY incompatibility On Windows, two Bun-specific PTY paths fail: - Bun.spawn({ terminal }) throws "not supported on this platform" - @lydell/node-pty's ConPTY backend uses net.Socket({ fd }) to wrap named pipes, which Bun's net.Socket implementation does not support on Windows, causing pid=0 and ERR_SOCKET_CLOSED on the first write bunx webtty already works because its #!/usr/bin/env node shim runs the server with Node.js, where node-pty's ConPTY backend works correctly. Mirror that explicitly in startServer: on Windows under Bun, spawn the server process with `node` instead of process.execPath. Co-Authored-By: Claude Sonnet 4.6 --- src/cli/http.ts | 9 ++++++++- src/client/index.ts | 8 +------- src/pty/bun.ts | 13 +------------ src/server/websocket.ts | 22 +++------------------- 4 files changed, 13 insertions(+), 39 deletions(-) diff --git a/src/cli/http.ts b/src/cli/http.ts index 06ae6af..4e796df 100644 --- a/src/cli/http.ts +++ b/src/cli/http.ts @@ -56,7 +56,14 @@ export async function startServer(timeoutMs = 10000, _spawn = childProcess.spawn stdio = ['ignore', logFd, logFd]; } - const child = _spawn(process.execPath, [serverEntry], { + // On Windows, Bun's net.Socket doesn't support the fd-based named-pipe + // wrapping that node-pty's ConPTY backend requires, so node-pty fails under + // Bun on Windows. bunx works because its #!/usr/bin/env node shim runs the + // server with Node.js instead. Mirror that here explicitly. + const serverExec = + process.platform === 'win32' && process.versions.bun ? 'node' : process.execPath; + + const child = _spawn(serverExec, [serverEntry], { detached: true, stdio, env: { ...process.env, PORT: String(PORT) }, diff --git a/src/client/index.ts b/src/client/index.ts index 36c607f..b5d7649 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -99,7 +99,6 @@ let ws: WebSocket; function connect(): void { const wsUrl = `${protocol}//${window.location.host}/ws/${sessionId}?cols=${term.cols}&rows=${term.rows}`; - console.log(`[webtty] connecting to ${wsUrl}`); ws = new WebSocket(wsUrl); const DIM = '\x1b[2m', @@ -110,19 +109,15 @@ function connect(): void { const msg = (text: string): string => `\r\n${tag} ${DIM}${ITALIC}${text}${RESET}\r\n`; ws.onopen = () => { - console.log(`[webtty] ws open cols=${term.cols} rows=${term.rows}`); ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows })); }; - let msgCount = 0; ws.onmessage = (event: MessageEvent) => { - if (msgCount++ < 5) console.log(`[webtty] ws message #${msgCount} ${event.data.length}B`); applyDecscusr(term, event.data); term.write(event.data); }; ws.onclose = (event: CloseEvent) => { - console.log(`[webtty] ws close code=${event.code} reason="${event.reason}" wasClean=${event.wasClean}`); if (event.code === 4001) { term.write(msg('Session removed.')); setTimeout(() => window.close(), 500); @@ -137,8 +132,7 @@ function connect(): void { setTimeout(connect, 2000); }; - ws.onerror = (event: Event) => { - console.log(`[webtty] ws error`, event); + ws.onerror = () => { term.write(msg('WebSocket error.')); }; } diff --git a/src/pty/bun.ts b/src/pty/bun.ts index 81cc711..cf5ced7 100644 --- a/src/pty/bun.ts +++ b/src/pty/bun.ts @@ -20,28 +20,20 @@ export function spawn( ): PtyProcess { let onDataCb: ((data: string) => void) | undefined; let onExitCb: ((e: { exitCode: number }) => void) | undefined; - let dataCount = 0; - - console.log(`[pty:bun] spawn shell=${shell} cols=${cols} rows=${rows} cwd=${homedir()}`); const proc = Bun.spawn([shell], { terminal: { cols, rows, data(_term: unknown, data: Uint8Array) { - const str = Buffer.from(data).toString('utf8'); - if (dataCount++ < 5) console.log(`[pty:bun] data #${dataCount} ${str.length}B`); - onDataCb?.(str); + onDataCb?.(Buffer.from(data).toString('utf8')); }, }, cwd: homedir(), env: { ...process.env, TERM: term, COLORTERM: colorTerm }, }); - console.log(`[pty:bun] spawned pid=${proc.pid} terminal=${proc.terminal != null ? 'ok' : 'NULL'}`); - proc.exited.then((exitCode) => { - console.log(`[pty:bun] pid=${proc.pid} exited exitCode=${exitCode}`); onExitCb?.({ exitCode: exitCode ?? 0 }); }); @@ -54,15 +46,12 @@ export function spawn( onExitCb = cb; }, write(data) { - console.log(`[pty:bun] write ${JSON.stringify(data.slice(0, 40))}`); proc.terminal?.write(data); }, resize(cols, rows) { - console.log(`[pty:bun] resize cols=${cols} rows=${rows}`); proc.terminal?.resize(cols, rows); }, kill() { - console.log(`[pty:bun] kill pid=${proc.pid}`); proc.kill(); }, }; diff --git a/src/server/websocket.ts b/src/server/websocket.ts index ee06dd4..b787c25 100644 --- a/src/server/websocket.ts +++ b/src/server/websocket.ts @@ -99,21 +99,17 @@ export function createWebSocketServer(httpServer: http.Server): WebSocketServer httpServer.on('upgrade', (req, socket, head) => { const url = new URL(req.url ?? '/', `http://${req.headers.host ?? '127.0.0.1'}`); - console.log(`[ws] upgrade request: ${req.url}`); if (url.pathname.match(/^\/ws\/([^/]+)$/)) { wss.handleUpgrade(req, socket, head, (ws) => wss.emit('connection', ws, req)); } else { - console.log(`[ws] upgrade rejected (bad path): ${url.pathname}`); socket.destroy(); } }); wss.on('connection', (ws: WS, req: http.IncomingMessage) => { const url = new URL(req.url ?? '/', `http://${req.headers.host ?? '127.0.0.1'}`); - console.log(`[ws] connection: ${req.url}`); const wsMatch = url.pathname.match(/^\/ws\/([^/]+)$/); if (!wsMatch) { - console.log(`[ws] closing: no path match`); ws.close(); return; } @@ -122,7 +118,6 @@ export function createWebSocketServer(httpServer: http.Server): WebSocketServer try { id = decodeURIComponent(wsMatch[1]); } catch { - console.log(`[ws] closing: bad session id`); ws.close(WS_CLOSE.BAD_REQUEST, 'Bad Request'); return; } @@ -135,28 +130,22 @@ export function createWebSocketServer(httpServer: http.Server): WebSocketServer Math.min(500, Number.parseInt(url.searchParams.get('rows') ?? '24', 10) || 24), ); - console.log(`[ws] session id="${id}" cols=${cols} rows=${rows} registry_has=${sessionRegistry.has(id)}`); if (!sessionRegistry.has(id)) { - console.log(`[ws] closing 4001: session not in registry`); ws.close(WS_CLOSE.SESSION_GONE, 'session deleted'); return; } const session = sessionRegistry.get(id); if (!session) { - console.log(`[ws] closing 4001: session get returned undefined`); ws.close(WS_CLOSE.SESSION_GONE, 'session deleted'); return; } session.clients.add(ws); setLastUsedId(id); - console.log(`[ws] session found hasPty=${session.pty != null} clients=${session.clients.size}`); if (!session.pty) { const config = loadConfig(); - console.log(`[ws] spawning PTY shell=${config.shell}`); session.pty = spawnForSession(cols, rows, config.shell, config.term, config.colorTerm); - console.log(`[ws] PTY spawned pid=${session.pty.pid}`); session.pty.onData((data: string) => { session.scrollback = (session.scrollback + data).slice(-config.scrollback); @@ -167,12 +156,10 @@ export function createWebSocketServer(httpServer: http.Server): WebSocketServer } }); - session.pty.onExit(({ exitCode }) => { - console.log(`[ws] PTY exited exitCode=${exitCode} session="${session.id}" clients=${session.clients.size}`); + session.pty.onExit(() => { sessionRegistry.delete(session.id); for (const client of session.clients) { if (client.readyState === client.OPEN) { - console.log(`[ws] closing client 4001 (shell exited)`); client.close(WS_CLOSE.SESSION_GONE, 'shell exited'); } } @@ -209,14 +196,11 @@ export function createWebSocketServer(httpServer: http.Server): WebSocketServer session.pty?.write(message); }); - ws.on('close', (code: number, reason: Buffer) => { - console.log(`[ws] client disconnected code=${code} reason="${reason.toString()}" session="${id}"`); + ws.on('close', () => { session.clients.delete(ws); }); - ws.on('error', (err: Error) => { - console.log(`[ws] client error: ${err.message}`); - }); + ws.on('error', () => {}); }); return wss; From fdade4717c9edab5c04a0ec8cd9cbd03116ca678 Mon Sep 17 00:00:00 2001 From: jesse23 Date: Tue, 21 Apr 2026 21:59:04 -0400 Subject: [PATCH 3/6] =?UTF-8?q?fix:=20address=20code=20review=20=E2=80=94?= =?UTF-8?q?=20.ts=20entry=20guard=20and=20spawn=20error=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - When serverExec is `node`, always resolve to the compiled `.js` entry since Node.js cannot execute `.ts` files directly (unlike Bun) - Add a spawn `error` event handler so a missing `node` binary on PATH surfaces immediately with a clear message rather than timing out Co-Authored-By: Claude Sonnet 4.6 --- src/cli/http.ts | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/cli/http.ts b/src/cli/http.ts index 4e796df..d63d477 100644 --- a/src/cli/http.ts +++ b/src/cli/http.ts @@ -39,7 +39,18 @@ export async function isServerRunning(): Promise { */ export async function startServer(timeoutMs = 10000, _spawn = childProcess.spawn): Promise { const isBun = typeof (globalThis as Record).Bun !== 'undefined'; - const isTs = isBun && __filename.endsWith('.ts'); + + // On Windows, Bun's net.Socket doesn't support the fd-based named-pipe + // wrapping that node-pty's ConPTY backend requires, so node-pty fails under + // Bun on Windows. bunx works because its #!/usr/bin/env node shim runs the + // server with Node.js instead. Mirror that here explicitly. + const useNode = process.platform === 'win32' && isBun; + const serverExec = useNode ? 'node' : process.execPath; + + // When the executor is Node.js it cannot run .ts files directly, so always + // resolve to the compiled .js entry regardless of whether the CLI itself is + // running from source. + const isTs = isBun && !useNode && __filename.endsWith('.ts'); const serverEntry = path.resolve(__dirname, isTs ? '../server/index.ts' : '../server/index.js'); if (!fs.existsSync(serverEntry)) { console.error(`webtty: server entry not found at ${serverEntry}`); @@ -56,18 +67,16 @@ export async function startServer(timeoutMs = 10000, _spawn = childProcess.spawn stdio = ['ignore', logFd, logFd]; } - // On Windows, Bun's net.Socket doesn't support the fd-based named-pipe - // wrapping that node-pty's ConPTY backend requires, so node-pty fails under - // Bun on Windows. bunx works because its #!/usr/bin/env node shim runs the - // server with Node.js instead. Mirror that here explicitly. - const serverExec = - process.platform === 'win32' && process.versions.bun ? 'node' : process.execPath; - const child = _spawn(serverExec, [serverEntry], { detached: true, stdio, env: { ...process.env, PORT: String(PORT) }, }); + child.on('error', (err) => { + const hint = useNode ? ' (Node.js must be on PATH when running webtty under Bun on Windows)' : ''; + console.error(`webtty: failed to start server: ${err.message}${hint}`); + process.exit(1); + }); child.unref(); if (logFd !== undefined) fs.closeSync(logFd); From 70df063dc98d970001096116b4526bfa7c7d46a1 Mon Sep 17 00:00:00 2001 From: jesse23 Date: Tue, 21 Apr 2026 23:05:37 -0400 Subject: [PATCH 4/6] fix: update tests for child.on and address pre-existing lint warnings - Add `on` method to fakeChild mocks in http.test.ts so startServer's new spawn error handler doesn't throw in tests - Cast process.exit to void-return type before the early return so the 'server entry not found' test path exits cleanly when exit is mocked - Replace noNonNullAssertion (session!) with optional chaining (session?) in websocket.test.ts to clear pre-existing biome lint warnings Co-Authored-By: Claude Sonnet 4.6 --- src/cli/http.test.ts | 6 +++--- src/cli/http.ts | 3 ++- src/server/websocket.test.ts | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/cli/http.test.ts b/src/cli/http.test.ts index 7d013fc..ed1f765 100644 --- a/src/cli/http.test.ts +++ b/src/cli/http.test.ts @@ -101,7 +101,7 @@ describe('startServer', () => { const mkdirSpy = spyOn(fs, 'mkdirSync').mockImplementation(() => undefined); const openSpy = spyOn(fs, 'openSync').mockReturnValue(99); const closeSpy = spyOn(fs, 'closeSync').mockImplementation(() => {}); - const fakeChild = { unref: mock(() => {}) }; + const fakeChild = { unref: mock(() => {}), on: mock(() => {}) }; const spawnMock = mock(() => fakeChild); globalThis.fetch = mock( @@ -152,7 +152,7 @@ describe('startServer', () => { test('spawns server and resolves when it becomes reachable', async () => { const existsSpy = spyOn(fs, 'existsSync').mockReturnValue(true); - const fakeChild = { unref: mock(() => {}) }; + const fakeChild = { unref: mock(() => {}), on: mock(() => {}) }; const spawnMock = mock(() => fakeChild); let calls = 0; @@ -173,7 +173,7 @@ describe('startServer', () => { test('exits with error when server does not start within timeout', async () => { const existsSpy = spyOn(fs, 'existsSync').mockReturnValue(true); - const spawnMock = mock(() => ({ unref: () => {} })); + const spawnMock = mock(() => ({ unref: () => {}, on: () => {} })); const exitSpy = spyOn(process, 'exit').mockImplementation((() => {}) as () => never); const errorSpy = spyOn(console, 'error').mockImplementation(() => {}); diff --git a/src/cli/http.ts b/src/cli/http.ts index d63d477..1d19da9 100644 --- a/src/cli/http.ts +++ b/src/cli/http.ts @@ -54,7 +54,8 @@ export async function startServer(timeoutMs = 10000, _spawn = childProcess.spawn const serverEntry = path.resolve(__dirname, isTs ? '../server/index.ts' : '../server/index.js'); if (!fs.existsSync(serverEntry)) { console.error(`webtty: server entry not found at ${serverEntry}`); - process.exit(1); + (process.exit as (code?: number) => void)(1); + return; } const config = loadConfig(); diff --git a/src/server/websocket.test.ts b/src/server/websocket.test.ts index e6c1202..0d641bc 100644 --- a/src/server/websocket.test.ts +++ b/src/server/websocket.test.ts @@ -221,9 +221,9 @@ describe('websocket', () => { }>; const session = sessions.find((s) => s.id === 'ws-test-pid-route'); expect(session).toBeDefined(); - expect(typeof session!.pid).toBe('number'); + expect(typeof session?.pid).toBe('number'); - const res = await fetch(`${baseUrl}/p/${session!.pid}`, { redirect: 'manual' }); + const res = await fetch(`${baseUrl}/p/${session?.pid}`, { redirect: 'manual' }); expect(res.status).toBe(302); expect(res.headers.get('location')).toBe('/s/ws-test-pid-route'); }); From 14479590b723c6f706ea170340d73fe21b7d3496 Mon Sep 17 00:00:00 2001 From: jesse23 Date: Tue, 21 Apr 2026 23:09:43 -0400 Subject: [PATCH 5/6] fix: split long ternary in http.ts to satisfy biome 100-char line limit Co-Authored-By: Claude Sonnet 4.6 --- src/cli/http.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cli/http.ts b/src/cli/http.ts index 1d19da9..2f4d24b 100644 --- a/src/cli/http.ts +++ b/src/cli/http.ts @@ -74,7 +74,9 @@ export async function startServer(timeoutMs = 10000, _spawn = childProcess.spawn env: { ...process.env, PORT: String(PORT) }, }); child.on('error', (err) => { - const hint = useNode ? ' (Node.js must be on PATH when running webtty under Bun on Windows)' : ''; + const hint = useNode + ? ' (Node.js must be on PATH when running webtty under Bun on Windows)' + : ''; console.error(`webtty: failed to start server: ${err.message}${hint}`); process.exit(1); }); From 6c1415a10a9989b0e352ce25d702bea5328048ad Mon Sep 17 00:00:00 2001 From: jesse23 Date: Tue, 21 Apr 2026 23:13:04 -0400 Subject: [PATCH 6/6] fix: mock loadConfig in startServer tests to avoid real fs reads in CI Co-Authored-By: Claude Sonnet 4.6 --- src/cli/http.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/cli/http.test.ts b/src/cli/http.test.ts index ed1f765..617e6a0 100644 --- a/src/cli/http.test.ts +++ b/src/cli/http.test.ts @@ -162,6 +162,11 @@ describe('startServer', () => { return new Response('[]', { status: 200 }); }) as unknown as typeof fetch; + const configModule = await import('../config'); + const configSpy = spyOn(configModule, 'loadConfig').mockReturnValue({ + ...configModule.DEFAULT_CONFIG, + }); + const { startServer } = await import('./http'); await startServer(10000, spawnMock as never); @@ -169,6 +174,7 @@ describe('startServer', () => { expect(fakeChild.unref).toHaveBeenCalled(); existsSpy.mockRestore(); + configSpy.mockRestore(); }); test('exits with error when server does not start within timeout', async () => { @@ -181,6 +187,11 @@ describe('startServer', () => { throw new Error('not yet'); }) as unknown as typeof fetch; + const configModule = await import('../config'); + const configSpy = spyOn(configModule, 'loadConfig').mockReturnValue({ + ...configModule.DEFAULT_CONFIG, + }); + const { startServer } = await import('./http'); await startServer(100, spawnMock as never); @@ -190,6 +201,7 @@ describe('startServer', () => { existsSpy.mockRestore(); exitSpy.mockRestore(); errorSpy.mockRestore(); + configSpy.mockRestore(); }); });