Files
orca/tests/tools/omp-relay-close-lifecycle.test.mjs
Neil 8c6ae79e94 fix(relay): stop detached tools on immediate terminal close (#20645)
* fix(relay): sweep detached tools on immediate terminal close

* test(relay): reject failed process cleanup queries
2026-09-17 20:58:27 -07:00

151 lines
5.3 KiB
JavaScript

import { it, expect } from 'vitest'
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import {
createMockDispatcher,
createTestPtyHandler
} from '../../src/relay/pty-handler-test-harness.ts'
import {
captureDescendantSnapshot,
readProcessTable
} from '../../src/main/pty-descendant-termination.ts'
import { runProcess } from '../../src/shared/child-process/run-process.ts'
const binary = process.env.ORCA_OMP_PROBE_BINARY
const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`
it.skipIf(!binary || process.platform === 'win32')(
'closes actual OMP detached tools through the relay host',
async () => {
const fixtures = join(process.cwd(), '.bench-fixtures')
mkdirSync(fixtures, { recursive: true })
const output = mkdtempSync(join(fixtures, 'omp-relay-close-'))
const home = mkdtempSync(join(tmpdir(), 'orca-omp-relay-close-home-'))
const agentHome = join(home, 'agent')
mkdirSync(agentHome)
const config = join(home, 'probe.yml')
writeFileSync(
config,
'startup:\n setupWizard: false\n showSplash: false\n checkUpdate: false\n'
)
const dispatcher = createMockDispatcher()
let transcript = ''
dispatcher.notify = (method, params) => {
if (method === 'pty.data' && typeof params?.data === 'string') {
transcript = (transcript + params.data).slice(-131072)
}
}
const handler = createTestPtyHandler(dispatcher)
let snapshot
let id
try {
const spawned = await dispatcher.callRequest('pty.spawn', {
cwd: home,
cols: 120,
rows: 35,
env: {
HOME: home,
USERPROFILE: home,
ZDOTDIR: home,
ORCA_ORIG_ZDOTDIR: home,
SHELL:
process.env.ORCA_OMP_PROBE_SHELL ??
(process.platform === 'darwin' ? '/bin/zsh' : '/bin/bash'),
XDG_CONFIG_HOME: join(home, 'config'),
XDG_DATA_HOME: join(home, 'data'),
XDG_CACHE_HOME: join(home, 'cache'),
XDG_STATE_HOME: join(home, 'state'),
OMP_CODING_AGENT_DIR: agentHome,
PI_CODING_AGENT_DIR: agentHome,
OMP_PROFILE: '',
PI_PROFILE: '',
PI_CONFIG_DIR: '.omp',
PI_CONFIG_FILES: '',
ORCA_BACKGROUND_LAUNCH: '1',
ORCA_PANE_KEY: 'omp-relay-probe:owned-leaf',
ORCA_TAB_ID: 'omp-relay-probe'
},
envToDelete: ['BASH_ENV', 'ENV', 'ORCA_OMP_STATUS_EXTENSION', 'ORCA_PI_STATUS_EXTENSION']
})
id = spawned.id
const [entry] = JSON.parse(await dispatcher.callRequest('pty.serialize', { ids: [id] }))
snapshot = await captureDescendantSnapshot(entry.pid)
expect(snapshot?.root?.pid).toBe(entry.pid)
dispatcher.callNotification('pty.data', {
id,
data: `${quote(binary)} --no-session --config ${quote(config)}\r`
})
await pause(5000)
dispatcher.callNotification('pty.data', { id, data: '! /bin/sleep 120\r' })
for (let attempt = 0; attempt < 25; attempt++) {
await pause(200)
snapshot = await captureDescendantSnapshot(entry.pid)
if (snapshot?.descendants.length > 1) {
break
}
}
expect(snapshot?.descendants.length).toBeGreaterThan(1)
const pids = [entry.pid, ...snapshot.descendants.map((row) => row.pid)]
const rows = async () => {
const result = await runProcess({
program: 'ps',
args: ['-p', pids.join(','), '-o', 'pid=,ppid=,pgid=,stat=,comm='],
maxOutputBytes: 16000
})
expect(result.timedOut).toBe(false)
expect(result.signal).toBeNull()
expect(result.stderr.trim()).toBe('')
expect([0, 1]).toContain(result.code)
if (result.code === 1) {
expect(result.stdout.trim()).toBe('')
}
return result.stdout.trim()
}
const before = await rows()
expect(before).toContain('omp')
expect(before).toContain('sleep')
await dispatcher.callRequest('pty.shutdown', {
id,
immediate: true,
expectedIncarnationId: spawned.incarnationId
})
await pause(6000)
const after = await rows()
writeFileSync(
join(output, 'report.json'),
JSON.stringify({ backend: 'relay-host', before, after, pid: entry.pid, id, home }, null, 2)
)
writeFileSync(join(output, 'transcript.txt'), transcript)
console.log(output)
expect(after).toBe('')
} finally {
if (snapshot) {
const current = await readProcessTable()
const owned = [
...snapshot.descendants,
...(snapshot.root ? [{ ...snapshot.root, pgid: snapshot.rootPgid }] : [])
]
for (const row of current.rows) {
if (
owned.some(
(known) =>
known.pid === row.pid &&
known.startedAt === row.startedAt &&
known.pgid === row.pgid
)
) {
try {
process.kill(row.pid, 'SIGKILL')
} catch {}
}
}
}
await handler.dispose({ waitForPhysicalExit: false })
rmSync(home, { recursive: true, force: true })
}
},
45000
)