From 119aa3be984042a354a216582bc566a4b715a3ff Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Tue, 28 Apr 2026 11:39:40 -0400 Subject: [PATCH] fix: keep config dashboard alive when interface detection fails --- src/commands/config-dashboard-host.ts | 19 +++++++++++--- tests/unit/commands/config-command.test.ts | 26 +++++++++++++++++-- .../commands/config-dashboard-host.test.ts | 19 +++++++++++++- 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/src/commands/config-dashboard-host.ts b/src/commands/config-dashboard-host.ts index 7a29242c..c0e10fb7 100644 --- a/src/commands/config-dashboard-host.ts +++ b/src/commands/config-dashboard-host.ts @@ -45,7 +45,7 @@ export function normalizeDashboardHost(host: string | undefined): string | undef export function resolveDashboardUrls( host: string | undefined, port: number, - networkInterfaces: NetworkInterfacesMap = os.networkInterfaces() + networkInterfaces?: NetworkInterfacesMap ): DashboardUrls { const bindHost = normalizeDashboardHost(host); if (!bindHost) { @@ -58,7 +58,7 @@ export function resolveDashboardUrls( return { bindHost, browserUrl: `http://localhost:${port}`, - networkUrls: getExternalIpv4Urls(port, networkInterfaces), + networkUrls: getExternalIpv4Urls(port, networkInterfaces ?? getNetworkInterfacesSafe()), }; } @@ -70,8 +70,12 @@ export function resolveDashboardUrls( function getExternalIpv4Urls( port: number, - networkInterfaces: NetworkInterfacesMap + networkInterfaces: NetworkInterfacesMap | undefined ): string[] | undefined { + if (!networkInterfaces) { + return undefined; + } + const urls: string[] = []; const seen = new Set(); @@ -97,6 +101,15 @@ function getExternalIpv4Urls( return urls.length > 0 ? urls : undefined; } +function getNetworkInterfacesSafe(): NetworkInterfacesMap | undefined { + try { + // Some constrained environments (for example proot) throw here; keep the local dashboard URL. + return os.networkInterfaces(); + } catch { + return undefined; + } +} + function formatHostForUrl(host: string): string { if (host.includes(':') && !host.startsWith('[') && !host.endsWith(']')) { return `[${host}]`; diff --git a/tests/unit/commands/config-command.test.ts b/tests/unit/commands/config-command.test.ts index 2fd3fbcf..bd80a4f0 100644 --- a/tests/unit/commands/config-command.test.ts +++ b/tests/unit/commands/config-command.test.ts @@ -1,4 +1,5 @@ -import { afterEach, beforeEach, describe, expect, it } from 'bun:test'; +import { afterEach, beforeEach, describe, expect, it, spyOn } from 'bun:test'; +import * as os from 'os'; import { handleConfigCommand } from '../../../src/commands/config-command'; import { resolveNamedCommand } from '../../../src/commands/named-command-router'; @@ -104,7 +105,9 @@ describe('config command dashboard startup', () => { throw new Error(`process.exit(${code ?? 0})`); }) as typeof process.exit; - await expect(handleConfigCommand(['help'], createTestDeps())).rejects.toThrow('process.exit(0)'); + await expect(handleConfigCommand(['help'], createTestDeps())).rejects.toThrow( + 'process.exit(0)' + ); expect(startServerCalls).toHaveLength(0); expect(logLines.join('\n')).toContain('Usage: ccs config [command] [options]'); @@ -191,6 +194,25 @@ describe('config command dashboard startup', () => { expect(rendered).toContain('Dashboard will work but Control Panel/Stats may be limited'); }); + it('still opens the dashboard when wildcard network detection is unavailable', async () => { + const networkInterfacesSpy = spyOn(os, 'networkInterfaces').mockImplementation(() => { + throw new Error('uv_interface_addresses returned Unknown system error 13'); + }); + + try { + await handleConfigCommand([], createTestDeps()); + + expect(startServerCalls).toHaveLength(1); + + const rendered = logLines.join('\n'); + expect(rendered).toContain('Dashboard: http://localhost:3000'); + expect(rendered).toContain('Bind host: ::'); + expect(errorLines).toHaveLength(0); + } finally { + networkInterfacesSpy.mockRestore(); + } + }); + it('fails cleanly when the server cannot bind the requested host', async () => { startServerError = new Error( 'Unable to bind 192.0.2.123:4100; the address may be unavailable or the port may already be in use' diff --git a/tests/unit/commands/config-dashboard-host.test.ts b/tests/unit/commands/config-dashboard-host.test.ts index a0f85c4e..f91782ec 100644 --- a/tests/unit/commands/config-dashboard-host.test.ts +++ b/tests/unit/commands/config-dashboard-host.test.ts @@ -1,4 +1,5 @@ -import { describe, expect, it } from 'bun:test'; +import { describe, expect, it, spyOn } from 'bun:test'; +import * as os from 'os'; import { isLoopbackHost, @@ -66,4 +67,20 @@ describe('config dashboard host helpers', () => { expect(urls.bindHost).toBe('::1'); expect(urls.browserUrl).toBe('http://[::1]:3000'); }); + + it('falls back to localhost-only URLs when interface enumeration fails', () => { + const networkInterfacesSpy = spyOn(os, 'networkInterfaces').mockImplementation(() => { + throw new Error('uv_interface_addresses returned Unknown system error 13'); + }); + + try { + const urls = resolveDashboardUrls('::', 3000); + + expect(urls.bindHost).toBe('::'); + expect(urls.browserUrl).toBe('http://localhost:3000'); + expect(urls.networkUrls).toBeUndefined(); + } finally { + networkInterfacesSpy.mockRestore(); + } + }); });