mirror of
https://github.com/stablyai/orca.git
synced 2026-11-04 11:07:42 +00:00
296 lines
9.1 KiB
TypeScript
296 lines
9.1 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
||
import type {
|
||
RuntimeTerminalShow,
|
||
RuntimeTerminalWait,
|
||
RuntimeTerminalWaitBlockedReason
|
||
} from '../shared/runtime-terminal-contracts'
|
||
import {
|
||
formatTerminalClose,
|
||
formatTerminalFocus,
|
||
formatTerminalRead,
|
||
formatTerminalSend,
|
||
formatTerminalShow,
|
||
formatTerminalWait
|
||
} from './terminal-format'
|
||
|
||
describe('formatTerminalFocus', () => {
|
||
it('distinguishes superseded navigation from a winning focus', () => {
|
||
expect(
|
||
formatTerminalFocus({
|
||
focus: {
|
||
handle: 'term_stale',
|
||
tabId: 'tab-stale',
|
||
worktreeId: 'worktree-1',
|
||
navigated: false
|
||
}
|
||
})
|
||
).toBe(
|
||
'Focus request for terminal term_stale was superseded or host navigation was skipped (tab tab-stale).'
|
||
)
|
||
expect(
|
||
formatTerminalFocus({
|
||
focus: { handle: 'term_winner', tabId: 'tab-winner', worktreeId: 'worktree-1' }
|
||
})
|
||
).toBe('Focused terminal term_winner (tab tab-winner).')
|
||
})
|
||
})
|
||
|
||
describe('formatTerminalClose', () => {
|
||
it('prints "PTY killed." only for a confirmed kill', () => {
|
||
expect(
|
||
formatTerminalClose({ close: { handle: 'term_local', tabId: 'tab-1', ptyKilled: true } })
|
||
).toBe('Closed terminal term_local. PTY killed.')
|
||
})
|
||
|
||
it('says the remote process was not confirmed stopped instead of claiming a kill', () => {
|
||
expect(
|
||
formatTerminalClose({
|
||
close: {
|
||
handle: 'term_remote',
|
||
tabId: 'tab-1',
|
||
ptyKilled: false,
|
||
ptyStopVerdict: 'unverifiable',
|
||
ptyStopReason: 'its SSH provider is no longer registered'
|
||
}
|
||
})
|
||
).toBe(
|
||
'Closed terminal term_remote. The PTY was not confirmed stopped: its SSH provider is no longer registered.'
|
||
)
|
||
})
|
||
|
||
it('promises the reconnect replay only when the host recorded the kill for it', () => {
|
||
const close = {
|
||
handle: 'term_remote',
|
||
tabId: 'tab-1',
|
||
ptyKilled: false,
|
||
ptyStopVerdict: 'unverifiable' as const,
|
||
ptyStopReason: 'SSH connection lost, reconnecting...'
|
||
}
|
||
expect(formatTerminalClose({ close: { ...close, pendingKillRecorded: true } })).toBe(
|
||
'Closed terminal term_remote. The PTY was not confirmed stopped: SSH connection lost, reconnecting... The kill retries when the host reconnects.'
|
||
)
|
||
expect(formatTerminalClose({ close })).toBe(
|
||
'Closed terminal term_remote. The PTY was not confirmed stopped: SSH connection lost, reconnecting...'
|
||
)
|
||
})
|
||
|
||
it('names a PTY known to be live', () => {
|
||
expect(
|
||
formatTerminalClose({
|
||
close: {
|
||
handle: 'term_live',
|
||
tabId: 'tab-1',
|
||
ptyKilled: false,
|
||
ptyStopVerdict: 'live'
|
||
}
|
||
})
|
||
).toBe('Closed terminal term_live. The PTY is live.')
|
||
})
|
||
})
|
||
|
||
describe('formatTerminalSend', () => {
|
||
it('exposes the provider and healthy delivery observation', () => {
|
||
expect(
|
||
formatTerminalSend({
|
||
send: {
|
||
handle: 'term_worker',
|
||
accepted: true,
|
||
bytesWritten: 8,
|
||
prompt: {
|
||
requestId: 'prompt-healthy',
|
||
stages: ['input_accepted', 'turn_started'],
|
||
provider: 'codex',
|
||
observation: 'supported',
|
||
processIncarnation: 'inc-1',
|
||
generation: 1,
|
||
baselineWorkingSequence: 0
|
||
}
|
||
}
|
||
})
|
||
).toBe(
|
||
[
|
||
'Prompt prompt-healthy on term_worker: input_accepted -> turn_started.',
|
||
'provider: codex',
|
||
'delivery observation: supported'
|
||
].join('\n')
|
||
)
|
||
})
|
||
|
||
it.each([
|
||
{
|
||
observation: 'permission' as const,
|
||
warning: 'Resolve the permission prompt in the terminal',
|
||
nextStep: '--retry-request prompt-unhealthy'
|
||
},
|
||
{
|
||
observation: 'incarnation_replaced' as const,
|
||
warning: 'the terminal process was replaced',
|
||
nextStep: 'Inspect the current terminal before sending a new prompt'
|
||
}
|
||
])('warns and gives a next step for $observation', ({ observation, warning, nextStep }) => {
|
||
const output = formatTerminalSend({
|
||
send: {
|
||
handle: 'term_worker',
|
||
accepted: true,
|
||
bytesWritten: 8,
|
||
prompt: {
|
||
requestId: 'prompt-unhealthy',
|
||
stages: ['input_accepted'],
|
||
provider: 'codex',
|
||
observation,
|
||
processIncarnation: 'inc-1',
|
||
generation: 1,
|
||
baselineWorkingSequence: 0
|
||
}
|
||
}
|
||
})
|
||
|
||
expect(output).toContain(`provider: codex`)
|
||
expect(output).toContain(`delivery observation: ${observation}`)
|
||
expect(output).toContain(`warning: delivery was not observed`)
|
||
expect(output).toContain(warning)
|
||
expect(output).toContain(nextStep)
|
||
})
|
||
|
||
it.each([
|
||
{ provider: 'claude' as const, expected: 'no turn start was observed' },
|
||
{ provider: 'unsupported' as const, expected: 'this provider cannot report delivery' },
|
||
{ provider: 'old-host' as const, expected: 'predates durable prompt receipts' }
|
||
])('warns per provider when delivery was not observed ($provider)', ({ provider, expected }) => {
|
||
const output = formatTerminalSend({
|
||
send: {
|
||
handle: 'term_worker',
|
||
accepted: true,
|
||
bytesWritten: 8,
|
||
prompt: {
|
||
requestId: 'prompt-unobserved',
|
||
stages: ['input_accepted'],
|
||
provider,
|
||
observation: 'unsupported',
|
||
processIncarnation: 'inc-1',
|
||
generation: 1,
|
||
baselineWorkingSequence: 0
|
||
}
|
||
}
|
||
})
|
||
|
||
expect(output).toContain(expected)
|
||
})
|
||
|
||
it('names the next command when a supported send never reached turn_started', () => {
|
||
const output = formatTerminalSend({
|
||
send: {
|
||
handle: 'term_worker',
|
||
accepted: true,
|
||
bytesWritten: 8,
|
||
prompt: {
|
||
requestId: 'prompt-swallowed',
|
||
stages: ['input_accepted'],
|
||
provider: 'claude',
|
||
observation: 'supported',
|
||
processIncarnation: 'inc-1',
|
||
generation: 1,
|
||
baselineWorkingSequence: 0
|
||
}
|
||
}
|
||
})
|
||
|
||
expect(output).toContain('no turn start was observed')
|
||
expect(output).toContain('--retry-request prompt-swallowed --wait-submit <seconds>')
|
||
})
|
||
})
|
||
|
||
function showResult(reason?: RuntimeTerminalWaitBlockedReason): {
|
||
terminal: RuntimeTerminalShow
|
||
} {
|
||
return {
|
||
terminal: {
|
||
handle: 'term_agy',
|
||
ptyId: 'pty-1',
|
||
paneRuntimeId: 1,
|
||
rendererGraphEpoch: 1,
|
||
worktreeId: 'worktree-1',
|
||
worktreePath: '/tmp/w',
|
||
branch: 'main',
|
||
tabId: 'tab-1',
|
||
leafId: 'leaf-1',
|
||
title: 'Antigravity',
|
||
connected: true,
|
||
writable: true,
|
||
lastOutputAt: null,
|
||
preview: 'Do you trust the files in this folder?',
|
||
agentWait: { source: 'prompt-text', reason }
|
||
}
|
||
}
|
||
}
|
||
|
||
describe('formatTerminalShow orphaned marker', () => {
|
||
it('prints orphaned only for a terminal with no pane binding', () => {
|
||
expect(
|
||
formatTerminalShow({ terminal: { ...showResult().terminal, orphaned: true } })
|
||
).toContain('orphaned: true\nconnected: true')
|
||
expect(formatTerminalShow(showResult())).not.toContain('orphaned')
|
||
})
|
||
})
|
||
|
||
// Older hosts can mislabel other providers' dialogs with codex-* tokens; include the neutral names.
|
||
describe('blocked-reason rendering against a mixed-version host', () => {
|
||
function waitResult(blockedReason: RuntimeTerminalWaitBlockedReason): {
|
||
wait: RuntimeTerminalWait
|
||
} {
|
||
return {
|
||
wait: {
|
||
handle: 'term_agy',
|
||
condition: 'tui-idle',
|
||
satisfied: false,
|
||
status: 'running',
|
||
exitCode: null,
|
||
blockedReason
|
||
}
|
||
}
|
||
}
|
||
|
||
// Pair alias additions with unchanged tokens so missing alias logic cannot pass on absence alone.
|
||
it.each([
|
||
['codex-trust-workspace', 'codex-trust-workspace (agent-trust-workspace)'],
|
||
['codex-update-prompt', 'codex-update-prompt (agent-update-prompt)'],
|
||
['codex-cwd-prompt', 'codex-cwd-prompt (agent-cwd-prompt)'],
|
||
['codex-hooks-review-prompt', 'codex-hooks-review-prompt (agent-hooks-review-prompt)'],
|
||
['codex-interactive-prompt', 'codex-interactive-prompt (agent-interactive-prompt)'],
|
||
['agent-trust-workspace', 'agent-trust-workspace'],
|
||
['codex-model-migration-prompt', 'codex-model-migration-prompt']
|
||
] as const)('renders %s as %s on both wait and show', (reason, rendered) => {
|
||
expect(formatTerminalWait(waitResult(reason)).split('\n').at(-1)).toBe(
|
||
`blockedReason: ${rendered}`
|
||
)
|
||
expect(formatTerminalShow(showResult(reason))).toContain(
|
||
`agentWait: ${rendered} (via prompt-text)`
|
||
)
|
||
})
|
||
|
||
it('still describes a wait with no reason at all', () => {
|
||
expect(formatTerminalShow(showResult(undefined))).toContain(
|
||
'agentWait: interactive prompt (via prompt-text)'
|
||
)
|
||
})
|
||
})
|
||
|
||
describe('formatTerminalRead draft separation', () => {
|
||
it('labels composer drafts outside terminal output', () => {
|
||
const output = formatTerminalRead({
|
||
terminal: {
|
||
handle: 'term_1',
|
||
status: 'running',
|
||
tail: ['Build passed', '❯'],
|
||
truncated: false,
|
||
nextCursor: null,
|
||
source: 'screen',
|
||
draft: 'proceed with the release'
|
||
}
|
||
})
|
||
|
||
expect(output).toContain('draft: "proceed with the release"')
|
||
expect(output).toContain('\n\nBuild passed\n❯')
|
||
})
|
||
})
|