merge: PR #20098 into tier1-rc

This commit is contained in:
Neil
2026-09-14 15:05:45 -07:00
4 changed files with 91 additions and 7 deletions
+52 -1
View File
@@ -2,7 +2,7 @@ import { mkdtempSync, writeFileSync } from 'node:fs'
import { createServer, type Socket } from 'node:net'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { getRuntimeMetadataPath } from '../../shared/runtime-bootstrap'
import type { RuntimeStatus } from '../../shared/runtime-types'
import { RuntimeClient } from './client'
@@ -86,6 +86,57 @@ describe.skipIf(process.platform === 'win32')('CLI runtime status', () => {
})
})
// Why: `kill(pid, 0)` answers EPERM when the pid exists under another uid — an Orca the
// CLI was pointed at with ORCA_USER_DATA_PATH, or one started with sudo. Reading that
// refusal as absence reports a live app as a dead one
// (docs/reference/ssh-execution-boundary.md).
describe.skipIf(process.platform === 'win32')('CLI status pid fallback', () => {
async function statusWithUnreachableRuntime(
killError: NodeJS.ErrnoException
): Promise<Awaited<ReturnType<RuntimeClient['getCliStatus']>>> {
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-status-probe-'))
writeFileSync(
getRuntimeMetadataPath(userDataPath),
JSON.stringify({
runtimeId: 'runtime-unreachable',
pid: 424242,
// Nothing is listening here, so `status.get` fails and the pid probe decides.
transport: { kind: 'unix', endpoint: join(userDataPath, 'absent.sock') },
authToken: 'token',
startedAt: Date.now()
})
)
const killSpy = vi.spyOn(process, 'kill').mockImplementation(() => {
throw killError
})
try {
return await new RuntimeClient(userDataPath).getCliStatus()
} finally {
killSpy.mockRestore()
}
}
it('keeps an unsignalable app running rather than calling the bootstrap stale', async () => {
const status = await statusWithUnreachableRuntime(
Object.assign(new Error('kill EPERM'), { code: 'EPERM' })
)
expect(status.result.app).toMatchObject({ running: true, pid: 424242 })
expect(status.result.runtime.state).toBe('starting')
expect(status.result.graph.state).toBe('starting')
})
it('still reports a stale bootstrap when the host proves the pid is gone', async () => {
const status = await statusWithUnreachableRuntime(
Object.assign(new Error('kill ESRCH'), { code: 'ESRCH' })
)
expect(status.result.app).toMatchObject({ running: false, pid: null })
expect(status.result.runtime.state).toBe('stale_bootstrap')
expect(status.result.graph.state).toBe('not_running')
})
})
describe('projectRemoteAppStatus', () => {
function remoteStatus(overrides: Partial<RuntimeStatus> = {}): RuntimeStatus {
return {
+4 -2
View File
@@ -106,7 +106,9 @@ function isProcessRunning(pid: number | null | undefined): boolean {
try {
process.kill(pid, 0)
return true
} catch {
return false
} catch (error) {
// Why: only ESRCH proves the pid is gone. EPERM means it exists under another uid, and
// reporting that as `stale_bootstrap` calls a live Orca dead.
return (error as NodeJS.ErrnoException)?.code !== 'ESRCH'
}
}
+31
View File
@@ -518,6 +518,37 @@ describe('PtyHandler', () => {
expect(JSON.parse(live).map((entry: { id: string }) => entry.id)).toEqual(['pty-21'])
})
// Why: the pid gate is the one place revive turns an observation into "this pane is
// finished". `kill(pid, 0)` answers EPERM when the process exists under another uid, and
// the same ESRCH-only rule `reapPtyProvenExited` applies has to hold here
// (docs/reference/ssh-execution-boundary.md).
it('keeps a pane whose pid refuses the probe and drops only a proven-gone one', async () => {
const state = JSON.stringify([
{ id: 'pty-30', pid: 424242, cols: 80, rows: 24, cwd: LIVE_CWD },
{ id: 'pty-31', pid: 434343, cols: 80, rows: 24, cwd: LIVE_CWD }
])
const killSpy = vi.spyOn(process, 'kill').mockImplementation((pid) => {
if (pid === 424242) {
throw Object.assign(new Error('kill EPERM'), { code: 'EPERM' })
}
if (pid === 434343) {
throw Object.assign(new Error('kill ESRCH'), { code: 'ESRCH' })
}
return true
})
try {
await dispatcher.callRequest('pty.revive', { state })
} finally {
killSpy.mockRestore()
}
expect(mockPtySpawn).toHaveBeenCalledTimes(1)
const live = (await dispatcher.callRequest('pty.serialize', {
ids: ['pty-30', 'pty-31']
})) as string
expect(JSON.parse(live).map((entry: { id: string }) => entry.id)).toEqual(['pty-30'])
})
describe('a Windows relay reviving a WSL pane', () => {
const worktreeId = 'r::/remote/wsl-worktree'
const historyFile = join(
+4 -4
View File
@@ -2907,10 +2907,10 @@ export class PtyHandler {
if (this.ptys.has(entry.id) || this.pendingReviveIds.has(entry.id)) {
continue
}
// Only re-attach if the original process is still alive
try {
process.kill(entry.pid, 0)
} catch {
// Only re-attach if the host proves the original process is still there. `isProcessAlive`
// is ESRCH-only for the same reason `reapPtyProvenExited` is: a refusal this host cannot
// resolve is unverifiable, not absence (docs/reference/ssh-execution-boundary.md).
if (!Number.isInteger(entry.pid) || entry.pid <= 0 || !isProcessAlive(entry.pid)) {
continue
}
const ownedPath = entry.worktreeId