Files
orca/src/shared/agent-transcript-poll-scheduler.test.ts
T
Neil 02790c53e8 Fix Codex status after Ctrl+C copy and side-chat navigation (#24339)
* fix: preserve Codex status on ambiguous Ctrl+C input

* fix: confirm Codex turn cancellations from host rollout records

* fix: keep ephemeral side hooks separate from the Codex main turn

* perf: watch active Codex rollouts and skip unrelated records

* fix: retain confirmed Codex cancellation across late relay events
2026-10-01 17:33:58 -07:00

143 lines
4.8 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const watchers = vi.hoisted(() => ({
bound: true,
entries: Array<{
wake: () => void
error: () => void
bind: ReturnType<typeof vi.fn>
dispose: ReturnType<typeof vi.fn>
rebind: boolean
}>()
}))
vi.mock('./transcript-native-watcher', () => ({
createTranscriptNativeWatcher: (_path: string, wake: () => void, error: () => void) => {
const entry = {
wake,
error,
rebind: true,
bind: vi.fn(() => {
entry.rebind = !watchers.bound
return watchers.bound
}),
dispose: vi.fn()
}
watchers.entries.push(entry)
return { bind: entry.bind, dispose: entry.dispose, needsRebind: () => entry.rebind }
}
}))
import { AgentTranscriptPollScheduler } from './agent-transcript-poll-scheduler'
import { CodexSubagentPollScheduler } from './codex-subagent-poll-scheduler'
describe('host transcript wakeup budget', () => {
beforeEach(() => {
vi.useFakeTimers()
watchers.bound = true
watchers.entries.length = 0
})
afterEach(() => vi.useRealTimers())
it.each([500, 1_000])(
'bounds quiet root probes across 100 panes with one timer (cadence %i ms)',
(cadence) => {
let before = 0
let after = 0
const baseline = new CodexSubagentPollScheduler<number>(cadence, (key, value) => {
before++
baseline.schedule(key, value)
})
const optimized = new AgentTranscriptPollScheduler<number>(cadence, (key, value) => {
after++
optimized.schedule(key, value, `/rollout-${key}.jsonl`)
})
for (let i = 0; i < 100; i++) {
baseline.schedule(String(i), i)
optimized.schedule(String(i), i, `/rollout-${i}.jsonl`)
}
expect(vi.getTimerCount()).toBe(2)
vi.advanceTimersByTime(60_000)
expect(before).toBe(100 * (60_000 / cadence))
expect(after).toBe(1_200)
optimized.clearAll()
baseline.clearAll()
expect(vi.getTimerCount()).toBe(0)
expect(watchers.entries.every((entry) => entry.dispose.mock.calls.length === 1)).toBe(true)
}
)
it('coalesces a stream without postponing reads and reuses the latest hook', () => {
const seen: string[] = []
const scheduler = new AgentTranscriptPollScheduler<string>(500, (key, value) => {
seen.push(value)
scheduler.schedule(key, value, '/rollout.jsonl')
})
scheduler.schedule('pane', 'first', '/rollout.jsonl')
vi.advanceTimersByTime(500)
const watch = watchers.entries[0]!
for (let i = 0; i < 1_000; i++) {
watch.wake()
scheduler.schedule('pane', `hook-${i}`, '/rollout.jsonl')
}
expect(watchers.entries).toHaveLength(1)
expect(vi.getTimerCount()).toBe(1)
vi.advanceTimersByTime(500)
expect(seen).toEqual(['first', 'hook-999'])
scheduler.clear('pane')
watch.wake()
expect(vi.getTimerCount()).toBe(0)
expect(watch.dispose).toHaveBeenCalledOnce()
})
it('keeps unreadable watches on the old cadence but bounds bind retries', () => {
watchers.bound = false
let reads = 0
const scheduler = new AgentTranscriptPollScheduler<number>(500, (key, value) => {
reads++
scheduler.schedule(key, value, '/missing.jsonl')
})
scheduler.schedule('pane', 1, '/missing.jsonl')
vi.advanceTimersByTime(10_000)
expect(reads).toBe(20)
expect(watchers.entries[0]!.bind).toHaveBeenCalledTimes(3)
scheduler.clearAll()
})
it('rebinds after rename or error and drops replaced watches and stale callbacks', () => {
const seen: number[] = []
const scheduler = new AgentTranscriptPollScheduler<number>(500, (key, value) => {
seen.push(value)
scheduler.schedule(key, value, '/new.jsonl')
})
scheduler.schedule('pane', 1, '/old.jsonl')
const old = watchers.entries[0]!
scheduler.schedule('pane', 2, '/new.jsonl')
old.wake()
expect(old.dispose).toHaveBeenCalledOnce()
vi.advanceTimersByTime(500)
const current = watchers.entries[1]!
current.rebind = true
current.wake()
vi.advanceTimersByTime(500)
expect(current.bind).toHaveBeenCalledTimes(2)
current.rebind = true
current.error()
vi.advanceTimersByTime(500)
expect(current.bind).toHaveBeenCalledTimes(3)
expect(seen).toEqual([2, 2, 2])
scheduler.clearAll()
})
it('retains polling for children and WSL UNC and releases abandoned callbacks', () => {
const seen = vi.fn()
const scheduler = new AgentTranscriptPollScheduler<number>(500, seen)
scheduler.schedule('child', 1)
scheduler.schedule('wsl', 2, '\\\\wsl.localhost\\Ubuntu\\rollout.jsonl')
scheduler.schedule('root', 3, '/rollout.jsonl')
vi.advanceTimersByTime(500)
expect(seen).toHaveBeenCalledTimes(3)
expect(watchers.entries).toHaveLength(1)
expect(watchers.entries[0]!.dispose).toHaveBeenCalledOnce()
expect(vi.getTimerCount()).toBe(0)
})
})