diff --git a/src/cli/http.test.ts b/src/cli/http.test.ts index 7d013fc..617e6a0 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; @@ -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,11 +174,12 @@ describe('startServer', () => { expect(fakeChild.unref).toHaveBeenCalled(); existsSpy.mockRestore(); + configSpy.mockRestore(); }); 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(() => {}); @@ -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(); }); }); diff --git a/src/cli/http.ts b/src/cli/http.ts index 06ae6af..2f4d24b 100644 --- a/src/cli/http.ts +++ b/src/cli/http.ts @@ -39,11 +39,23 @@ 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}`); - process.exit(1); + (process.exit as (code?: number) => void)(1); + return; } const config = loadConfig(); @@ -56,11 +68,18 @@ export async function startServer(timeoutMs = 10000, _spawn = childProcess.spawn stdio = ['ignore', logFd, logFd]; } - const child = _spawn(process.execPath, [serverEntry], { + 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); 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'); });