mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
* test(omp): add opt-in owned PTY closure probe * fix(pty): sweep detached tools on immediate unrecognized shell close * test(omp): create close probe evidence root in fresh worktrees * test(pty): account for asynchronous immediate descendant cleanup * test(pty): reject inconclusive descendant cleanup probes
225 lines
7.9 KiB
JavaScript
225 lines
7.9 KiB
JavaScript
import { it, expect } from 'vitest'
|
|
import * as pty from 'node-pty'
|
|
import { Session } from '../../src/main/daemon/session.ts'
|
|
import { TerminalSessionTeardown } from '../../src/main/daemon/terminal-session-teardown.ts'
|
|
import { createDaemonPtySubprocessHandle } from '../../src/main/daemon/pty-subprocess/subprocess-handle.ts'
|
|
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
import { tmpdir } from 'node:os'
|
|
import { runProcess } from '../../src/shared/child-process/run-process.ts'
|
|
import {
|
|
captureDescendantSnapshot,
|
|
readProcessTable
|
|
} from '../../src/main/pty-descendant-termination.ts'
|
|
import {
|
|
createPtyPhysicalExit,
|
|
shutdownLocalPty,
|
|
killAllLocalPtys
|
|
} from '../../src/main/providers/local-pty-termination.ts'
|
|
import {
|
|
ptyProcesses,
|
|
ptyAgentSessionIds,
|
|
ptyPhysicalExits,
|
|
ptyExitDisposables,
|
|
clearPtyState
|
|
} from '../../src/main/providers/local-pty-provider-state.ts'
|
|
|
|
const binary = process.env.ORCA_OMP_PROBE_BINARY
|
|
const externalTool = process.env.ORCA_OMP_PROBE_EXTERNAL_TOOL === '1'
|
|
const daemonBackend = process.env.ORCA_OMP_PROBE_BACKEND === 'daemon'
|
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`
|
|
const ownedPidRows = async (pids) => {
|
|
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()
|
|
}
|
|
it.skipIf(!binary || process.platform === 'win32')(
|
|
'observes actual OMP under production owned-PTY closure policy',
|
|
async () => {
|
|
const fixtures = join(process.cwd(), '.bench-fixtures')
|
|
mkdirSync(fixtures, { recursive: true })
|
|
const output = mkdtempSync(join(fixtures, 'omp-close-'))
|
|
const report = []
|
|
for (const launch of ['recognized', 'typed']) {
|
|
for (const close of externalTool || daemonBackend ? ['explicit'] : ['explicit', 'quit']) {
|
|
expect(ptyProcesses.size).toBe(0)
|
|
const home = mkdtempSync(join(tmpdir(), 'orca-omp-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 id = `${launch}-${close}`
|
|
let transcript = ''
|
|
let nativeExit = null
|
|
const shell =
|
|
process.env.ORCA_OMP_PROBE_SHELL ??
|
|
(process.platform === 'darwin' ? '/bin/zsh' : '/bin/bash')
|
|
const shellArgs = shell.endsWith('zsh') ? ['-f', '-i'] : ['--noprofile', '--norc', '-i']
|
|
const proc = pty.spawn(shell, shellArgs, {
|
|
name: 'xterm-256color',
|
|
cols: 120,
|
|
rows: 35,
|
|
cwd: home,
|
|
env: {
|
|
...process.env,
|
|
HOME: home,
|
|
USERPROFILE: home,
|
|
ZDOTDIR: home,
|
|
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'
|
|
}
|
|
})
|
|
const daemonSession = daemonBackend
|
|
? new Session({
|
|
sessionId: id,
|
|
cols: 120,
|
|
rows: 35,
|
|
shellReadySupported: false,
|
|
...(launch === 'recognized' ? { launchAgent: 'omp' } : {}),
|
|
subprocess: createDaemonPtySubprocessHandle({
|
|
process: proc,
|
|
shellPath: shell,
|
|
spawnCwd: home,
|
|
env: process.env,
|
|
startupCommandDeliveredInShellArgs: false,
|
|
reportsChildExitStatus: true,
|
|
sessionId: id,
|
|
startupAgentRecognition: null
|
|
})
|
|
})
|
|
: null
|
|
proc.onData((data) => {
|
|
transcript = (transcript + data).slice(-131072)
|
|
})
|
|
if (!daemonSession) {
|
|
ptyProcesses.set(id, proc)
|
|
createPtyPhysicalExit(id)
|
|
if (launch === 'recognized') {
|
|
ptyAgentSessionIds.add(id)
|
|
}
|
|
}
|
|
ptyExitDisposables.set(
|
|
id,
|
|
proc.onExit((event) => {
|
|
nativeExit = event
|
|
ptyPhysicalExits.get(id)?.markExited()
|
|
clearPtyState(id)
|
|
rmSync(home, { recursive: true, force: true })
|
|
})
|
|
)
|
|
let snapshot
|
|
try {
|
|
proc.write(`${quote(binary)} --no-session --config ${quote(config)}\r`)
|
|
await delay(5000)
|
|
snapshot = await captureDescendantSnapshot(proc.pid)
|
|
expect(snapshot?.descendants.length).toBeGreaterThan(0)
|
|
if (externalTool) {
|
|
proc.write('! /bin/sleep 120\r')
|
|
for (let attempt = 0; attempt < 25; attempt++) {
|
|
await delay(200)
|
|
snapshot = await captureDescendantSnapshot(proc.pid)
|
|
if (snapshot?.descendants.length > 1) {
|
|
break
|
|
}
|
|
}
|
|
expect(snapshot?.descendants.length).toBeGreaterThan(1)
|
|
}
|
|
const pids = [proc.pid, ...snapshot.descendants.map((row) => row.pid)]
|
|
const before = await ownedPidRows(pids)
|
|
expect(before).toContain('omp')
|
|
if (externalTool) {
|
|
expect(before).toContain('sleep')
|
|
}
|
|
const started = Date.now()
|
|
let closeError = null
|
|
try {
|
|
if (daemonSession) {
|
|
await new TerminalSessionTeardown(new Map([[id, daemonSession]])).killSession(
|
|
id,
|
|
daemonSession,
|
|
true
|
|
)
|
|
} else if (close === 'explicit') {
|
|
await shutdownLocalPty(id, { immediate: externalTool })
|
|
} else {
|
|
killAllLocalPtys()
|
|
}
|
|
} catch (error) {
|
|
closeError = String(error)
|
|
}
|
|
await delay(6000)
|
|
const after = await ownedPidRows(pids)
|
|
report.push({
|
|
launch,
|
|
close,
|
|
externalTool,
|
|
backend: daemonBackend ? 'daemon' : 'local',
|
|
before,
|
|
after,
|
|
nativeExit,
|
|
tracked: daemonSession ? daemonSession.isAlive : ptyProcesses.has(id),
|
|
elapsedMs: Date.now() - started,
|
|
closeError,
|
|
home
|
|
})
|
|
writeFileSync(join(output, `${id}.txt`), transcript)
|
|
writeFileSync(join(output, 'report.json'), JSON.stringify(report, null, 2))
|
|
expect(closeError).toBeNull()
|
|
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 {}
|
|
}
|
|
}
|
|
}
|
|
daemonSession?.dispose()
|
|
clearPtyState(id)
|
|
rmSync(home, { recursive: true, force: true })
|
|
}
|
|
}
|
|
}
|
|
writeFileSync(join(output, 'report.json'), JSON.stringify(report, null, 2))
|
|
console.log(output)
|
|
},
|
|
90000
|
|
)
|