Merge branch 'nwparker/agy-history-workspace' into nwparker/agy-history-folders

This commit is contained in:
Neil
2026-09-19 02:21:44 -07:00
6 changed files with 92 additions and 84 deletions
@@ -11,6 +11,7 @@ export type TranscriptPaneOptions = {
paneTitle: string
foregroundProcess: string | null
data: string
size?: { cols: number; rows: number }
/** Set for a pane whose PTY lives on an SSH host or WSL distro rather than locally. */
connectionId?: string
/** Simulates a PTY controller whose foreground probe never settles. */
@@ -71,6 +72,9 @@ export async function createTranscriptPane(
}
]
})
if (options.size) {
runtime.seedHeadlessTerminal(TRANSCRIPT_PANE_PTY_ID, '\x1b[0m', options.size)
}
// Why the guard: a restore seed is only applied to a never-written record, so the restore
// cases must not write an empty chunk first.
if (options.data.length > 0) {
@@ -1,17 +1,5 @@
/**
* Pins Antigravity readiness to captured transcripts instead of hand-written fixtures.
*
* Five detector attempts were tuned against a five-line screen someone typed from memory, and
* three of them shipped worse behaviour than the bug they replaced. Nothing here asserts what
* Antigravity prints: the transcripts do. Six are recorded from a live `agy`; the rest name
* themselves as skipped until someone can reach them.
*
* Four cases are pinned as KNOWN DEFECT: on real output the shipped detector refuses the ready
* screen and accepts the live model picker. Those assert what it does, not what it should.
*
* Capture protocol: docs/reference/agent-pty-transcript-capture.md
* What each transcript decides: docs/reference/antigravity-readiness-evidence.md
*/
// Replays captured agy screens through the real runtime wait at their recorded 120×40 grid.
// Capture protocol: docs/reference/agent-pty-transcript-capture.md.
import { existsSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it, vi } from 'vitest'
@@ -35,10 +23,9 @@ const EVIDENCE_DOC = join(
'reference',
'antigravity-readiness-evidence.md'
)
// Why asymmetric: a ready verdict has to survive the settle window, while a refusal only has to
// hold for one poll. Keeping the refusal short keeps seven transcripts off the suite's clock.
const READY_TIMEOUT_MS = 2_000
const REFUSAL_TIMEOUT_MS = 600
// Allow the 2s idle poll to observe the asynchronously rendered snapshot before timing out.
const READY_TIMEOUT_MS = 3_500
const REFUSAL_TIMEOUT_MS = READY_TIMEOUT_MS
/** Antigravity's binary, as Orca launches and probes it (`tui-agent-config.ts` detectCmd). */
const ANTIGRAVITY_COMMAND = 'agy'
// String.fromCharCode, not a literal: the formatter rewrites an escape sequence into a raw
@@ -53,28 +40,23 @@ type TranscriptCase = {
what: string
/** What a correct detector must answer. Not what the shipped one answers. */
expectReady: boolean
/**
* Set where the shipped detector contradicts the transcript. The case then runs inverted, so
* CI pins the defect instead of going permanently red — and flips to failing the moment
* someone fixes it, which is exactly when these expectations need re-reading.
*/
knownDefect?: string
beforeShutdown?: boolean
}
const TRANSCRIPTS: readonly TranscriptCase[] = [
{
name: 'antigravity-ready-api-key-gemini-model',
beforeShutdown: true,
capture: 'B',
what: 'ready screen, API-key identity — the account row reads "Gemini API key", not an email',
expectReady: true,
knownDefect: 'refused: the model row never starts a line, the logo shares it'
expectReady: true
},
{
name: 'antigravity-ready-account-info-hidden',
beforeShutdown: true,
capture: 'B',
what: 'ready screen with AGY_CLI_HIDE_ACCOUNT_INFO=1 — no account row at all',
expectReady: true,
knownDefect: 'refused: same line-start defect, and no account row exists to require'
expectReady: true
},
{
name: 'antigravity-dialog-trust-workspace',
@@ -86,8 +68,7 @@ const TRANSCRIPTS: readonly TranscriptCase[] = [
name: 'antigravity-dialog-model-picker',
capture: 'C',
what: 'model picker owning the screen',
expectReady: false,
knownDefect: "accepted: the picker's own `Gemini 3.x Flash` rows satisfy the model rule"
expectReady: false
},
{
name: 'antigravity-dialog-command-palette',
@@ -107,15 +88,14 @@ const TRANSCRIPTS: readonly TranscriptCase[] = [
name: 'antigravity-busy-turn-ended',
capture: 'E',
what: 'the turn has ended and the composer has returned, process still alive',
expectReady: true,
knownDefect: 'refused: the retained tail ends on the error block, with no composer row in it'
expectReady: true
},
{
name: 'antigravity-dialog-dismissed',
beforeShutdown: true,
capture: 'D',
what: 'the screen immediately after the model picker is dismissed',
expectReady: true,
knownDefect: 'refused: the banner is not reprinted and no model row starts a line'
expectReady: true
},
// Not captured: this machine's agy has no OAuth session and offers only Gemini models, and
// reaching the rest would mean signing the operator out or deleting their config. See
@@ -156,6 +136,19 @@ function fixturePath(name: string): string {
return join(FIXTURE_DIR, `${name}.txt`)
}
function liveCapture(transcript: TranscriptCase): string {
const raw = readFileSync(fixturePath(transcript.name), 'utf8')
if (!transcript.beforeShutdown) {
return raw
}
// These recordings include process shutdown, which erases the live shortcut footer.
const shutdown = `${ESC}[>4m${ESC}[=0;1u`
const offset = raw.lastIndexOf(shutdown)
expect(offset).toBeGreaterThan(0)
expect(raw.slice(offset)).toContain(`${ESC}[?2004l`)
return raw.slice(0, offset)
}
/**
* A `tui-idle` wait ends three ways, and only one of them is readiness: it resolves satisfied, it
* resolves unsatisfied with a blocked reason, or it rejects with `timeout` because nothing ever
@@ -171,6 +164,7 @@ async function readinessVerdict(
// capture carries the OSC bytes, so the pane wears whatever the CLI actually set.
paneTitle: extractLastOscTitle(transcript) ?? ANTIGRAVITY_COMMAND,
foregroundProcess: ANTIGRAVITY_COMMAND,
size: { cols: 120, rows: 40 },
data: transcript
})
try {
@@ -194,29 +188,19 @@ describe('Antigravity readiness, decided by captured transcripts', () => {
const captured = existsSync(path)
const label = `capture ${transcript.capture}: ${transcript.what}`
// A pinned defect asserts what the detector DOES, so CI is honest rather than permanently
// red; fixing the detector flips this case to failing, which is when these expectations
// need re-reading. The correct answer stays in `expectReady` and in the test's name.
const shipped =
transcript.knownDefect === undefined ? transcript.expectReady : !transcript.expectReady
const verdictName =
transcript.knownDefect === undefined
? `${label}${transcript.expectReady ? 'ready' : 'not ready'}`
: `${label} → must be ${transcript.expectReady ? 'ready' : 'not ready'}; KNOWN DEFECT, ${transcript.knownDefect}`
const verdictName = `${label}${transcript.expectReady ? 'ready' : 'not ready'}`
it.skipIf(!captured)(
verdictName,
async () => {
// A refusal only has to hold for one poll; a ready verdict has to survive the settle
// window. Keeping the refusal short keeps eleven transcripts off the suite's clock.
const verdict = await readinessVerdict(
readFileSync(path, 'utf8'),
liveCapture(transcript),
transcript.expectReady ? READY_TIMEOUT_MS : REFUSAL_TIMEOUT_MS
)
// A silent dialog carries no blocked-signal wording, so the assertion is only that Orca
// does not call the pane ready and type a prompt into a dialog that owns the screen.
expect({ ready: verdict.ready, outcome: verdict.outcome }).toMatchObject({
ready: shipped
ready: transcript.expectReady
})
},
READY_TIMEOUT_MS + 10_000
@@ -253,19 +237,10 @@ describe('Antigravity readiness, decided by captured transcripts', () => {
})
describe('scaffold self-check', () => {
// Why these two live here: when a transcript lands and fails, the failure has to mean the
// capture disagreed with the detector — not that the harness or the timeouts are broken.
// Neither case is evidence about Antigravity; both are shapes the current detector already
// decides, used only to prove the plumbing reaches a verdict.
// Verify both completion paths independently of the capture inventory.
it('reaches a ready verdict through the harness', async () => {
const verdict = await readinessVerdict(
[
'Antigravity CLI 1.0.3',
'user@example.com (Antigravity Business)',
'Gemini 3.5 Flash (High)',
'~/orca/workspaces/orca/agy-dispatch-issue',
'>'
].join('\n'),
readFileSync(fixturePath('antigravity-ready-default-127'), 'utf8'),
READY_TIMEOUT_MS
)
expect(verdict.ready).toBe(true)
@@ -22,6 +22,9 @@ async function capturedScreen(name: string): Promise<string[]> {
describe('Antigravity visible-screen readiness from real PTY captures', () => {
it.each([
['antigravity-ready-default-127', true],
['antigravity-ready-api-key-gemini-model', false],
['antigravity-ready-account-info-hidden', false],
['antigravity-dialog-dismissed', false],
['antigravity-ready-plan-127', true],
['antigravity-ready-accept-edits-127', true],
['antigravity-plan-hint-as-draft-127', false],
@@ -1,14 +1,19 @@
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it, vi } from 'vitest'
import { OrcaRuntimeService } from '../orca-runtime-test-mocks.spec'
import {
TEST_WORKTREE_ID,
TEST_WORKTREE_PATH,
antigravityPromptBeforeModelReadyScreen,
antigravityReadyScreen,
store,
syncSinglePty
} from '../orca-runtime-test-fixtures.spec'
const antigravityCapturedReady = readFileSync(
join(__dirname, '..', '__fixtures__', 'antigravity-ready-default-127.txt'),
'utf8'
)
describe('OrcaRuntimeService', () => {
it('waits for exit on background terminal handles', async () => {
const runtime = new OrcaRuntimeService(store)
@@ -532,10 +537,11 @@ describe('OrcaRuntimeService', () => {
getForegroundProcess: async () => null
})
const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`)
runtime.onPtyData('pty-bg', antigravityReadyScreen('Gemini 4 Experimental (High)'), Date.now())
runtime.seedHeadlessTerminal('pty-bg', '\x1b[0m', { cols: 120, rows: 40 })
runtime.onPtyData('pty-bg', antigravityCapturedReady, Date.now())
await expect(
runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 1_000 })
runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 3_500 })
).resolves.toMatchObject({
handle,
condition: 'tui-idle',
@@ -558,21 +564,11 @@ describe('OrcaRuntimeService', () => {
}
const splitSpy = vi.spyOn(String.prototype, 'split')
runtime.onPtyData(
'pty-bg',
[
'Antigravity CLI 1.0.3\n',
'user@example.com (Antigravity Business)\n',
pastedTail,
'Gemini 4 Experimental (High)\n',
'~/orca/workspaces/orca/agy-dispatch-issue\n',
'>'
].join(''),
Date.now()
)
runtime.seedHeadlessTerminal('pty-bg', '\x1b[0m', { cols: 120, rows: 40 })
runtime.onPtyData('pty-bg', [pastedTail, antigravityCapturedReady].join(''), Date.now())
await expect(
runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 1_000 })
runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 3_500 })
).resolves.toMatchObject({
handle,
condition: 'tui-idle',
@@ -581,12 +577,14 @@ describe('OrcaRuntimeService', () => {
})
const splitReadyTail = splitSpy.mock.contexts.some((context) => {
const value = typeof context === 'string' ? context : String(context)
return value.includes('antigravity cli') && value.includes('pasted text pasted text')
return (
value.toLowerCase().includes('antigravity cli') && value.includes('pasted text pasted text')
)
})
expect(splitReadyTail).toBe(false)
})
it('resolves tui-idle from an Antigravity prompt before the model line', async () => {
it('resolves tui-idle after retained trust text is replaced by a captured Antigravity screen', async () => {
const runtime = new OrcaRuntimeService(store)
runtime.setPtyController({
spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }),
@@ -595,18 +593,19 @@ describe('OrcaRuntimeService', () => {
getForegroundProcess: async () => null
})
const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`)
runtime.seedHeadlessTerminal('pty-bg', '\x1b[0m', { cols: 120, rows: 40 })
runtime.onPtyData(
'pty-bg',
[
'Do you trust this workspace directory?\n',
'Press t to trust\n',
antigravityPromptBeforeModelReadyScreen('Gemini 3.5 Flash (High)')
antigravityCapturedReady
].join(''),
Date.now()
)
await expect(
runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 1_000 })
runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 3_500 })
).resolves.toMatchObject({
handle,
condition: 'tui-idle',
@@ -639,11 +638,12 @@ describe('OrcaRuntimeService', () => {
}
]
})
runtime.onPtyData('pty-1', antigravityReadyScreen(), Date.now())
runtime.seedHeadlessTerminal('pty-1', '\x1b[0m', { cols: 120, rows: 40 })
runtime.onPtyData('pty-1', antigravityCapturedReady, Date.now())
const [terminal] = (await runtime.listTerminals()).terminals
await expect(
runtime.waitForTerminal(terminal.handle, { condition: 'tui-idle', timeoutMs: 1_000 })
runtime.waitForTerminal(terminal.handle, { condition: 'tui-idle', timeoutMs: 3_500 })
).resolves.toMatchObject({
handle: terminal.handle,
condition: 'tui-idle',
@@ -1,3 +1,5 @@
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it, vi } from 'vitest'
import {
AGENT_PROMPT_BRACKETED_PASTE_END,
@@ -11,12 +13,16 @@ import { OrcaRuntimeService } from '../orca-runtime'
import { acknowledgeAgentPromptSubmit } from '../orca-runtime-test-mocks.spec'
import {
TEST_WORKTREE_PATH,
antigravityReadyScreen,
cursorBusyScreen,
cursorReadyScreen,
store
} from '../orca-runtime-test-fixtures.spec'
const antigravityCapturedReady = readFileSync(
join(__dirname, '..', '__fixtures__', 'antigravity-ready-default-127.txt'),
'utf8'
)
describe('OrcaRuntimeService', () => {
it('resolves tui-idle from a Codex ready prompt even when stale startup lines remain', async () => {
const runtime = new OrcaRuntimeService(store)
@@ -64,19 +70,20 @@ describe('OrcaRuntimeService', () => {
getForegroundProcess: async () => null
})
const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`)
runtime.seedHeadlessTerminal('pty-bg', '\x1b[0m', { cols: 120, rows: 40 })
runtime.onPtyData(
'pty-bg',
[
'Do you trust this workspace directory?\n',
'Press t to trust\n',
antigravityReadyScreen(),
antigravityCapturedReady,
'\n'
].join(''),
Date.now()
)
await expect(
runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 1_000 })
runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 3_500 })
).resolves.toMatchObject({
handle,
condition: 'tui-idle',