diff --git a/src/cli/runtime-client.test.ts b/src/cli/runtime-client.test.ts index d3d6917b54d..ecb5030a73b 100644 --- a/src/cli/runtime-client.test.ts +++ b/src/cli/runtime-client.test.ts @@ -348,6 +348,56 @@ describe.skipIf(process.platform === 'win32')('RuntimeClient', () => { }) }) + // STA-3969: the poll carried the earlier reason forward with `?? lastReason`, so a runtime + // that RECOVERED mid-wait was still reported unreachable at the timeout -- a stale negative + // presented as the current diagnosis. + it('openOrca stops reporting a runtime unreachable once it answers again', async () => { + const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-client-')) + const endpoint = join(userDataPath, 'runtime.sock') + const server = createServer((socket) => { + sockets.add(socket) + socket.once('close', () => sockets.delete(socket)) + socket.once('data', (data) => { + const request = JSON.parse(String(data).trim()) as { id: string } + socket.write( + `${JSON.stringify({ + id: request.id, + ok: true, + result: { + runtimeId: 'runtime-1', + rendererGraphEpoch: 0, + graphStatus: 'ready', + authoritativeWindowId: 0, + desktopWindowStatus: 'initializing', + liveTabCount: 0, + liveLeafCount: 0 + }, + _meta: { runtimeId: 'runtime-1' } + })}\n` + ) + }) + }) + servers.add(server) + await new Promise((resolve) => server.listen(endpoint, resolve)) + // Starts pointed at an endpoint nothing serves, then recovers onto the live one. + writeMetadata(userDataPath, join(userDataPath, 'never-listened.sock'), 'token', process.pid) + vi.mocked(launchOrcaApp).mockImplementationOnce(() => { + writeMetadata(userDataPath, endpoint, 'token', process.pid) + }) + + const client = new RuntimeClient(userDataPath, 100) + + const failure = await client.openOrca(1_000).then( + () => null, + (error: unknown) => error as { code: string; message: string; data?: unknown } + ) + expect(failure?.code).toBe('runtime_open_timeout') + expect(failure?.message).not.toContain('unreachable') + expect(failure?.data).toBeUndefined() + // The second timeout case: answers all the way through, just no window. + expect(failure?.message).toContain('is responding and still running headlessly') + }) + it('openOrca waits for a reachable headless runtime to expose a desktop window', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-client-')) const endpoint = join(userDataPath, 'runtime.sock') diff --git a/src/cli/runtime/client.ts b/src/cli/runtime/client.ts index 92afd58c35e..a3043139bb3 100644 --- a/src/cli/runtime/client.ts +++ b/src/cli/runtime/client.ts @@ -264,6 +264,7 @@ export class RuntimeClient { const startedAt = Date.now() let lastReason = initial.result.runtime.unreachableReason + let runtimeAnswered = initial.result.runtime.reachable while (Date.now() - startedAt < timeoutMs) { const status = await this.getCliStatus() if (status.result.app.desktopWindowStatus === 'blocked') { @@ -272,18 +273,30 @@ export class RuntimeClient { if (status.result.app.desktopWindowStatus === 'available') { return status } - lastReason = status.result.runtime.unreachableReason ?? lastReason + // Why (STA-3969): a runtime that answered is reachable NOW, so carrying the earlier + // reason forward would report a stale negative -- naming an endpoint it no longer + // uses -- as the current diagnosis. + runtimeAnswered = status.result.runtime.reachable + lastReason = runtimeAnswered + ? undefined + : (status.result.runtime.unreachableReason ?? lastReason) await delay(250) } // Why: STA-3969 — this loop polls getCliStatus, so when the runtime is // unreachable it burns the whole timeout and then reported only that it timed // out. Carry the reachability failure the poll already diagnosed. + // Why: the two timeouts are different failures. One never got an answer from the + // runtime; the other got answers the whole time and no window with them -- telling that + // user the runtime "may" be running headlessly understates what the poll already proved. + const timeoutDetail = lastReason + ? `: the Orca app process is running but its runtime is unreachable. ${lastReason.message}` + : runtimeAnswered + ? '. The Orca runtime is responding and still running headlessly; it did not open a window in time.' + : '. The runtime may still be running headlessly.' throw new RuntimeClientError( 'runtime_open_timeout', - lastReason - ? `Timed out waiting for an Orca desktop window: the Orca app process is running but its runtime is unreachable. ${lastReason.message}` - : 'Timed out waiting for an Orca desktop window. The runtime may still be running headlessly.', + `Timed out waiting for an Orca desktop window${timeoutDetail}`, lastReason ? { unreachableReason: lastReason } : undefined ) }