mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
* fix(runtime): agent-neutral wait-blocked reasons and non-Gemini Antigravity readiness Reported by a user via the in-app help menu (report "not captured", 1.4.198). The trust/interactive/update/cwd prompt matchers are agent-agnostic - they match on dialog wording and never inspect the pane's agent - yet emitted hardcoded codex-* reasons. Those reached users verbatim in worker receipts (local-worker-start, federation), two automation surfaces, and raw CLI output, so an Antigravity user was told they had a Codex problem. findAntigravityReadyPromptIndex also required the model line to start with the literal "gemini". Antigravity CLI is not Gemini-only, so a non-Gemini session never registered as ready, stale trust text was never superseded, and the pane stayed blocked - which is why dispatch --inject answered agent_prompt_blocked. Add agent-neutral reasons additively (codex-* members kept on the wire per docs/reference/remote-wire-compatibility.md, with a legacy alias for older hosts) and decide Antigravity readiness structurally: header, then model/account rows, then the prompt caret. codex-model-migration-prompt and codex-hooks-review-prompt stay Codex-named - both key on Codex's own wording. * fix(runtime): finish the agent-neutral rename, revert the Antigravity readiness rewrite Review follow-up on this branch. Splits the two halves of the original commit: the reason rename lands, the Antigravity readiness detector goes back to merge-base until someone captures a real transcript. Rename half: - 'hooks need review' + 'press enter to confirm' inspects no agent, so it now publishes agent-hooks-review-prompt. That was the last agent-agnostic codex-* emission left, and it is the one the original report was about: a Claude Code user hitting a hooks dialog still read "codex-hooks-review-prompt". - The legacy alias is applied at all three surfaces that render a raw reason, not just the CLI. describeTerminalWaitBlockedReason() is the single formatter; the worker and federation "Agent startup blocked:" receipts use it too. Kept one-directional: nothing consumes agent-* -> codex-*, since an old client renders with its own shipped code. - Restores the compat note deleted at the permission-choices site. The Rule 1 citation is correct - remote-wire-compatibility.md names this enum by name. Antigravity half, reverted: findAntigravityReadyPromptIndex goes back to merge-base (header + a 'gemini' model line + a lone '>' caret) and antigravity-ready-prompt-index.ts is removed. Executing both builds against constructed tails, the rewrite read a live startup dialog as ready. Adding the account row from this repo's own ready-screen fixture to five silent startup dialogs (sign-in, model picker, theme picker, privacy notice, update banner) flipped all five from unready to ready; so did any narration line containing an email address, with no account row at all. Readiness is what gates typing the task prompt into the pane, so that path types a task prompt into a live authentication dialog. Merge-base returns unready for all ten. The rewrite also did not reliably fix the wedge it targeted: with no account row and a non-Gemini model - a personal or API-key user - it still returns unready. No real Antigravity transcript exists in this repo. The cursor-agent rules are derived from captures under src/main/runtime/__fixtures__; Antigravity has no equivalent, and every attempt so far has been tuned against a hand-written 5-line fixture. A false negative (the agent waits) is safer than a false positive (we type into an auth dialog), so this ships the known behaviour. Reverting restores a pre-existing gap, not a regression: a non-Gemini Antigravity session wedges on merge-base too. Closing it needs a captured ready screen and a captured dismissed-dialog screen, for a personal/API-key account as well as a Business one. Tests: - Ten ratchet fixtures pin the shapes any replacement detector must refuse - the five silent dialogs with an account row, and each with a narrated email. All ten fail against the reverted rewrite. - Vacuous tests rewritten so they fail without the code they cover: the CLI alias tests asserted only the absence of a suffix, and the worker receipt test asserted the raw token. Tests that are characterization rather than a guard now say so on the line above. --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> Co-authored-by: Neil <neil@stably.ai>
255 lines
7.9 KiB
TypeScript
255 lines
7.9 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import type {
|
|
RuntimeTerminalShow,
|
|
RuntimeTerminalWait,
|
|
RuntimeTerminalWaitBlockedReason
|
|
} from '../shared/runtime-terminal-contracts'
|
|
import {
|
|
formatTerminalClose,
|
|
formatTerminalFocus,
|
|
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('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>')
|
|
})
|
|
})
|
|
|
|
// Why: an older host still publishes the codex-* tokens for dialogs its matcher never proved were
|
|
// Codex's, so a Gemini/Cursor/Antigravity user reads a Codex label unless the CLI names the neutral one.
|
|
describe('blocked-reason rendering against a mixed-version host', () => {
|
|
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 }
|
|
}
|
|
}
|
|
}
|
|
|
|
function waitResult(blockedReason: RuntimeTerminalWaitBlockedReason): {
|
|
wait: RuntimeTerminalWait
|
|
} {
|
|
return {
|
|
wait: {
|
|
handle: 'term_agy',
|
|
condition: 'tui-idle',
|
|
satisfied: false,
|
|
status: 'running',
|
|
exitCode: null,
|
|
blockedReason
|
|
}
|
|
}
|
|
}
|
|
|
|
// Why one assertion over every reason: a test that only asserts the *absence* of an alias suffix
|
|
// passes when the aliasing code is deleted, so each case is paired with a legacy token that must
|
|
// gain one.
|
|
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)'],
|
|
// This build published these itself, so there is nothing to reinterpret.
|
|
['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)'
|
|
)
|
|
})
|
|
})
|