Files
orca/src/shared/codex-subagent-transcript.test.ts
T

301 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { afterEach, describe, expect, it, vi } from 'vitest'
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import {
createCodexSubagentTranscriptState,
hasTrackedCodexTranscriptSubagents,
reconcileCodexSubagentTranscript
} from './codex-subagent-transcript'
import {
codexRosterToSnapshots,
queueCodexSubagentIdentity,
retryPendingCodexSubagentIdentities,
upsertCodexSubagent,
type CodexSubagentRoster
} from './codex-subagent-roster'
const CHILD_ID = '019fa65f-3144-7151-9c02-cff7a28f316f'
function jsonl(records: unknown[]): string {
return `${records.map((record) => JSON.stringify(record)).join('\n')}\n`
}
function activity(kind: string, occurredAtMs = 1234): unknown {
return {
type: 'event_msg',
payload: {
type: 'sub_agent_activity',
occurred_at_ms: occurredAtMs,
agent_thread_id: CHILD_ID,
agent_path: '/root/sidebar_repro',
kind
}
}
}
/** Match Codex’s local-start-date rollout directories. */
function dayDirectory(root: string, atMs: number): string {
const at = new Date(atMs)
const pad = (value: number): string => String(value).padStart(2, '0')
return join(root, String(at.getFullYear()), pad(at.getMonth() + 1), pad(at.getDate()))
}
describe('Codex subagent transcript reconciliation', () => {
const dirs: string[] = []
afterEach(() => {
for (const dir of dirs) {
rmSync(dir, { recursive: true, force: true })
}
dirs.length = 0
})
it('adds a child from the parent rollout and removes it after task completion', () => {
const dir = mkdtempSync(join(tmpdir(), 'codex-subagent-transcript-'))
dirs.push(dir)
const parentPath = join(dir, 'rollout-parent.jsonl')
const childPath = join(dir, `rollout-child-${CHILD_ID}.jsonl`)
writeFileSync(parentPath, jsonl([activity('started')]))
writeFileSync(childPath, jsonl([{ type: 'event_msg', payload: { type: 'task_started' } }]))
const state = createCodexSubagentTranscriptState()
const roster: CodexSubagentRoster = new Map()
reconcileCodexSubagentTranscript(state, roster, parentPath)
expect(hasTrackedCodexTranscriptSubagents(state)).toBe(true)
expect(codexRosterToSnapshots(roster)).toEqual([
{
id: CHILD_ID,
description: 'sidebar_repro',
state: 'working',
startedAt: 1234,
agentType: undefined,
model: undefined
}
])
writeFileSync(
childPath,
jsonl([
{ type: 'event_msg', payload: { type: 'task_started' } },
{ type: 'event_msg', payload: { type: 'task_complete' } }
])
)
reconcileCodexSubagentTranscript(state, roster, parentPath)
expect(hasTrackedCodexTranscriptSubagents(state)).toBe(false)
expect(codexRosterToSnapshots(roster)).toBeUndefined()
})
it('resolves a child rollout filed under a later session day than the parent', () => {
const root = mkdtempSync(join(tmpdir(), 'codex-subagent-transcript-'))
dirs.push(root)
const childStartedAt = Date.now()
const parentDir = dayDirectory(root, childStartedAt - 24 * 60 * 60 * 1000)
const childDir = dayDirectory(root, childStartedAt)
mkdirSync(parentDir, { recursive: true })
mkdirSync(childDir, { recursive: true })
const parentPath = join(parentDir, 'rollout-parent.jsonl')
const childPath = join(childDir, `rollout-child-${CHILD_ID}.jsonl`)
writeFileSync(parentPath, jsonl([activity('started', childStartedAt)]))
writeFileSync(childPath, jsonl([{ type: 'event_msg', payload: { type: 'task_started' } }]))
const state = createCodexSubagentTranscriptState()
const roster: CodexSubagentRoster = new Map()
reconcileCodexSubagentTranscript(state, roster, parentPath)
writeFileSync(
childPath,
jsonl([
{ type: 'event_msg', payload: { type: 'task_started' } },
{ type: 'event_msg', payload: { type: 'task_complete' } }
])
)
reconcileCodexSubagentTranscript(state, roster, parentPath)
// Why: only a cross-day lookup can observe the completion; the parent-directory scan never finds this file.
expect(roster.size).toBe(0)
expect(hasTrackedCodexTranscriptSubagents(state)).toBe(false)
})
it('retires a child whose rollout never becomes readable', () => {
vi.useFakeTimers()
try {
const dir = mkdtempSync(join(tmpdir(), 'codex-subagent-transcript-'))
dirs.push(dir)
const parentPath = join(dir, 'rollout-parent.jsonl')
writeFileSync(parentPath, jsonl([activity('started')]))
const state = createCodexSubagentTranscriptState()
const roster: CodexSubagentRoster = new Map()
reconcileCodexSubagentTranscript(state, roster, parentPath)
expect(roster.size).toBe(1)
// Why: within the grace window a slow-to-appear rollout must not drop a live child.
vi.advanceTimersByTime(30_000)
reconcileCodexSubagentTranscript(state, roster, parentPath)
expect(roster.size).toBe(1)
vi.advanceTimersByTime(31_000)
reconcileCodexSubagentTranscript(state, roster, parentPath)
expect(roster.size).toBe(0)
expect(hasTrackedCodexTranscriptSubagents(state)).toBe(false)
} finally {
vi.useRealTimers()
}
})
it('names a hook-only worker child from its own rollout once the hook read missed it', () => {
const dir = mkdtempSync(join(tmpdir(), 'codex-subagent-transcript-'))
dirs.push(dir)
const parentPath = join(dir, 'rollout-parent.jsonl')
const childPath = join(dir, `rollout-child-${CHILD_ID}.jsonl`)
writeFileSync(parentPath, '')
const state = createCodexSubagentTranscriptState()
const roster: CodexSubagentRoster = new Map()
// SubagentStart carries only the role; a session_meta past 64 KiB (large tool lists) used to stay unread.
upsertCodexSubagent(roster, CHILD_ID, { agentType: 'worker', state: 'working' }, 1234)
writeFileSync(
childPath,
jsonl([
{
type: 'session_meta',
payload: {
id: CHILD_ID,
base_instructions: { text: 'x'.repeat(100 * 1024) },
source: {
subagent: {
thread_spawn: { agent_path: null, agent_nickname: 'Planck', agent_role: 'worker' }
}
}
}
}
])
)
reconcileCodexSubagentTranscript(state, roster, parentPath)
expect(retryPendingCodexSubagentIdentities(roster)).toBe(true)
expect(codexRosterToSnapshots(roster)).toEqual([
expect.objectContaining({ id: CHILD_ID, agentType: 'worker', description: 'Planck' })
])
})
it('keeps a task path the parent supplied over a queued header read', () => {
const dir = mkdtempSync(join(tmpdir(), 'codex-subagent-transcript-'))
dirs.push(dir)
const childPath = join(dir, `rollout-child-${CHILD_ID}.jsonl`)
writeFileSync(
childPath,
jsonl([{ type: 'session_meta', payload: { id: CHILD_ID, agent_nickname: 'Planck' } }])
)
const roster: CodexSubagentRoster = new Map()
upsertCodexSubagent(roster, CHILD_ID, { agentType: 'worker', state: 'working' }, 1234)
queueCodexSubagentIdentity(roster, CHILD_ID, childPath)
upsertCodexSubagent(roster, CHILD_ID, { description: 'pr_review', state: 'working' }, 1234)
expect(retryPendingCodexSubagentIdentities(roster)).toBe(false)
expect(codexRosterToSnapshots(roster)?.[0]).toMatchObject({ description: 'pr_review' })
})
it('removes a child when Codex reports it interrupted', () => {
const dir = mkdtempSync(join(tmpdir(), 'codex-subagent-transcript-'))
dirs.push(dir)
const parentPath = join(dir, 'rollout-parent.jsonl')
writeFileSync(parentPath, jsonl([activity('started')]))
const state = createCodexSubagentTranscriptState()
const roster: CodexSubagentRoster = new Map()
reconcileCodexSubagentTranscript(state, roster, parentPath)
writeFileSync(parentPath, jsonl([activity('started'), activity('interrupted')]))
reconcileCodexSubagentTranscript(state, roster, parentPath)
expect(hasTrackedCodexTranscriptSubagents(state)).toBe(false)
expect(roster.size).toBe(0)
})
describe('child model identity', () => {
function turnContext(model: string): unknown {
return { type: 'turn_context', payload: { turn_id: 'turn-1', model } }
}
function started(): unknown {
return { type: 'event_msg', payload: { type: 'task_started' } }
}
/** Parent rollout with one live child, plus that child's own rollout. */
function seedPair(childRecords: unknown[]): {
parentPath: string
childPath: string
} {
const dir = mkdtempSync(join(tmpdir(), 'codex-subagent-model-'))
dirs.push(dir)
const parentPath = join(dir, 'rollout-parent.jsonl')
const childPath = join(dir, `rollout-child-${CHILD_ID}.jsonl`)
writeFileSync(parentPath, jsonl([turnContext('gpt-5.6-sol'), activity('started')]))
writeFileSync(childPath, jsonl(childRecords))
return { parentPath, childPath }
}
it('reports the model from the child rollout, not the parent model', () => {
const { parentPath } = seedPair([started(), turnContext('gpt-5.6-terra')])
const state = createCodexSubagentTranscriptState()
const roster: CodexSubagentRoster = new Map()
reconcileCodexSubagentTranscript(state, roster, parentPath)
// The parent runs sol; only the child's own turn_context may set its model.
expect(codexRosterToSnapshots(roster)?.[0]?.model).toBe('gpt-5.6-terra')
})
it('keeps the discovered model when a later poll carries no turn_context', () => {
const { parentPath, childPath } = seedPair([started(), turnContext('gpt-5.6-terra')])
const state = createCodexSubagentTranscriptState()
const roster: CodexSubagentRoster = new Map()
reconcileCodexSubagentTranscript(state, roster, parentPath)
// The cursor is incremental: this appended line is all the next read sees.
writeFileSync(
childPath,
jsonl([
started(),
turnContext('gpt-5.6-terra'),
{ type: 'event_msg', payload: { type: 'agent_message' } }
])
)
reconcileCodexSubagentTranscript(state, roster, parentPath)
expect(codexRosterToSnapshots(roster)?.[0]?.model).toBe('gpt-5.6-terra')
})
it('tracks the newest model when the child switches mid-session', () => {
const { parentPath } = seedPair([
started(),
turnContext('gpt-5.6-terra'),
turnContext('gpt-5.6-sol')
])
const state = createCodexSubagentTranscriptState()
const roster: CodexSubagentRoster = new Map()
reconcileCodexSubagentTranscript(state, roster, parentPath)
expect(codexRosterToSnapshots(roster)?.[0]?.model).toBe('gpt-5.6-sol')
})
it('does not resurrect a child that completed in the same read', () => {
const { parentPath } = seedPair([
started(),
turnContext('gpt-5.6-terra'),
{ type: 'event_msg', payload: { type: 'task_complete' } }
])
const state = createCodexSubagentTranscriptState()
const roster: CodexSubagentRoster = new Map()
reconcileCodexSubagentTranscript(state, roster, parentPath)
expect(roster.size).toBe(0)
expect(hasTrackedCodexTranscriptSubagents(state)).toBe(false)
})
})
})