Files
orca/tests/tools/omp-child-session-runtime-smoke.mjs
Neil 9841f4f790 fix(omp): keep child sessions from reporting into the parent pane (#20611)
* fix(omp): fence pane status to the root session manager

* test(omp): preserve root preview and recovery through child hooks

* test(omp): exercise status ownership through actual runtime runner

* fix(omp): honor runtime subagent provenance when available

* test(omp): avoid writes to the read-only hook status view

* fix(omp): preserve child status ownership provenance

* fix(omp): normalize child transcript paths across platforms

* fix(omp): clean status handler rebase

* fix(omp): keep prefill inside session ownership fence
2026-09-19 00:29:55 -07:00

147 lines
5.7 KiB
JavaScript

// Run with Bun and a read-only OMP checkout path as the first argument.
import assert from 'node:assert/strict'
import { mkdtemp, writeFile, rm } from 'node:fs/promises'
import { createServer } from 'node:http'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import { build } from 'esbuild'
const reference = process.argv[2]
assert.ok(reference, 'Pass the read-only oh-my-pi source checkout path')
const source = (path) =>
pathToFileURL(join(resolve(reference), 'packages/coding-agent/src', path)).href
const { loadExtensions } = await import(source('extensibility/extensions/loader.ts'))
const { ExtensionRunner } = await import(source('extensibility/extensions/runner.ts'))
const { EventBus } = await import(source('utils/event-bus.ts'))
const { SessionManager } = await import(source('session/session-manager.ts'))
const scratch = await mkdtemp(join(tmpdir(), 'orca-omp-child-status-'))
const posts = []
const errors = []
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
const server = createServer(async (request, response) => {
try {
let body = ''
for await (const chunk of request) {
body += chunk
}
posts.push(JSON.parse(body).payload)
response.writeHead(200).end()
} catch (error) {
errors.push(error)
response.writeHead(500).end()
}
})
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
try {
await build({
entryPoints: ['src/main/pi/agent-status-extension-source.ts'],
bundle: true,
platform: 'node',
format: 'esm',
outfile: join(scratch, 'generator.mjs')
})
const { getPiAgentStatusExtensionSource } = await import(
pathToFileURL(join(scratch, 'generator.mjs')).href
)
const extensionPath = join(scratch, 'orca-agent-status.ts')
await writeFile(extensionPath, getPiAgentStatusExtensionSource('omp'))
process.env.ORCA_PANE_KEY = 'test-parent-pane'
process.env.ORCA_AGENT_LAUNCH_TOKEN = 'test-parent-launch'
process.env.ORCA_AGENT_HOOK_PORT = String(server.address().port)
process.env.ORCA_AGENT_HOOK_TOKEN = 'test-token'
delete process.env.ORCA_AGENT_HOOK_ENDPOINT
delete process.env.ORCA_PI_STATUS_OWNED
const load = async (manager) => {
const result = await loadExtensions([extensionPath], scratch, new EventBus())
assert.deepEqual(result.errors, [])
assert.equal(result.extensions.length, 1)
// These lifecycle handlers never query models or invoke agent actions.
const runner = new ExtensionRunner(result.extensions, result.runtime, scratch, manager, {})
runner.onError((error) => errors.push(error))
return runner
}
const emit = async (runner, type, expectedCount) => {
await runner.emit({ type })
if (expectedCount !== undefined) {
const deadline = Date.now() + 2000
while (posts.length < expectedCount && errors.length === 0 && Date.now() < deadline) {
await delay(10)
}
assert.equal(posts.length, expectedCount, `HTTP delivery after ${type}`)
}
assert.deepEqual(errors, [])
}
const assertQuiet = async (expectedCount) => {
// Suppressed events have no completion callback; allow local HTTP dispatch to settle.
await delay(100)
assert.deepEqual(errors, [])
assert.equal(posts.length, expectedCount, 'child lifecycle must not post pane status')
}
const rootManager = SessionManager.create(scratch, join(scratch, 'sessions'))
const childManager = SessionManager.inMemory(scratch)
const root = await load(rootManager)
await emit(root, 'session_start')
await emit(root, 'agent_start', 1)
const child = await load(childManager)
assert.notEqual(root, child)
assert.notEqual(rootManager, childManager)
await emit(child, 'session_start')
await emit(child, 'agent_start')
await emit(child, 'agent_end')
await assertQuiet(1)
await emit(root, 'agent_end', 2)
await writeFile(extensionPath, `${getPiAgentStatusExtensionSource('omp')}\n// Reloaded module\n`)
const reloaded = await load(rootManager)
await emit(reloaded, 'session_start')
const previousId = rootManager.getSessionId()
await rootManager.newSession()
assert.notEqual(rootManager.getSessionId(), previousId)
await emit(reloaded, 'agent_start', 3)
await emit(reloaded, 'agent_end', 4)
assert.equal(posts.at(-1).session_id, rootManager.getSessionId())
const resumedPath = join(scratch, 'resume-target.jsonl')
await writeFile(
resumedPath,
`${JSON.stringify({
type: 'session',
version: 3,
id: 'runtime-resume-target',
timestamp: new Date().toISOString(),
cwd: scratch
})}\n`
)
// Resume/reopen the transcript through the same manager.
for (const expectedCount of [5, 7]) {
await rootManager.setSessionFile(resumedPath)
assert.equal(rootManager.getSessionId(), 'runtime-resume-target')
await emit(reloaded, 'session_switch')
await emit(reloaded, 'agent_start', expectedCount)
await emit(reloaded, 'agent_end', expectedCount + 1)
assert.equal(posts.at(-1).session_id, 'runtime-resume-target')
}
await emit(reloaded, 'session_shutdown')
await emit(child, 'agent_start')
await emit(child, 'agent_end')
await emit(child, 'session_shutdown')
await assertQuiet(8)
assert.deepEqual(
posts.map((post) => post.hook_event_name),
Array.from({ length: 4 }, () => ['agent_start', 'agent_end']).flat()
)
console.log(
JSON.stringify({
platform: process.platform,
posts: posts.map((post) => post.hook_event_name),
distinctManagers: rootManager !== childManager,
scope:
'Actual OMP loader, ExtensionRunner and SessionManager; controlled lifecycle order; real native HTTP'
})
)
} finally {
server.closeAllConnections()
await new Promise((resolve) => server.close(resolve))
await rm(scratch, { recursive: true, force: true })
}