sim: merge PR #17209

This commit is contained in:
Brennan Benson
2026-08-30 15:53:59 -07:00
4 changed files with 310 additions and 191 deletions
@@ -0,0 +1,107 @@
import { describe, expect, it, vi } from 'vitest'
import { createAgentStatusExtensionHarness } from './agent-status-extension-test-harness'
function postedHookNames(fetchMock: ReturnType<typeof vi.fn>): string[] {
return fetchMock.mock.calls.map(
(call) => JSON.parse(String(call[1]?.body)).payload.hook_event_name as string
)
}
const OMP_RUNTIME_CASES = [
['configured OMP', { kind: 'omp' as const }],
['title-routed OMP', { kind: 'pi' as const, title: 'omp' }],
['argv-routed OMP', { kind: 'pi' as const, argv: ['node', '/usr/local/bin/omp'] }]
] as const
describe('OMP agent_end contract', () => {
it.each(OMP_RUNTIME_CASES)(
'keeps %s working when agent_end will continue',
async (_name, args) => {
vi.useFakeTimers()
try {
const harness = createAgentStatusExtensionHarness(args)
const context = { isIdle: vi.fn(() => true) }
await harness.callHook('agent_start')
await harness.callHook('agent_end', { willContinue: true }, context)
await vi.advanceTimersByTimeAsync(1_000)
expect(postedHookNames(harness.fetchMock)).toEqual(['agent_start'])
expect(context.isIdle).not.toHaveBeenCalled()
expect(vi.getTimerCount()).toBe(0)
} finally {
vi.useRealTimers()
}
}
)
it.each(OMP_RUNTIME_CASES)(
'settles a completed %s turn without waiting for ctx.isIdle',
async (_name, args) => {
// Why: absent payload and absent flag are both terminal for a version that cannot send one.
for (const event of [{ willContinue: false }, {}, undefined]) {
const harness = createAgentStatusExtensionHarness(args)
const context = { isIdle: vi.fn(() => false) }
await harness.callHook('agent_start')
await harness.callHook('agent_end', event, context)
await vi.waitFor(() =>
expect(postedHookNames(harness.fetchMock)).toEqual(['agent_start', 'agent_end'])
)
expect(context.isIdle).not.toHaveBeenCalled()
}
}
)
it('settles a later terminal OMP agent_end after a continuation', async () => {
const harness = createAgentStatusExtensionHarness({ kind: 'omp' })
const context = { isIdle: vi.fn(() => false) }
await harness.callHook('agent_start')
await harness.callHook('agent_end', { willContinue: true }, context)
expect(postedHookNames(harness.fetchMock)).toEqual(['agent_start'])
await harness.callHook('agent_end', { willContinue: false }, context)
await vi.waitFor(() =>
expect(postedHookNames(harness.fetchMock)).toEqual(['agent_start', 'agent_end'])
)
})
it('does not apply the OMP contract to Pi or Prime', async () => {
vi.useFakeTimers()
try {
for (const kind of ['pi', 'prime-agent'] as const) {
const harness = createAgentStatusExtensionHarness({ kind })
const context = { isIdle: vi.fn(() => false) }
await harness.callHook('agent_end', { willContinue: false }, context)
await vi.advanceTimersByTimeAsync(1_000)
expect(postedHookNames(harness.fetchMock)).toEqual([])
expect(context.isIdle).toHaveBeenCalled()
}
} finally {
vi.useRealTimers()
}
})
it('preserves non-terminal agent_end handling for Pi and Prime', async () => {
vi.useFakeTimers()
try {
for (const kind of ['pi', 'prime-agent'] as const) {
const harness = createAgentStatusExtensionHarness({ kind })
const context = { isIdle: vi.fn(() => true) }
await harness.callHook('agent_end', { willContinue: true }, context)
await vi.advanceTimersByTimeAsync(1_000)
expect(postedHookNames(harness.fetchMock)).toEqual([])
expect(context.isIdle).not.toHaveBeenCalled()
}
} finally {
vi.useRealTimers()
}
})
})
@@ -1,193 +1,9 @@
import { runInNewContext } from 'node:vm'
// TypeScript 7 is a native CLI; transpile tests still need the legacy JavaScript API.
import ts from 'typescript-api'
import { describe, expect, it, vi } from 'vitest'
import { getPiAgentStatusExtensionSource } from './agent-status-extension-source'
type HookContext = {
isIdle?: () => boolean
sessionManager?: {
getSessionId?: () => unknown
getSessionFile?: () => unknown
}
}
type HookHandler = (event?: unknown, context?: HookContext) => Promise<void> | void
type FakeCurlChild = {
on: ReturnType<typeof vi.fn>
stdin: {
on: ReturnType<typeof vi.fn>
end: ReturnType<typeof vi.fn>
}
}
type Harness = {
fetchMock: ReturnType<typeof vi.fn>
spawnMock: ReturnType<typeof vi.fn>
spawnedChildren: FakeCurlChild[]
fsMock: {
existsSync: ReturnType<typeof vi.fn>
readFileSync: ReturnType<typeof vi.fn>
statSync: ReturnType<typeof vi.fn>
}
handlers: Record<string, HookHandler>
processEnv: Record<string, string | undefined>
callHook: (name: string, event?: unknown, context?: HookContext) => Promise<void>
// Re-invoke the extension factory in the same process (as Pi does on an
// in-process extension reload), swapping in the freshly registered handlers.
reload: () => void
}
const BASE_ENV = {
ORCA_PANE_KEY: 'pane-1',
ORCA_AGENT_LAUNCH_TOKEN: 'launch-1',
ORCA_TAB_ID: 'tab-1',
ORCA_WORKTREE_ID: 'tree-1',
ORCA_AGENT_HOOK_PORT: '4321',
ORCA_AGENT_HOOK_TOKEN: 'token-1',
ORCA_AGENT_HOOK_ENV: 'env-1',
ORCA_AGENT_HOOK_VERSION: '1.2.3'
} satisfies Record<string, string>
// Why: ownership keys on process.pid, so reload and child-process tests need
// stable, distinct identities.
const SELF_PID = 4242
function createHarness(args: {
kind: 'pi' | 'omp' | 'prime-agent'
env?: Record<string, string | undefined>
pid?: number
title?: string
argv?: string[]
existsSync?: (path: string) => boolean
readFileSync?: (path: string, encoding: string) => string
statSync?: (path: string) => { mtimeMs: number; size: number; ino: number }
fetchImpl?: (...params: Parameters<typeof fetch>) => Promise<unknown>
}): Harness {
const fetchMock = vi.fn(
args.fetchImpl ??
(async () => ({
ok: true
}))
)
const spawnedChildren: FakeCurlChild[] = []
const spawnMock = vi.fn(() => {
const child: FakeCurlChild = {
on: vi.fn(),
stdin: {
on: vi.fn(),
end: vi.fn()
}
}
spawnedChildren.push(child)
return child
})
const fsMock = {
existsSync: vi.fn(args.existsSync ?? (() => false)),
statSync: vi.fn(
args.statSync ??
((path: string) => {
throw Object.assign(new Error(`ENOENT: ${path}`), { code: 'ENOENT' })
})
),
readFileSync: vi.fn(
args.readFileSync ??
((path: string) => {
throw Object.assign(new Error(`ENOENT: ${path}`), { code: 'ENOENT' })
})
)
}
const module = {
exports: {} as { default?: (pi: { on: (name: string, handler: HookHandler) => void }) => void }
}
const requireMock = vi.fn((specifier: string) => {
if (specifier === 'fs') {
return fsMock
}
if (specifier === 'child_process') {
return { spawn: spawnMock }
}
throw new Error(`unexpected require(${specifier})`)
})
const processMock = {
env: {
...BASE_ENV,
...(args.kind === 'prime-agent' ? { PRIME_AGENT_INTERNAL_DAEMON_WORKER: '1' } : {}),
...args.env
},
pid: args.pid ?? SELF_PID,
title: args.title ?? 'node',
argv: args.argv ?? ['node', '/usr/bin/orca']
}
const context = {
module,
exports: module.exports,
require: requireMock,
process: processMock,
fetch: fetchMock,
console: {
warn: vi.fn(),
error: vi.fn(),
log: vi.fn()
},
Promise,
Buffer,
URL,
AbortController,
setTimeout,
clearTimeout
} as Record<string, unknown>
context.globalThis = context
const source = getPiAgentStatusExtensionSource(args.kind)
const output = ts.transpileModule(source, {
compilerOptions: {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2020
}
}).outputText
runInNewContext(output, context)
const register = module.exports.default
if (!register) {
throw new Error('expected default export from generated source')
}
const handlers: Record<string, HookHandler> = {}
const registerInto = (target: Record<string, HookHandler>): void => {
register({
on(name: string, handler: HookHandler) {
target[name] = handler
}
})
}
registerInto(handlers)
return {
fetchMock,
spawnMock,
spawnedChildren,
fsMock,
handlers,
processEnv: processMock.env,
callHook: async (name, event, hookContext) => {
await handlers[name]?.(event, hookContext)
},
reload: () => {
for (const key of Object.keys(handlers)) {
delete handlers[key]
}
registerInto(handlers)
}
}
}
import {
AGENT_STATUS_EXTENSION_SELF_PID as SELF_PID,
createAgentStatusExtensionHarness as createHarness
} from './agent-status-extension-test-harness'
describe('getPiAgentStatusExtensionSource', () => {
it('registers Prime hooks only in the event-emitting daemon worker', () => {
@@ -826,10 +642,10 @@ describe('getPiAgentStatusExtensionSource', () => {
}
})
it('keeps reporting Pi-compatible agents once their agent_end handlers settle', async () => {
it('keeps polling Pi and Prime until their agent_end handlers settle', async () => {
vi.useFakeTimers()
try {
for (const kind of ['pi', 'omp', 'prime-agent'] as const) {
for (const kind of ['pi', 'prime-agent'] as const) {
const harness = createHarness({ kind })
let idle = false
const context = { isIdle: vi.fn(() => idle) }
@@ -0,0 +1,190 @@
import { runInNewContext } from 'node:vm'
// TypeScript 7 is a native CLI; transpile tests still need the legacy JavaScript API.
import ts from 'typescript-api'
import { vi } from 'vitest'
import { getPiAgentStatusExtensionSource } from './agent-status-extension-source'
export type HookContext = {
isIdle?: () => boolean
sessionManager?: {
getSessionId?: () => unknown
getSessionFile?: () => unknown
}
}
export type HookHandler = (event?: unknown, context?: HookContext) => Promise<void> | void
type FakeCurlChild = {
on: ReturnType<typeof vi.fn>
stdin: {
on: ReturnType<typeof vi.fn>
end: ReturnType<typeof vi.fn>
}
}
export type AgentStatusExtensionHarness = {
fetchMock: ReturnType<typeof vi.fn>
spawnMock: ReturnType<typeof vi.fn>
spawnedChildren: FakeCurlChild[]
fsMock: {
existsSync: ReturnType<typeof vi.fn>
readFileSync: ReturnType<typeof vi.fn>
statSync: ReturnType<typeof vi.fn>
}
handlers: Record<string, HookHandler>
processEnv: Record<string, string | undefined>
callHook: (name: string, event?: unknown, context?: HookContext) => Promise<void>
// Re-invoke the extension factory in the same process (as Pi does on an
// in-process extension reload), swapping in the freshly registered handlers.
reload: () => void
}
const BASE_ENV = {
ORCA_PANE_KEY: 'pane-1',
ORCA_AGENT_LAUNCH_TOKEN: 'launch-1',
ORCA_TAB_ID: 'tab-1',
ORCA_WORKTREE_ID: 'tree-1',
ORCA_AGENT_HOOK_PORT: '4321',
ORCA_AGENT_HOOK_TOKEN: 'token-1',
ORCA_AGENT_HOOK_ENV: 'env-1',
ORCA_AGENT_HOOK_VERSION: '1.2.3'
} satisfies Record<string, string>
// Why: ownership keys on process.pid, so reload and child-process tests need
// stable, distinct identities.
export const AGENT_STATUS_EXTENSION_SELF_PID = 4242
export function createAgentStatusExtensionHarness(args: {
kind: 'pi' | 'omp' | 'prime-agent'
env?: Record<string, string | undefined>
pid?: number
title?: string
argv?: readonly string[]
existsSync?: (path: string) => boolean
readFileSync?: (path: string, encoding: string) => string
statSync?: (path: string) => { mtimeMs: number; size: number; ino: number }
fetchImpl?: (...params: Parameters<typeof fetch>) => Promise<unknown>
}): AgentStatusExtensionHarness {
const fetchMock = vi.fn(
args.fetchImpl ??
(async () => ({
ok: true
}))
)
const spawnedChildren: FakeCurlChild[] = []
const spawnMock = vi.fn(() => {
const child: FakeCurlChild = {
on: vi.fn(),
stdin: {
on: vi.fn(),
end: vi.fn()
}
}
spawnedChildren.push(child)
return child
})
const fsMock = {
existsSync: vi.fn(args.existsSync ?? (() => false)),
statSync: vi.fn(
args.statSync ??
((path: string) => {
throw Object.assign(new Error(`ENOENT: ${path}`), { code: 'ENOENT' })
})
),
readFileSync: vi.fn(
args.readFileSync ??
((path: string) => {
throw Object.assign(new Error(`ENOENT: ${path}`), { code: 'ENOENT' })
})
)
}
const module = {
exports: {} as { default?: (pi: { on: (name: string, handler: HookHandler) => void }) => void }
}
const requireMock = vi.fn((specifier: string) => {
if (specifier === 'fs') {
return fsMock
}
if (specifier === 'child_process') {
return { spawn: spawnMock }
}
throw new Error(`unexpected require(${specifier})`)
})
const processMock = {
env: {
...BASE_ENV,
...(args.kind === 'prime-agent' ? { PRIME_AGENT_INTERNAL_DAEMON_WORKER: '1' } : {}),
...args.env
},
pid: args.pid ?? AGENT_STATUS_EXTENSION_SELF_PID,
title: args.title ?? 'node',
argv: args.argv ?? ['node', '/usr/bin/orca']
}
const context = {
module,
exports: module.exports,
require: requireMock,
process: processMock,
fetch: fetchMock,
console: {
warn: vi.fn(),
error: vi.fn(),
log: vi.fn()
},
Promise,
Buffer,
URL,
AbortController,
setTimeout,
clearTimeout
} as Record<string, unknown>
context.globalThis = context
const source = getPiAgentStatusExtensionSource(args.kind)
const output = ts.transpileModule(source, {
compilerOptions: {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2020
}
}).outputText
runInNewContext(output, context)
const register = module.exports.default
if (!register) {
throw new Error('expected default export from generated source')
}
const handlers: Record<string, HookHandler> = {}
const registerInto = (target: Record<string, HookHandler>): void => {
register({
on(name: string, handler: HookHandler) {
target[name] = handler
}
})
}
registerInto(handlers)
return {
fetchMock,
spawnMock,
spawnedChildren,
fsMock,
handlers,
processEnv: processMock.env,
callHook: async (name, event, hookContext) => {
await handlers[name]?.(event, hookContext)
},
reload: () => {
for (const key of Object.keys(handlers)) {
delete handlers[key]
}
registerInto(handlers)
}
}
}
+7 -1
View File
@@ -115,7 +115,9 @@ export function getPiAgentStatusHandlerSourceLines(kind: PiAgentKind): string[]
' })',
'',
' // Why: modern Pi stays non-idle across retry/compaction/follow-up work,',
' // while legacy Pi/OMP becomes idle after its final agent_end handlers.',
' // while legacy Pi becomes idle after its final agent_end handlers.',
' // OMP instead marks non-terminal agent_end events with willContinue, so it',
' // returns before the recheck timer is ever armed.',
' const AGENT_END_IDLE_RECHECK_MS = 25',
' const AGENT_END_IDLE_RECHECK_MAX_MS = 250',
' let agentSettledSupported = false',
@@ -173,6 +175,10 @@ export function getPiAgentStatusHandlerSourceLines(kind: PiAgentKind): string[]
' clearPendingAgentEndCheck()',
' return',
' }',
' if (isOmpRuntime()) {',
' postAgentEndOnce()',
' return',
' }',
' if (agentSettledSupported) return',
" if (!ctx || typeof ctx.isIdle !== 'function') {",
' postAgentEndOnce()',