mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
118 lines
4.5 KiB
TypeScript
118 lines
4.5 KiB
TypeScript
import { appendFileSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
import { normalizeHookPayload } from './agent-hook-listener'
|
|
import {
|
|
createHookListenerState,
|
|
seedLegacyAgentStatusForTests
|
|
} from './agent-hook-listener/listener-state'
|
|
import { pollCodexTranscriptStatus } from './agent-hook-listener/providers/codex-transcript-poll'
|
|
import { shouldPollHookTranscript } from './agent-hook-listener/transcript-poll-policy'
|
|
import { makePaneKey } from './stable-pane-id'
|
|
|
|
const PANE = makePaneKey('tab', '11111111-1111-4111-8111-111111111111')
|
|
const CHILD = '01a16839-7f84-7ed3-aebf-ea51e50330cf'
|
|
const line = (type: string): string =>
|
|
`${JSON.stringify({ type: 'event_msg', payload: { type } })}\n`
|
|
|
|
describe('Codex hook-only child transcript reconciliation', () => {
|
|
const directories: string[] = []
|
|
afterEach(() => {
|
|
for (const directory of directories) {
|
|
rmSync(directory, { recursive: true, force: true })
|
|
}
|
|
directories.length = 0
|
|
})
|
|
|
|
function fixture(completed: boolean) {
|
|
const directory = mkdtempSync(join(tmpdir(), 'codex-hook-child-'))
|
|
directories.push(directory)
|
|
const parentPath = join(directory, 'rollout-parent.jsonl')
|
|
const childPath = join(directory, `rollout-child-${CHILD}.jsonl`)
|
|
writeFileSync(parentPath, line('task_complete'))
|
|
writeFileSync(childPath, line('task_started') + (completed ? line('task_complete') : ''))
|
|
const state = createHookListenerState()
|
|
const rootBody = {
|
|
paneKey: PANE,
|
|
payload: {
|
|
hook_event_name: 'Stop',
|
|
session_id: 'root',
|
|
transcript_path: parentPath
|
|
}
|
|
}
|
|
const root = normalizeHookPayload(state, 'codex', rootBody, 'production')
|
|
if (!root) {
|
|
throw new Error('Root hook was rejected')
|
|
}
|
|
const childBody = {
|
|
paneKey: PANE,
|
|
payload: {
|
|
hook_event_name: 'SubagentStart',
|
|
agent_id: CHILD,
|
|
session_id: CHILD,
|
|
transcript_path: childPath
|
|
}
|
|
}
|
|
return { state, root, childBody, childPath }
|
|
}
|
|
|
|
it('keeps a live hook-only child working, then settles without a SubagentStop hook', () => {
|
|
const { state, childBody, childPath } = fixture(false)
|
|
const started = normalizeHookPayload(state, 'codex', childBody, 'production')
|
|
if (!started) {
|
|
throw new Error('Child hook was rejected')
|
|
}
|
|
expect(started.payload).toMatchObject({
|
|
state: 'working',
|
|
mainAgent: { state: 'done' },
|
|
subagents: [{ id: CHILD, state: 'working' }]
|
|
})
|
|
expect(shouldPollHookTranscript(state, 'codex', started)).toBe(true)
|
|
appendFileSync(childPath, line('task_complete'))
|
|
const settled = pollCodexTranscriptStatus(state, started, childBody, 'production')
|
|
expect(settled?.payload.state).toBe('done')
|
|
expect(settled?.payload.subagents).toBeUndefined()
|
|
if (!settled) {
|
|
throw new Error('No completion status')
|
|
}
|
|
expect(shouldPollHookTranscript(state, 'codex', settled)).toBe(false)
|
|
})
|
|
|
|
it('does not resurrect a completed child when a delayed start hook arrives', () => {
|
|
const { state, childBody } = fixture(true)
|
|
const delayed = normalizeHookPayload(state, 'codex', childBody, 'production')
|
|
expect(delayed?.payload.state).toBe('done')
|
|
expect(delayed?.payload.subagents).toBeUndefined()
|
|
})
|
|
|
|
it('reconciles a child using the retained root path after clearing transcript state', () => {
|
|
const { state, root, childBody } = fixture(true)
|
|
seedLegacyAgentStatusForTests(state, root)
|
|
state.codexSubagentTranscriptByPaneKey.clear()
|
|
const delayed = normalizeHookPayload(state, 'codex', childBody, 'production')
|
|
expect(delayed?.payload.state).toBe('done')
|
|
expect(delayed?.payload.subagents).toBeUndefined()
|
|
expect(state.codexSubagentTranscriptByPaneKey.get(PANE)?.parent.filePath).toBe(
|
|
root.providerSession?.transcriptPath
|
|
)
|
|
})
|
|
|
|
it('honors an explicit stop even before the child transcript has flushed its completion', () => {
|
|
const { state, childBody } = fixture(false)
|
|
normalizeHookPayload(state, 'codex', childBody, 'production')
|
|
const stopped = normalizeHookPayload(
|
|
state,
|
|
'codex',
|
|
{ ...childBody, payload: { ...childBody.payload, hook_event_name: 'SubagentStop' } },
|
|
'production'
|
|
)
|
|
expect(stopped?.payload.state).toBe('done')
|
|
expect(stopped?.payload.subagents).toBeUndefined()
|
|
if (!stopped) {
|
|
throw new Error('Child stop was rejected')
|
|
}
|
|
expect(shouldPollHookTranscript(state, 'codex', stopped)).toBe(false)
|
|
})
|
|
})
|