mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 00:03:47 +00:00
fix: align structured TUI return readiness
This commit is contained in:
@@ -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 }>
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user