mirror of
https://github.com/stablyai/orca.git
synced 2026-10-09 00:02:39 +00:00
sim: merge PR #17209
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()',
|
||||
|
||||
Reference in New Issue
Block a user