Files
orca/src/relay/pty-shell-utils.test.ts
Neil 406bd0e378 perf(relay): cache process-table descendant indexes (#17646)
* perf(relay): cache process-table descendant indexes

* fix(relay): keep the process-table index first-wins and narrow

Two defects in the memoized index this PR introduced.

- Restore the first-wins duplicate-pid tie-break the relay had as
  `rows.find()`. A process whose argv contains a newline makes `ps` print a
  continuation line that the lenient parser can accept as a spurious row
  duplicating a real pid; that row always FOLLOWS the real one, so last-wins let
  it capture the pane's foreground. The rule now lives in
  `buildProcessTableIndex`, so the batched evidence resolver's `byPid.get(rootPid)`
  root lookup gets the same semantics the subsystem had before indexing.
- Build only the two indexes a resolver reads. `byPgid`/`byTpgid` have no readers
  repo-wide, and delegating to a four-map build made a one-pane relay pay more
  per 500ms capture than the single `childrenByParent` map it replaced --
  a regression in the majority topology, in a PR whose point is relay CPU.

Matches the same deletion in #17763 line for line so whichever merges second
resolves trivially.
2026-08-31 18:38:03 -07:00

504 lines
16 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
const { execFileMock, execFileSyncMock, getAllProcessesMock } = vi.hoisted(() => ({
execFileMock: vi.fn(),
getAllProcessesMock: vi.fn(),
execFileSyncMock: vi.fn()
}))
vi.mock('child_process', () => ({
execFile: execFileMock,
execFileSync: execFileSyncMock
}))
import { resetWindowsProcessRowsSnapshotForTests } from '../main/providers/windows-foreground-process-rows'
import { __setWindowsProcessTreeLoaderForTests } from '../main/windows/windows-process-table'
import { resetProcessTableSnapshotForTests } from '../shared/process-table-snapshot'
import {
getForegroundProcessName,
isProcessAlive,
resolveDefaultCwd,
resolveWindowsDefaultShell
} from './pty-shell-utils'
function mockExecFile(
implementation: (command: string, args: string[]) => { stdout: string; stderr?: string } | Error
): void {
execFileMock.mockImplementation(
(command: string, args: string[], _opts: unknown, cb: unknown) => {
const callback = cb as (err: unknown, result: { stdout: string; stderr: string }) => void
const result = implementation(command, args)
if (result instanceof Error) {
callback(result, { stdout: '', stderr: '' })
return
}
callback(null, { stdout: result.stdout, stderr: result.stderr ?? '' })
}
)
}
/**
* Feed the native Windows snapshot. A real snapshot always contains the
* querying process, and the reader rejects a table without it.
*/
function mockWindowsProcessTable(
rows: { pid: number; ppid: number; name: string; commandLine?: string }[]
): void {
__setWindowsProcessTreeLoaderForTests(() => ({
ProcessDataFlag: { None: 0, Memory: 1, CommandLine: 2 },
getAllProcesses: (cb: (value: typeof rows | undefined) => void) =>
cb([{ pid: process.pid, ppid: 0, name: 'vitest.exe', commandLine: 'vitest' }, ...rows])
}))
getAllProcessesMock.mockClear()
}
async function withProcessPlatform<T>(
platform: NodeJS.Platform,
run: () => T | Promise<T>
): Promise<T> {
const descriptor = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: platform })
try {
return await run()
} finally {
if (descriptor) {
Object.defineProperty(process, 'platform', descriptor)
}
}
}
beforeEach(() => {
vi.resetModules()
execFileMock.mockReset()
execFileSyncMock.mockReset()
resetProcessTableSnapshotForTests()
resetWindowsProcessRowsSnapshotForTests()
__setWindowsProcessTreeLoaderForTests()
})
describe('isProcessAlive', () => {
it('reports the test runner process as alive', () => {
expect(isProcessAlive(process.pid)).toBe(true)
})
it('reports a process as dead ONLY on ESRCH', () => {
// Why: attach() reaps a lingering managed PTY based on this, so a false
// positive would kill a live remote shell. Only "no such process" counts.
const spy = vi.spyOn(process, 'kill').mockImplementation(() => {
const err = new Error('no such process') as NodeJS.ErrnoException
err.code = 'ESRCH'
throw err
})
try {
expect(isProcessAlive(2147483646)).toBe(false)
} finally {
spy.mockRestore()
}
})
it('treats an unsignalable process (EPERM) as alive', () => {
const spy = vi.spyOn(process, 'kill').mockImplementation(() => {
const err = new Error('operation not permitted') as NodeJS.ErrnoException
err.code = 'EPERM'
throw err
})
try {
expect(isProcessAlive(1)).toBe(true)
} finally {
spy.mockRestore()
}
})
})
describe('resolveWindowsDefaultShell', () => {
it('uses an existing SHELL override when one is provided', () => {
expect(
resolveWindowsDefaultShell(
{
SHELL: 'C:\\Tools\\pwsh.exe',
SystemRoot: 'C:\\Windows',
ComSpec: 'C:\\Windows\\System32\\cmd.exe'
},
(path) => path === 'C:\\Tools\\pwsh.exe',
() => {
throw new Error('DefaultShell should not be read when SHELL wins')
}
)
).toBe('C:\\Tools\\pwsh.exe')
})
it('uses an existing OpenSSH DefaultShell path', () => {
const powershell7 = 'C:\\Program Files\\PowerShell\\7\\pwsh.exe'
expect(
resolveWindowsDefaultShell(
{
SystemRoot: 'C:\\Windows',
ComSpec: 'C:\\Windows\\System32\\cmd.exe'
},
(path) => path === powershell7,
() => powershell7
)
).toBe(powershell7)
})
it('reads and memoizes the OpenSSH DefaultShell registry value', async () => {
execFileSyncMock.mockReturnValue(
[
'HKEY_LOCAL_MACHINE\\SOFTWARE\\OpenSSH',
' DefaultShell REG_SZ C:\\Program Files\\PowerShell\\7\\pwsh.exe'
].join('\n')
)
const { readOpenSshDefaultShell } = await import('./pty-shell-utils')
expect(readOpenSshDefaultShell()).toBe('C:\\Program Files\\PowerShell\\7\\pwsh.exe')
expect(readOpenSshDefaultShell()).toBe('C:\\Program Files\\PowerShell\\7\\pwsh.exe')
expect(execFileSyncMock).toHaveBeenCalledTimes(1)
expect(execFileSyncMock).toHaveBeenCalledWith(
'reg.exe',
['query', 'HKLM\\SOFTWARE\\OpenSSH', '/v', 'DefaultShell'],
{ encoding: 'utf8', timeout: 3000, windowsHide: true }
)
})
it('treats malformed OpenSSH DefaultShell output as empty and preserves the fallback chain', async () => {
execFileSyncMock.mockReturnValue(
[
'HKEY_LOCAL_MACHINE\\SOFTWARE\\OpenSSH',
' DefaultShellCommandOption REG_SZ /c'
].join('\n')
)
const { readOpenSshDefaultShell } = await import('./pty-shell-utils')
const powershell = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'
expect(readOpenSshDefaultShell()).toBe('')
expect(
resolveWindowsDefaultShell(
{
SystemRoot: 'C:\\Windows',
ComSpec: 'C:\\Windows\\System32\\cmd.exe'
},
(path) => path === powershell || path === 'C:\\Windows\\System32\\cmd.exe',
readOpenSshDefaultShell
)
).toBe(powershell)
})
it('treats reg.exe failures as empty and preserves the fallback chain', async () => {
execFileSyncMock.mockImplementation(() => {
throw new Error('reg.exe failed')
})
const { readOpenSshDefaultShell } = await import('./pty-shell-utils')
const powershell = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'
expect(readOpenSshDefaultShell()).toBe('')
expect(
resolveWindowsDefaultShell(
{
SystemRoot: 'C:\\Windows',
ComSpec: 'C:\\Windows\\System32\\cmd.exe'
},
(path) => path === powershell || path === 'C:\\Windows\\System32\\cmd.exe',
readOpenSshDefaultShell
)
).toBe(powershell)
})
it('preserves the fallback chain for an invalid OpenSSH DefaultShell', () => {
const powershell = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'
expect(
resolveWindowsDefaultShell(
{
SystemRoot: 'C:\\Windows',
ComSpec: 'C:\\Windows\\System32\\cmd.exe'
},
(path) => path === powershell,
() => 'C:\\missing\\pwsh.exe'
)
).toBe(powershell)
})
it('honors a deliberate OpenSSH PowerShell 5.1 DefaultShell value', () => {
const powershell = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'
expect(
resolveWindowsDefaultShell(
{
SystemRoot: 'C:\\Windows',
ComSpec: 'C:\\Windows\\System32\\cmd.exe'
},
(path) => path === powershell,
() => powershell
)
).toBe(powershell)
})
it('prefers inbox PowerShell before ComSpec for an interactive Windows PTY', () => {
const powershell = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'
expect(
resolveWindowsDefaultShell(
{
SystemRoot: 'C:\\Windows',
ComSpec: 'C:\\Windows\\System32\\cmd.exe'
},
(path) => path === powershell || path === 'C:\\Windows\\System32\\cmd.exe',
() => ''
)
).toBe(powershell)
})
it('falls back to ComSpec when PowerShell cannot be found by path', () => {
expect(
resolveWindowsDefaultShell(
{
SystemRoot: 'C:\\Windows',
ComSpec: 'C:\\Windows\\System32\\cmd.exe'
},
(path) => path === 'C:\\Windows\\System32\\cmd.exe',
() => ''
)
).toBe('C:\\Windows\\System32\\cmd.exe')
})
})
describe('resolveDefaultCwd', () => {
it('uses USERPROFILE for Windows PTYs without an explicit cwd', () => {
expect(
resolveDefaultCwd(
{
USERPROFILE: 'C:\\Users\\alice',
HOME: '/not/a/windows/cwd'
},
'win32',
'C:\\Users\\fallback'
)
).toBe('C:\\Users\\alice')
})
it('falls back to HOMEDRIVE plus HOMEPATH on Windows when USERPROFILE is missing', () => {
expect(
resolveDefaultCwd(
{
HOMEDRIVE: 'D:',
HOMEPATH: '\\Users\\bob'
},
'win32',
'C:\\Users\\fallback'
)
).toBe('D:\\Users\\bob')
})
it('keeps POSIX HOME fallback behavior', () => {
expect(resolveDefaultCwd({ HOME: '/home/alice' }, 'linux', '/fallback')).toBe('/home/alice')
})
})
describe('getForegroundProcessName', () => {
it('returns clear non-wrapper foregrounds without process-table enrichment', async () => {
await expect(getForegroundProcessName(100, 'vim')).resolves.toBe('vim')
expect(execFileMock).not.toHaveBeenCalled()
})
it('recognizes SSH relay node-wrapped agents from descendant command lines', async () => {
await withProcessPlatform('linux', async () => {
mockExecFile((_command, args) => {
if (args[0] === '-axo') {
return {
stdout: ['100 99 Ss bash -l', '101 100 S+ node /home/dev/.local/bin/codex'].join(
'\n'
)
}
}
return new Error('unexpected command')
})
await expect(getForegroundProcessName(100, 'node')).resolves.toBe('codex')
})
})
it('recognizes Windows SSH relay shell-rooted agent descendants', async () => {
await withProcessPlatform('win32', async () => {
mockWindowsProcessTable([
{ pid: 100, ppid: 99, name: 'powershell.exe', commandLine: 'powershell.exe' },
{
pid: 101,
ppid: 100,
name: 'node.exe',
commandLine: 'node C:\\Users\\dev\\AppData\\Roaming\\npm\\codex.cmd'
}
])
await expect(getForegroundProcessName(100, 'powershell.exe')).resolves.toBe('codex')
})
})
it('recognizes SSH relay wrapped agents when no foreground marker is available', async () => {
await withProcessPlatform('linux', async () => {
mockExecFile((_command, args) => {
if (args[0] === '-axo') {
return {
stdout: [
'100 99 Ss bash -l',
'101 100 S node /home/dev/.local/bin/node_modules/@google/gemini-cli/bundle/gemini.mjs'
].join('\n')
}
}
return new Error('unexpected command')
})
await expect(getForegroundProcessName(100, 'node')).resolves.toBe('gemini')
})
})
it('does not guess when SSH relay wrapper descendants are ambiguous', async () => {
await withProcessPlatform('linux', async () => {
mockExecFile((_command, args) => {
if (args[0] === '-axo') {
return {
stdout: [
'100 99 Ss bash -l',
'101 100 S node /home/dev/project/server.js',
'102 100 S node /home/dev/.local/bin/node_modules/@openai/codex/bin/codex.js'
].join('\n')
}
}
return new Error('unexpected command')
})
await expect(getForegroundProcessName(100, 'node')).resolves.toBe('node')
})
})
it('does not report a stopped SSH relay agent when another process has foreground', async () => {
await withProcessPlatform('linux', async () => {
mockExecFile((_command, args) => {
if (args[0] === '-axo') {
return {
stdout: [
'100 99 Ss bash -l',
'101 100 T node /home/dev/.local/bin/codex',
'102 100 S+ vim notes.txt'
].join('\n')
}
}
return new Error('unexpected command')
})
await expect(getForegroundProcessName(100, 'node')).resolves.toBe('node')
})
})
// Why: OMP embeds Pi, but the outer process is the user-visible identity (#6364).
it('reports the outer omp wrapper over the wrapped pi child from a shell fallback', async () => {
await withProcessPlatform('linux', async () => {
mockExecFile((_command, args) => {
if (args[0] === '-axo') {
return {
stdout: ['100 99 Ss bash -l', '101 100 S+ omp', '102 101 S+ pi'].join('\n')
}
}
return new Error('unexpected command')
})
await expect(getForegroundProcessName(100, 'bash')).resolves.toBe('omp')
})
})
it('rescans for the omp wrapper when node-pty reports the wrapped pi as foreground', async () => {
await withProcessPlatform('linux', async () => {
mockExecFile((_command, args) => {
if (args[0] === '-axo') {
return {
stdout: ['100 99 Ss bash -l', '101 100 S+ omp', '102 101 S+ pi'].join('\n')
}
}
return new Error('unexpected command')
})
await expect(getForegroundProcessName(100, 'pi')).resolves.toBe('omp')
})
})
it('returns an outer omp fallback without a process-table scan', async () => {
mockExecFile(() => new Error('unexpected process-table scan'))
await expect(getForegroundProcessName(100, 'omp')).resolves.toBe('omp')
expect(execFileMock).not.toHaveBeenCalled()
})
it('rescans a Windows pi fallback for its outer omp wrapper', async () => {
await withProcessPlatform('win32', async () => {
mockWindowsProcessTable([
{ pid: 100, ppid: 99, name: 'powershell.exe', commandLine: 'powershell.exe' },
{ pid: 101, ppid: 100, name: 'omp.exe', commandLine: 'omp.exe' },
{ pid: 102, ppid: 101, name: 'pi.exe', commandLine: 'pi.exe' }
])
await expect(getForegroundProcessName(100, 'pi')).resolves.toBe('omp')
})
})
it('keeps a pi fallback as pi when no omp wrapper is in the tree', async () => {
await withProcessPlatform('linux', async () => {
mockExecFile((_command, args) => {
if (args[0] === '-axo') {
return { stdout: ['100 99 Ss bash -l', '101 100 S+ pi'].join('\n') }
}
return new Error('unexpected command')
})
await expect(getForegroundProcessName(100, 'pi')).resolves.toBe('pi')
})
})
it('normalizes a wrapper fallback the process table cannot confirm', async () => {
// Why: the table scan must answer null, not the raw node-pty name, so the
// ladder still publishes the RECOGNIZED (normalized) identity.
await withProcessPlatform('linux', async () => {
mockExecFile((_command, args) => {
if (args[0] === '-axo') {
return { stdout: ['100 99 Ss bash -l', '101 100 S+ vim notes.txt'].join('\n') }
}
return new Error('unexpected command')
})
await expect(getForegroundProcessName(100, '/opt/homebrew/bin/pi')).resolves.toBe('pi')
})
})
it('resolves a duplicated root pid to the FIRST capture row', async () => {
// Preserve rows.find() semantics if a malformed table repeats a pid: an argv
// newline makes `ps` print a continuation line that can parse as a spurious
// row, so the real root (row one) must keep owning the pane's foreground.
await withProcessPlatform('linux', async () => {
mockExecFile((_command, args) => {
if (args[0] === '-axo') {
return {
stdout: ['100 1 Ss bash', '100 1 Ss+ bash', '101 100 S node /opt/codex'].join('\n')
}
}
return new Error('unexpected command')
})
await expect(getForegroundProcessName(100, 'bash')).resolves.toBe('codex')
})
})
it('falls back to the root process command when descendant inspection fails', async () => {
mockExecFile((_command, args) => {
if (args[0] === '-axo') {
return new Error('ps table unavailable')
}
return { stdout: 'bash\n' }
})
await expect(getForegroundProcessName(100)).resolves.toBe('bash')
})
})