fix(codex): keep working status when Escape closes search or permissions (#25769)

* fix(codex): preserve working status when Escape dismisses a view

* test(codex): cover navigation Escape during terminal exit cleanup
This commit is contained in:
Neil
2026-10-05 21:34:27 -07:00
committed by GitHub
parent 3ee3a41b6f
commit bfd1e9c579
10 changed files with 281 additions and 238 deletions
@@ -85,16 +85,22 @@ describe('Codex recorded turn interruption', () => {
expect(server.getStatusSnapshot()[0]).toEqual(beforeSide)
}
const baseline = server.getStatusSnapshot()[0]
expect(
server.inferInterrupt({
paneKey: PANE,
baselineUpdatedAt: baseline.receivedAt,
baselineStateStartedAt: baseline.stateStartedAt,
baselinePrompt: baseline.prompt,
baselineAgentType: 'codex',
intent: 'ctrl-c'
})
).toBe(false)
for (const intent of ['ctrl-c', 'plain-escape'] as const) {
for (const inputCount of [1, 2]) {
expect(
server.inferInterrupt({
paneKey: PANE,
baselineUpdatedAt: baseline.receivedAt,
baselineStateStartedAt: baseline.stateStartedAt,
baselinePrompt: baseline.prompt,
baselineAgentType: 'codex',
intent,
inputCount
})
).toBe(false)
expect(server.getStatusSnapshot()[0]).toEqual(baseline)
}
}
await new Promise((resolve) => setTimeout(resolve, 600))
expect(server.getStatusSnapshot()[0].state).toBe('working')
appendFileSync(
@@ -27,7 +27,7 @@ afterEach(() => {
})
describe('AgentHookServer listener replay', () => {
it('keeps Codex lead state terminal after an inferred interrupt', () => {
it('keeps Codex lead state terminal after a confirmed interrupt', () => {
vi.useFakeTimers()
vi.setSystemTime(1_000)
try {
@@ -50,19 +50,19 @@ describe('AgentHookServer listener replay', () => {
},
'conn-1'
)
const baseline = server.getStatusSnapshot()[0]
vi.setSystemTime(1_500)
const applied = server.inferInterrupt({
paneKey: PANE,
baselineUpdatedAt: baseline.receivedAt,
baselineStateStartedAt: baseline.stateStartedAt,
baselinePrompt: 'long task',
baselineAgentType: 'codex',
intent: 'plain-escape'
})
server.ingestRemote(
{
paneKey: PANE,
tabId: 'tab-1',
worktreeId: 'wt-1',
providerSession: { key: 'session_id', id: 'codex-interrupt-session-1' },
hookEventName: 'Interrupt',
payload: { state: 'done', prompt: 'long task', agentType: 'codex', interrupted: true }
},
'conn-1'
)
expect(applied).toBe(true)
expect(server.getStatusSnapshot()).toEqual([
expect.objectContaining({
paneKey: PANE,
@@ -87,7 +87,7 @@ describe('AgentHookServer listener replay', () => {
}
})
it('does not let late Codex tool hooks with explicit prompt resurrect an inferred interrupt', () => {
it('does not let late Codex tool hooks with explicit prompt resurrect a confirmed interrupt', () => {
vi.useFakeTimers()
vi.setSystemTime(1_000)
try {
@@ -107,19 +107,22 @@ describe('AgentHookServer listener replay', () => {
},
'conn-1'
)
const baseline = server.getStatusSnapshot()[0]
vi.setSystemTime(1_500)
expect(
server.inferInterrupt({
server.ingestRemote(
{
paneKey: PANE,
baselineUpdatedAt: baseline.receivedAt,
baselineStateStartedAt: baseline.stateStartedAt,
baselinePrompt: 'Run sleep 30, then reply done.',
baselineAgentType: 'codex',
intent: 'plain-escape'
})
).toBe(true)
tabId: 'tab-1',
worktreeId: 'wt-1',
hookEventName: 'Interrupt',
payload: {
state: 'done',
prompt: 'Run sleep 30, then reply done.',
agentType: 'codex',
interrupted: true
}
},
'conn-1'
)
vi.setSystemTime(6_000)
server.ingestRemote(
@@ -2,7 +2,6 @@ import {
markClaudeLeadTurnInterrupted,
clearClaudeAnsweredQuestionWait
} from '../../../shared/agent-hook-listener/providers/claude-roster-state'
import { markCodexLeadTurnInterrupted } from '../../../shared/agent-hook-listener/providers/codex-state'
import {
isAgentInterruptInputIntent,
isNavigationEscapeIntent,
@@ -81,13 +80,8 @@ export abstract class AgentHookServerStatusInference extends AgentHookServerRowO
this.state.claudeActiveSessionCronPaneKeys.has(existing.paneKey)))
// Why: a 'working' pane can be child-driven, and Ctrl+C at the idle prompt of a main agent that
// child work holds open cancels nothing, so the main agent fact decides. A row from a host too
// old to publish `mainAgent` keeps the evidence guard, and so does Codex: its synthesized row is
// a plain done, which would retire the live children its combine keeps working.
if (
payload.mainAgent
? payload.mainAgent.state !== 'working' || (agentType === 'codex' && childWorkEvidenced)
: childWorkEvidenced
) {
// old to publish `mainAgent` keeps the evidence guard.
if (payload.mainAgent ? payload.mainAgent.state !== 'working' : childWorkEvidenced) {
return false
}
// Why: whoever owns the provider records folds the cancel with the child work the turn left
@@ -101,9 +95,6 @@ export abstract class AgentHookServerStatusInference extends AgentHookServerRowO
agentType === 'claude' && existing.connectionId
? foldMainAgentWithRowChildWork('done', existing)
: undefined
if (agentType === 'codex') {
markCodexLeadTurnInterrupted(this.state, existing.paneKey)
}
const state = local?.state ?? relayed?.stateName ?? 'done'
const workingMode = local?.workingMode ?? relayed?.workingMode
const inferred = this.applyNormalizedStatus({
@@ -56,6 +56,22 @@ it('forwards host-confirmed Codex interruption without requiring a local rollout
expect(sideResponse.status).toBe(204)
expect(forward).toHaveBeenCalledTimes(1)
const baseline = desktop.getStatusSnapshot()[0]
for (const inputCount of [1, 2]) {
expect(
desktop.inferInterrupt({
paneKey: PANE_KEY,
baselineUpdatedAt: baseline.receivedAt,
baselineStateStartedAt: baseline.stateStartedAt,
baselinePrompt: baseline.prompt,
baselineAgentType: 'codex',
intent: 'plain-escape',
inputCount
})
).toBe(false)
expect(desktop.getStatusSnapshot()[0]).toEqual(baseline)
}
appendFileSync(
transcriptPath,
line({ type: 'turn_aborted', turn_id: 'turn-1', reason: 'interrupted' })
@@ -14,7 +14,7 @@ function makeEntry(overrides: Partial<AgentStatusEntry> = {}): AgentStatusEntry
prompt: 'write tests',
updatedAt: 1_000,
stateStartedAt: 900,
agentType: 'codex',
agentType: 'custom-agent',
paneKey: PANE_KEY,
terminalTitle: 'Codex',
stateHistory: [],
@@ -72,8 +72,7 @@ describe('agent interrupt inference', () => {
it.each([
['plain-escape', 'gemini'],
['ctrl-c', 'gemini'],
['plain-escape', 'codex']
['ctrl-c', 'gemini']
] as const)('emits a strict baseline request for %s from %s immediately', (intent, agentType) => {
vi.useFakeTimers()
let entry: AgentStatusEntry | undefined = makeEntry({ agentType })
@@ -321,8 +320,8 @@ describe('agent interrupt inference', () => {
entry = undefined
})
it.each([['claude'], ['omp'], ['pi'], ['prime-agent']] as const)(
'never asks main to interrupt %s on a single Escape while working',
it.each([['claude'], ['codex'], ['omp'], ['pi'], ['prime-agent']] as const)(
'never asks main to interrupt %s on repeated navigation Escape while working',
(agentType) => {
// Why: Escape is ambiguous at the source for these TUIs, so the renderer does not spend a
// round-trip on it. main re-checks the same rule for requests that never came from here.
@@ -336,8 +335,10 @@ describe('agent interrupt inference', () => {
now: () => 1_100
})
tracker.observeInputIntent('plain-escape')
tracker.observeInputIntent('plain-escape')
vi.advanceTimersByTime(500)
expect(tracker.flushPending()).toBe(false)
expect(inferInterrupt).not.toHaveBeenCalled()
tracker.dispose()
@@ -493,7 +494,7 @@ describe('agent interrupt inference', () => {
baselineUpdatedAt: 1_000,
baselineStateStartedAt: 900,
baselinePrompt: 'write tests',
baselineAgentType: 'codex',
baselineAgentType: 'custom-agent',
intent: 'plain-escape'
})
tracker.dispose()
@@ -580,7 +581,7 @@ describe('agent interrupt inference', () => {
baselineUpdatedAt: 2_000,
baselineStateStartedAt: 1_900,
baselinePrompt: 'newer task',
baselineAgentType: 'codex',
baselineAgentType: 'custom-agent',
intent: 'plain-escape'
})
}
@@ -46,8 +46,7 @@ function shouldFlushInterruptImmediately(
): boolean {
return (
requiresDoubleEscapeInterrupt(baseline.agentType, baseline.intent) ||
baseline.agentType === 'gemini' ||
(baseline.agentType === 'codex' && baseline.intent === 'plain-escape')
baseline.agentType === 'gemini'
)
}
@@ -747,96 +747,107 @@ describe('connectPanePty', () => {
expect(resolveMockPaneWindowsShiftEnterEncoding(mockStoreState, paneKey)).toBe('alt-enter')
})
it('pins interrupt inference before acknowledged input and command exit cleanup', async () => {
const { connectPanePty } = await import('./pty-connection')
it.each(['gemini', 'codex'] as const)(
'pins %s interrupt policy before acknowledged input and command exit cleanup',
async (agentType) => {
const { connectPanePty } = await import('./pty-connection')
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
const transport = createMockTransport()
const writeAccepted = createDeferred<boolean>()
transport.sendInputAccepted = vi.fn(() => writeAccepted.promise)
transport.connect.mockImplementation(async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
capturedDataCallback.current = callbacks.onData ?? null
return { id: 'tab-pty' }
})
transport.attach.mockImplementation(({ callbacks }: { callbacks: ConnectCallbacks }) => {
capturedDataCallback.current = callbacks.onData ?? null
})
transportFactoryQueue.push(transport)
vi.useFakeTimers()
vi.setSystemTime(1_100)
const paneKey = makePaneKey('tab-1', LEAF_1)
mockStoreState = {
...mockStoreState,
agentStatusByPaneKey: {
[paneKey]: {
paneKey,
state: 'working',
prompt: 'stop quickly',
updatedAt: 1_000,
stateStartedAt: 900,
agentType: 'codex',
terminalTitle: 'Codex',
stateHistory: []
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
const transport = createMockTransport()
const writeAccepted = createDeferred<boolean>()
transport.sendInputAccepted = vi.fn(() => writeAccepted.promise)
transport.connect.mockImplementation(
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
capturedDataCallback.current = callbacks.onData ?? null
return { id: 'tab-pty' }
}
}
}
vi.mocked(window.api.agentStatus.inferInterrupt).mockImplementation(async () => {
mockStoreState.agentStatusByPaneKey[paneKey] = {
paneKey,
state: 'done',
prompt: 'stop quickly',
updatedAt: 1_100,
stateStartedAt: 1_100,
agentType: 'codex',
terminalTitle: 'Codex',
interrupted: true,
stateHistory: [
{
)
transport.attach.mockImplementation(({ callbacks }: { callbacks: ConnectCallbacks }) => {
capturedDataCallback.current = callbacks.onData ?? null
})
transportFactoryQueue.push(transport)
vi.useFakeTimers()
vi.setSystemTime(1_100)
const paneKey = makePaneKey('tab-1', LEAF_1)
mockStoreState = {
...mockStoreState,
agentStatusByPaneKey: {
[paneKey]: {
paneKey,
state: 'working',
prompt: 'stop quickly',
startedAt: 900
updatedAt: 1_000,
stateStartedAt: 900,
agentType,
terminalTitle: agentType,
stateHistory: []
}
]
}
}
return true
})
const terminalTarget = createKeyboardEventTarget()
const pane = createPane(1)
;(pane.terminal as { element?: unknown }).element = terminalTarget.target
let onDataHandler: ((data: string) => void) | null = null
pane.terminal.onData = vi.fn(((handler: (data: string) => void) => {
onDataHandler = handler
return { dispose: vi.fn() }
}) as typeof pane.terminal.onData)
vi.mocked(window.api.agentStatus.inferInterrupt).mockImplementation(async () => {
mockStoreState.agentStatusByPaneKey[paneKey] = {
paneKey,
state: 'done',
prompt: 'stop quickly',
updatedAt: 1_100,
stateStartedAt: 1_100,
agentType,
terminalTitle: agentType,
interrupted: true,
stateHistory: [
{
state: 'working',
prompt: 'stop quickly',
startedAt: 900
}
]
}
return true
})
const terminalTarget = createKeyboardEventTarget()
const pane = createPane(1)
;(pane.terminal as { element?: unknown }).element = terminalTarget.target
let onDataHandler: ((data: string) => void) | null = null
pane.terminal.onData = vi.fn(((handler: (data: string) => void) => {
onDataHandler = handler
return { dispose: vi.fn() }
}) as typeof pane.terminal.onData)
connectPanePty(pane as never, createManager(1) as never, createDeps() as never)
vi.advanceTimersByTime(1_000)
await flushAsyncTicks()
expect(capturedDataCallback.current).not.toBeNull()
if (!onDataHandler) {
throw new Error('expected onData handler to be registered')
connectPanePty(pane as never, createManager(1) as never, createDeps() as never)
vi.advanceTimersByTime(1_000)
await flushAsyncTicks()
expect(capturedDataCallback.current).not.toBeNull()
if (!onDataHandler) {
throw new Error('expected onData handler to be registered')
}
terminalTarget.dispatch(keyEvent({ key: 'Escape' }))
;(onDataHandler as unknown as (data: string) => void)('\x1b')
capturedDataCallback.current?.('\x1b]133;D;130\x07thebr ~/repo $ ')
expect(window.api.agentStatus.inferInterrupt).not.toHaveBeenCalled()
expect(mockStoreState.dropAgentStatus).not.toHaveBeenCalled()
delete mockStoreState.agentStatusByPaneKey[paneKey]
writeAccepted.resolve(true)
await flushAsyncTicks()
if (agentType === 'codex') {
expect(window.api.agentStatus.inferInterrupt).not.toHaveBeenCalled()
expect(mockStoreState.dropAgentStatus).not.toHaveBeenCalled()
expect(mockStoreState.clearAgentLaunchConfig).toHaveBeenCalledWith(paneKey)
} else {
expect(window.api.agentStatus.inferInterrupt).toHaveBeenCalledWith({
paneKey,
baselineUpdatedAt: 1_000,
baselineStateStartedAt: 900,
baselinePrompt: 'stop quickly',
baselineAgentType: agentType,
intent: 'plain-escape'
})
expect(mockStoreState.dropAgentStatus).toHaveBeenCalledWith(paneKey)
}
}
terminalTarget.dispatch(keyEvent({ key: 'Escape' }))
;(onDataHandler as unknown as (data: string) => void)('\x1b')
capturedDataCallback.current?.('\x1b]133;D;130\x07thebr ~/repo $ ')
expect(window.api.agentStatus.inferInterrupt).not.toHaveBeenCalled()
expect(mockStoreState.dropAgentStatus).not.toHaveBeenCalled()
delete mockStoreState.agentStatusByPaneKey[paneKey]
writeAccepted.resolve(true)
await flushAsyncTicks()
expect(window.api.agentStatus.inferInterrupt).toHaveBeenCalledWith({
paneKey,
baselineUpdatedAt: 1_000,
baselineStateStartedAt: 900,
baselinePrompt: 'stop quickly',
baselineAgentType: 'codex',
intent: 'plain-escape'
})
expect(mockStoreState.dropAgentStatus).toHaveBeenCalledWith(paneKey)
})
)
it('drops the command-finished status when pending interrupt inference is rejected', async () => {
const { connectPanePty } = await import('./pty-connection')
+2 -4
View File
@@ -26,12 +26,10 @@ export function shouldIgnoreInterruptIntent(
return intent === 'ctrl-c' && (agentType === 'codex' || agentType === 'droid')
}
// Why: these TUIs also close an overlay on a bare Escape (Claude's /btw composer, OMP/Pi's
// focused-child and settings views). The keypress is ambiguous at the source and nothing outside
// the TUI can disambiguate it, so it is never evidence a turn ended — only the provider's own
// hook may retire the row (#13547, #9208). Ctrl+C is unaffected; it has no navigation meaning.
// Escape also closes views (including Codex search and /permissions); only provider evidence ends the turn.
const ESCAPE_ALSO_NAVIGATES_AGENT_TYPES: ReadonlySet<AgentType> = new Set([
'claude',
'codex',
'omp',
'pi',
'prime-agent'
+116 -98
View File
@@ -18,111 +18,129 @@ import {
waitForActiveTerminalManager
} from './helpers/terminal'
test('Codex Ctrl+C preserves working status until a confirmed interruption', async ({
orcaPage,
electronApp
}, testInfo) => {
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
await ensureTerminalVisible(orcaPage)
await waitForActiveTerminalManager(orcaPage, 30_000)
const endpoint = await readHookEndpoint(electronApp)
const descriptor = await waitForActivePaneHookDescriptor(orcaPage)
await orcaPage.evaluate(() => {
const state = window.__store?.getState()
state?.setAgentActivityDisplayMode('full')
if (state && !state.worktreeCardProperties.includes('inline-agents')) {
state.setWorktreeCardProperties([...state.worktreeCardProperties, 'inline-agents'])
}
})
const dir = mkdtempSync(join(tmpdir(), 'orca-codex-interruption-'))
const transcriptPath = join(dir, 'rollout-root.jsonl')
writeFileSync(
transcriptPath,
`${JSON.stringify({ type: 'event_msg', payload: { type: 'task_started', turn_id: 'turn-1' } })}\n`
)
try {
const inputScript = join(dir, 'codex-input.cjs')
// Replay navigation input through a real PTY; hook and rollout fixtures own agent status.
for (const navigation of ['Control+c', 'F3 search', '/permissions'] as const) {
test(`Codex ${navigation} preserves working status until a confirmed interruption`, async ({
orcaPage,
electronApp
}, testInfo) => {
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
await ensureTerminalVisible(orcaPage)
await waitForActiveTerminalManager(orcaPage, 30_000)
const endpoint = await readHookEndpoint(electronApp)
const descriptor = await waitForActivePaneHookDescriptor(orcaPage)
await orcaPage.evaluate(() => {
const state = window.__store?.getState()
state?.setAgentActivityDisplayMode('full')
if (state && !state.worktreeCardProperties.includes('inline-agents')) {
state.setWorktreeCardProperties([...state.worktreeCardProperties, 'inline-agents'])
}
})
const dir = mkdtempSync(join(tmpdir(), 'orca-codex-interruption-'))
const transcriptPath = join(dir, 'rollout-root.jsonl')
writeFileSync(
inputScript,
`
transcriptPath,
`${JSON.stringify({ type: 'event_msg', payload: { type: 'task_started', turn_id: 'turn-1' } })}\n`
)
try {
const inputScript = join(dir, 'codex-input.cjs')
writeFileSync(
inputScript,
`
process.stdin.setRawMode(true)
process.stdin.resume()
process.stdin.on('data', (chunk) => {
if (chunk.includes(3)) process.stdout.write('ORCA_CTRL_C_RECEIVED\\n')
if (chunk.toString() === '\\x1bOR' || chunk.toString() === '\\x1b[13~') {
process.stdout.write('ORCA_F3_SEARCH_OPENED\\n')
}
if (chunk.includes(13)) process.stdout.write('ORCA_PERMISSIONS_OPENED\\n')
if (chunk.toString() === '\\x1b') process.stdout.write('ORCA_ESCAPE_RECEIVED: view dismissed; main task continues\\n')
})
process.stdout.write('ORCA_CODEX_INPUT_READY\\n')
`
)
const terminalWindowsShell = await orcaPage.evaluate(
() => window.__store?.getState().settings.terminalWindowsShell
)
const shell = resolveStartupShell(
process.platform,
resolveLocalWindowsAgentStartupShell({
platform: process.platform,
isRemote: false,
terminalWindowsShell
)
const terminalWindowsShell = await orcaPage.evaluate(
() => window.__store?.getState().settings.terminalWindowsShell
)
const shell = resolveStartupShell(
process.platform,
resolveLocalWindowsAgentStartupShell({
platform: process.platform,
isRemote: false,
terminalWindowsShell
})
)
const ptyId = await waitForActivePanePtyId(orcaPage)
await execInTerminal(
orcaPage,
ptyId,
buildShellCommandFromArgv([process.execPath, inputScript], shell)
)
await waitForTerminalOutput(orcaPage, 'ORCA_CODEX_INPUT_READY')
await emitCodexHookStatus(endpoint, {
...descriptor,
transcriptPath,
sessionId: 'main-session',
state: 'working',
prompt: 'Main task continues'
})
)
const ptyId = await waitForActivePanePtyId(orcaPage)
await execInTerminal(
orcaPage,
ptyId,
buildShellCommandFromArgv([process.execPath, inputScript], shell)
)
await waitForTerminalOutput(orcaPage, 'ORCA_CODEX_INPUT_READY')
await emitCodexHookStatus(endpoint, {
...descriptor,
transcriptPath,
sessionId: 'main-session',
state: 'working',
prompt: 'Main task continues'
})
const working = orcaPage.locator('[aria-label="Working"]')
const interrupted = orcaPage.locator('[aria-label="Interrupted"]')
await expect(working.first()).toBeVisible()
await emitCodexHookStatus(endpoint, {
...descriptor,
state: 'working',
sessionId: 'side-session',
transcriptPath: null,
prompt: 'Side chat'
})
await expect(orcaPage.getByText('Main task continues', { exact: true })).toBeVisible()
await focusActiveTerminalInput(orcaPage)
await orcaPage.keyboard.press('Control+c')
await waitForTerminalOutput(orcaPage, 'ORCA_CTRL_C_RECEIVED')
// Allow the old 500 ms inference timer to fire before recording the rendered result.
await orcaPage.waitForTimeout(1_000)
await orcaPage.screenshot({
path: testInfo.outputPath('status-after-ctrl-c.png'),
clip: { x: 0, y: 180, width: 280, height: 240 }
})
await expect(interrupted).toHaveCount(0)
await expect(working.first()).toBeVisible()
const working = orcaPage.locator('[aria-label="Working"]')
const interrupted = orcaPage.locator('[aria-label="Interrupted"]')
await expect(working.first()).toBeVisible()
await emitCodexHookStatus(endpoint, {
...descriptor,
state: 'working',
sessionId: 'side-session',
transcriptPath: null,
prompt: 'Side chat'
})
await expect(orcaPage.getByText('Main task continues', { exact: true })).toBeVisible()
await focusActiveTerminalInput(orcaPage)
const key = navigation === 'Control+c' ? 'Control+c' : 'Escape'
if (navigation === 'F3 search') {
await orcaPage.keyboard.press('F3')
await waitForTerminalOutput(orcaPage, 'ORCA_F3_SEARCH_OPENED')
} else if (navigation === '/permissions') {
await orcaPage.keyboard.type('/permissions')
await orcaPage.keyboard.press('Enter')
await waitForTerminalOutput(orcaPage, 'ORCA_PERMISSIONS_OPENED')
}
await orcaPage.keyboard.press(key)
await waitForTerminalOutput(
orcaPage,
key === 'Escape' ? 'ORCA_ESCAPE_RECEIVED' : 'ORCA_CTRL_C_RECEIVED'
)
// Allow the old 500 ms inference timer to fire before recording the rendered result.
await orcaPage.waitForTimeout(1_000)
await orcaPage.screenshot({
path: testInfo.outputPath('status-after-input.png')
})
await expect(interrupted).toHaveCount(0)
await expect(working.first()).toBeVisible()
appendFileSync(
transcriptPath,
`${JSON.stringify({ type: 'event_msg', payload: { type: 'turn_aborted', turn_id: 'turn-1', reason: 'interrupted' } })}\n`
)
await expect(interrupted.first()).toBeVisible()
await expect(working).toHaveCount(0)
await orcaPage.screenshot({
path: testInfo.outputPath('status-after-confirmed-interruption.png'),
clip: { x: 0, y: 180, width: 280, height: 240 }
})
await emitCodexHookStatus(endpoint, {
...descriptor,
state: 'working',
prompt: 'Next main task'
})
await expect(working.first()).toBeVisible()
await expect(interrupted).toHaveCount(0)
await emitCodexHookStatus(endpoint, { ...descriptor, state: 'done' })
await expect(working).toHaveCount(0)
await expect(interrupted).toHaveCount(0)
} finally {
rmSync(dir, { recursive: true, force: true })
}
})
appendFileSync(
transcriptPath,
`${JSON.stringify({ type: 'event_msg', payload: { type: 'turn_aborted', turn_id: 'turn-1', reason: 'interrupted' } })}\n`
)
await expect(interrupted.first()).toBeVisible()
await expect(working).toHaveCount(0)
await orcaPage.screenshot({
path: testInfo.outputPath('status-after-confirmed-interruption.png')
})
await emitCodexHookStatus(endpoint, {
...descriptor,
state: 'working',
prompt: 'Next main task'
})
await expect(working.first()).toBeVisible()
await expect(interrupted).toHaveCount(0)
await emitCodexHookStatus(endpoint, { ...descriptor, state: 'done' })
await expect(working).toHaveCount(0)
await expect(interrupted).toHaveCount(0)
} finally {
rmSync(dir, { recursive: true, force: true })
}
})
}