Files
orca/tests/tools/omp-completion-runtime-smoke.mjs
T
a259616f7c feat(pi): show Pi and OMP background children as rows (#23201)
* feat(pi): show Pi and OMP background children as rows

Refs #22868

* fix(pi): derive the extension's roster cap from the host's

The generated extension re-typed the 32-row limit as its own literal, so
a change to AGENT_STATUS_MAX_SUBAGENTS would leave the extension posting
rosters the host silently truncates on arrival. Interpolate the host
constant instead, and cover the cap with a test that posts past it.

* test(pi): pin the Pi cancel verdict beside a live child row

Publishing a subagent roster for Pi puts its panes behind the same
child-work guard Claude and Codex sit behind, so Ctrl+C beside a live
child no longer settles a stopped row. The extension reports no
main-agent state, so Orca cannot tell a cancelled turn from Ctrl+C at
the idle prompt of a lead children alone hold open, and keeps the live
row. Both halves of that are now pinned, as is the resume-placeholder
guard on the model_select arm of the same code path.

* fix(pi): end a run with its session and keep children across reload

Pi re-runs the extension on a fresh event bus for /new, resume, fork and
/reload, so the roster kept on that bus was lost and nothing ended the
run the old session's children held open.

* fix(pi): preserve pending completion and OMP pane ownership

* test(pi): use the hook owner context type

* fix(pi): retain completion timers and delivery across session boundaries

* test(omp): preserve unsent completion at session boundaries

* test(journal): retain provider handle import across main integration

* test(startup): account for encoded Windows prompt lines

* fix(pi): serialize status delivery across module reloads

* test(compatibility): retain the released journal parser dependency

---------

Co-authored-by: Neil <neil@stably.ai>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
2026-10-05 20:04:48 -07:00

140 lines
4.4 KiB
JavaScript

import assert from 'node:assert/strict'
import { once } from 'node:events'
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { createServer } from 'node:http'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { setTimeout as delay } from 'node:timers/promises'
import { build, transform } from 'esbuild'
const scratch = await mkdtemp(join(tmpdir(), 'orca-omp-completion-'))
const received = []
let rejectCompletion = true
const server = createServer(async (request, response) => {
let body = ''
for await (const chunk of request) {
body += chunk
}
const payload = JSON.parse(body).payload
const event = payload.hook_event_name
received.push(payload)
const reject = event === 'agent_end' && rejectCompletion
if (reject) {
rejectCompletion = false
}
response.writeHead(reject ? 503 : 204)
response.end()
})
async function waitForRequests(count) {
const deadline = Date.now() + 5000
while (received.length < count && Date.now() < deadline) {
await delay(10)
}
assert.equal(received.length, count, 'Hook requests did not arrive before the deadline')
}
try {
const bundle = join(scratch, 'source.cjs')
await build({
entryPoints: ['src/main/pi/agent-status-extension-source.ts'],
bundle: true,
platform: 'node',
format: 'cjs',
outfile: bundle
})
const require = createRequire(import.meta.url)
const { getPiAgentStatusExtensionSource } = require(bundle)
const generated = await transform(getPiAgentStatusExtensionSource('omp'), {
loader: 'ts',
format: 'cjs',
target: 'node24'
})
const extension = join(scratch, 'extension.cjs')
await writeFile(extension, generated.code)
server.listen(0, '127.0.0.1')
await once(server, 'listening')
Object.assign(process.env, {
ORCA_BACKGROUND_LAUNCH: '1',
ORCA_PANE_KEY: 'completion-proof',
ORCA_TAB_ID: 'proof-tab',
ORCA_AGENT_HOOK_PORT: String(server.address().port),
ORCA_AGENT_HOOK_TOKEN: 'isolated-proof-token',
ORCA_AGENT_HOOK_ENDPOINT: '',
ORCA_PI_STATUS_OWNED: '',
WSL_DISTRO_NAME: ''
})
const handlers = new Map()
require(extension).default({ on: (event, handler) => handlers.set(event, handler) })
let sessionId = 'A'
const context = {
isIdle: () => false,
sessionManager: {
getSessionId: () => sessionId,
getSessionFile: () => join(scratch, `${sessionId}.jsonl`)
}
}
const emit = async (event, payload = {}) => {
assert.ok(handlers.has(event), `Missing lifecycle handler: ${event}`)
await handlers.get(event)(payload, context)
}
await emit('agent_start')
await waitForRequests(1)
await emit('agent_end')
await waitForRequests(3)
assert.deepEqual(
received.map((event) => event.hook_event_name),
['agent_start', 'agent_end', 'agent_end']
)
const recovered = [...received]
received.length = 0
rejectCompletion = true
await emit('agent_start')
await waitForRequests(1)
await emit('agent_end')
await waitForRequests(2)
await emit('agent_start')
await delay(600)
assert.equal(
received.filter((event) => event.hook_event_name === 'agent_end').length,
1,
'An obsolete completion retried after the next turn started'
)
for (const boundary of ['session_switch', 'session_shutdown']) {
received.length = 0
rejectCompletion = true
sessionId = 'A'
await emit('agent_start')
await waitForRequests(1)
await emit('agent_end')
await waitForRequests(2)
sessionId = 'B'
await emit(boundary, { reason: 'new' })
await emit('agent_start')
await waitForRequests(4)
await delay(600)
const completions = received.filter((event) => event.hook_event_name === 'agent_end')
assert.equal(completions.length, 2, `Unsent completion was lost at ${boundary}`)
assert.ok(completions.every((event) => event.session_id === 'A'))
assert.equal(received.at(-1).hook_event_name, 'agent_start')
assert.equal(received.at(-1).session_id, 'B', 'Old completion overwrote the new session')
}
console.log(
JSON.stringify({
platform: process.platform,
node: process.version,
recovered,
cancelledAt: ['agent_start'],
preservedAt: ['session_switch', 'session_shutdown'],
scope: 'Generated OMP extension, real native HTTP 503, synthetic lifecycle callbacks'
})
)
} finally {
server.closeAllConnections()
server.close()
await rm(scratch, { recursive: true, force: true })
}