diff --git a/src/commands/bar/bar-server-probe.ts b/src/commands/bar/bar-server-probe.ts index c4c05a0e..0ec13b42 100644 --- a/src/commands/bar/bar-server-probe.ts +++ b/src/commands/bar/bar-server-probe.ts @@ -34,9 +34,9 @@ export function resolveBarPort(ccsDir: string): number | null { * * Both IPv4 (127.0.0.1) and IPv6 (::1) loopback addresses are probed for each * port. All probes are fired concurrently so worst-case latency is ~1.5 s - * (one timeout) rather than N × 1.5 s sequentially. Priority selection is - * applied after all results are in: the bar.json port is preferred over the - * defaults, and within a port 127.0.0.1 is preferred over [::1]. + * (one timeout) rather than N × 1.5 s sequentially. Results are awaited in + * priority order so a lower-priority slow or streaming response cannot block + * returning an already-known higher-priority hit. */ export async function defaultFindRunningServer(ccsDir: string): Promise { const { request } = await import('undici'); @@ -48,7 +48,22 @@ export async function defaultFindRunningServer(ccsDir: string): Promise probe(t.url))); + const probes = probeTargets.map((t) => probe(t.url)); for (let i = 0; i < probeTargets.length; i++) { - if (results[i].ok) { + if ((await probes[i]).ok) { const { port, baseUrl } = probeTargets[i]; return { port, baseUrl }; } diff --git a/tests/unit/commands/bar-command.test.ts b/tests/unit/commands/bar-command.test.ts index adde60df..bccee39c 100644 --- a/tests/unit/commands/bar-command.test.ts +++ b/tests/unit/commands/bar-command.test.ts @@ -2889,3 +2889,60 @@ describe('bar install: silent-decline fix — hint on user decline (review findi expect(allOutput).not.toMatch(/Run `ccs bar` to launch later/i); }); }); + +describe('defaultFindRunningServer: streaming lower-priority probes', () => { + it('returns a higher-priority hit without waiting for lower-priority response bodies', async () => { + const ccsDir = path.join(tempHome, '.ccs'); + fs.mkdirSync(ccsDir, { recursive: true }); + fs.writeFileSync( + path.join(ccsDir, 'bar.json'), + JSON.stringify({ port: 41235, baseUrl: 'http://127.0.0.1:41235', authMode: 'loopback' }) + ); + + let highPriorityBodyDestroyed = false; + let lowerPriorityProbeStarted = false; + + mock.module('undici', () => ({ + request: (url: string) => { + if (url === 'http://127.0.0.1:41235/api/bar/summary') { + return Promise.resolve({ + statusCode: 200, + body: { + destroy: () => { + highPriorityBodyDestroyed = true; + }, + }, + }); + } + + if (url === 'http://127.0.0.1:3000/api/bar/summary') { + lowerPriorityProbeStarted = true; + return new Promise(() => { + // Simulate a lower-priority service that never finishes responding. + }); + } + + return Promise.resolve({ + statusCode: 404, + body: { destroy: () => {} }, + }); + }, + })); + + moduleSeq++; + const { defaultFindRunningServer } = (await import( + `../../../src/commands/bar/bar-server-probe?test=${Date.now()}-${moduleSeq}` + )) as { + defaultFindRunningServer: (ccsDir: string) => Promise<{ port: number; baseUrl: string } | null>; + }; + + const result = await Promise.race([ + defaultFindRunningServer(ccsDir), + new Promise<'timeout'>((resolve) => setTimeout(() => resolve('timeout'), 250)), + ]); + + expect(result).toEqual({ port: 41235, baseUrl: 'http://127.0.0.1:41235' }); + expect(highPriorityBodyDestroyed).toBe(true); + expect(lowerPriorityProbeStarted).toBe(true); + }); +});