fix: align structured TUI return readiness

This commit is contained in:
Brennan Benson
2026-08-11 06:45:21 -07:00
parent 9d8857c20a
commit 34635aed0f
4 changed files with 100 additions and 19 deletions
@@ -34,18 +34,32 @@ function notifier(revealTerminalSession: ReturnType<typeof vi.fn>) {
describe('structured TUI launch tab binding', () => {
it('proves the published launch tab before returning its revealed renderer binding', async () => {
let explicitStatus: {
state: 'working' | 'done'
prompt: string
receivedAt: number
stateStartedAt: number
paneKey: string
terminalHandle: string
} | null = null
const revealTerminalSession = vi.fn(
(_worktreeId: string, _options: { tabId?: string; leafId?: string; ptyId?: string }) =>
Promise.resolve({ tabId: 'tab-renderer' })
)
const runtime = new OrcaRuntimeService({
getSettings: () => ({
disabledTuiAgents: [],
agentCmdOverrides: {},
agentDefaultArgs: {},
agentDefaultEnv: {}
})
} as never)
const runtime = new OrcaRuntimeService(
{
getSettings: () => ({
disabledTuiAgents: [],
agentCmdOverrides: {},
agentDefaultArgs: {},
agentDefaultEnv: {}
})
} as never,
undefined,
{
getAgentStatusSnapshot: () => (explicitStatus ? [explicitStatus as never] : [])
}
)
runtime.setNotifier(notifier(revealTerminalSession) as never)
runtime.setPtyController({
spawn: vi.fn().mockResolvedValue({ id: 'pty-structured', pid: 4242 }),
@@ -134,6 +148,21 @@ describe('structured TUI launch tab binding', () => {
revealTerminalSession.mock.invocationCallOrder[0]!
)
explicitStatus = {
state: 'working',
prompt: '',
receivedAt: Date.now(),
stateStartedAt: Date.now(),
paneKey: owner.terminal.paneKey,
terminalHandle: owner.terminal.handle
}
expect(transport.tuiStatus(owner)).toBe('busy')
await expect(transport.waitForTuiIdle(owner, new AbortController().signal)).resolves.toBe(false)
explicitStatus = { ...explicitStatus, state: 'done', receivedAt: Date.now() }
expect(transport.tuiStatus(owner)).toBe('idle')
await expect(transport.waitForTuiIdle(owner, new AbortController().signal)).resolves.toBe(true)
const pty = (
runtime as unknown as {
ptysById: Map<string, { launchToken: string | null }>
+12 -3
View File
@@ -9172,13 +9172,18 @@ export class OrcaRuntimeService {
})
return owner.transcriptPath ? { transcriptPath: owner.transcriptPath } : {}
},
waitForTuiIdle: async (owner, signal) =>
(
waitForTuiIdle: async (owner, signal) => {
const explicit = this.getFreshExplicitAgentStatusForHandle(owner.terminal.handle)
if (explicit) {
return explicit.status === 'idle'
}
return (
await this.waitForTerminal(owner.terminal.handle, {
condition: 'tui-idle',
signal
})
).satisfied,
).satisfied
},
reproveTuiOwner: async ({ record, owner }) => {
const persisted = record.lease.ownerProcess
if (
@@ -9288,6 +9293,10 @@ export class OrcaRuntimeService {
}
private structuredTuiStatus(handle: string): 'idle' | 'busy' {
const explicit = this.getFreshExplicitAgentStatusForHandle(handle)
if (explicit) {
return explicit.status === 'idle' ? 'idle' : 'busy'
}
const pty = this.getLivePtyForHandle(handle)
if (pty) {
const text = buildTerminalWaitText(
@@ -44,11 +44,36 @@ describe('structured TUI exit proof', () => {
waitForExit: async () => {
throw new Error('terminal_handle_stale')
},
probe: async () => proof
probe: async () => proof,
staleHandleProbeAttempts: 1
})
).rejects.toThrow('terminal_handle_stale')
})
it('retries the persisted process identity when handle retirement wins the exit race', async () => {
const probe = vi
.fn()
.mockResolvedValueOnce({
outcome: 'identity-matched' as const,
matchedOn: ['process-start-time' as const]
})
.mockResolvedValueOnce({ outcome: 'pid-absent' as const })
await expect(
waitForStructuredTuiExitProof({
identity,
waitForExit: async () => {
throw new Error('terminal_handle_stale')
},
probe,
staleHandleProbeAttempts: 2,
staleHandleProbeIntervalMs: 0
})
).resolves.toBeUndefined()
expect(probe).toHaveBeenNthCalledWith(1, identity)
expect(probe).toHaveBeenNthCalledWith(2, identity)
})
it('preserves unrelated terminal wait failures', async () => {
const probe = vi.fn()
+25 -7
View File
@@ -6,6 +6,30 @@ type ExitProofInput = {
identity: AgentSessionProcessIdentity
waitForExit: () => Promise<unknown>
probe?: (identity: AgentSessionProcessIdentity) => Promise<AgentSessionOwnerProbe>
staleHandleProbeAttempts?: number
staleHandleProbeIntervalMs?: number
}
const DEFAULT_STALE_HANDLE_PROBE_ATTEMPTS = 50
const DEFAULT_STALE_HANDLE_PROBE_INTERVAL_MS = 100
function provesRecordedProcessExited(proof: AgentSessionOwnerProbe): boolean {
return proof.outcome === 'pid-absent' || proof.outcome === 'identity-mismatch'
}
async function waitForRecordedProcessExit(input: ExitProofInput, staleError: Error): Promise<void> {
const probe = input.probe ?? ((identity) => probeAgentSessionProcessIdentity({ identity }))
const attempts = input.staleHandleProbeAttempts ?? DEFAULT_STALE_HANDLE_PROBE_ATTEMPTS
const intervalMs = input.staleHandleProbeIntervalMs ?? DEFAULT_STALE_HANDLE_PROBE_INTERVAL_MS
for (let attempt = 0; attempt < attempts; attempt += 1) {
if (provesRecordedProcessExited(await probe(input.identity))) {
return
}
if (attempt + 1 < attempts) {
await new Promise((resolve) => setTimeout(resolve, intervalMs))
}
}
throw staleError
}
export async function waitForStructuredTuiExitProof(input: ExitProofInput): Promise<void> {
@@ -15,12 +39,6 @@ export async function waitForStructuredTuiExitProof(input: ExitProofInput): Prom
if (!(error instanceof Error) || error.message !== 'terminal_handle_stale') {
throw error
}
const proof = await (
input.probe ?? ((identity) => probeAgentSessionProcessIdentity({ identity }))
)(input.identity)
if (proof.outcome === 'pid-absent' || proof.outcome === 'identity-mismatch') {
return
}
throw error
await waitForRecordedProcessExit(input, error)
}
}