Files
orca/tests/e2e/terminal-opencode-altscreen-reveal-artifacts.spec.ts
T
JinjingandOrcaWin a40183389b feat: bound direct SSH reconnect fan-out and recovery (#11003)
* docs: design for direct SSH reconnect fan-out

Capture the implementation-ready plan for host-qualified, epoch-fenced
SSH reconnect recovery after two rounds of multi-model LLM counsel review.

* docs: reconcile SSH reconnect fan-out design

* docs: close reconnect design consistency gaps

* feat: implement bounded direct SSH reconnect recovery

* fix: bound direct SSH retry settlement

* fix: harden direct SSH reconnect authority

* fix: preserve split SSH retry ownership

* fix: preserve SSH split continuation authority

* docs: record final SSH reconnect validation

* fix: preserve SSH authority through retained and detached state

* fix: retain SSH authority across delayed split mounts

* fix: close SSH authority recovery gaps

* fix: fence stale SSH transport replacement

* fix: serialize SSH target teardown

* fix: settle SSH teardown failures before reconnect

* fix: retire failed SSH reset sessions

* test: reconcile current main E2E contracts

* fix: close direct SSH reconnect review gaps

* fix: fence stale SSH reconnect side effects

* fix: close final SSH reconnect lifecycle gaps

* test: stabilize current-main reliability gates

* test: prove plugin navigation containment

* test: make plugin navigation oracle authoritative

* test: make plugin navigation oracle deterministic

* ci: allow sharded e2e suite to finish

* test: wait for runtime pane publication

* test: classify pane readiness by error code

* test: select close persistence terminal by tab identity

* docs: mark reconnect implementation validated

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-28 12:33:17 -07:00

822 lines
31 KiB
TypeScript

/**
* STA-2694 repro attempt: switching away from a workspace running an alt-screen
* AI TUI (OpenCode/OpenTUI, also Claude Code and grok) and returning later shows
* a garbled/distorted terminal that only a manual window resize repairs.
*
* Why this spec exists next to terminal-inline-tui-reveal-convergence.spec.ts:
* that one drives the INLINE shape (normal buffer, live block glued to the
* bottom, history scrolling into scrollback). OpenCode runs FULL-SCREEN on the
* alternate buffer and repaints absolutely-positioned rows — nothing scrolls, so
* no row ever self-heals through the scroll path. These tests cover that shape
* across the hide/reveal boundaries (worktree switch, cold park, idle agent,
* desktop hide) and guard the convergence properties we CAN observe: the buffer
* converges to the live frame, the pane stays on the alt screen, and xterm's
* grid, the fit proposal, and the PTY-applied size all agree without a resize.
*
* ⚠ These tests do NOT observe the CANVAS. Two pixel oracles were tried here and
* both were proven blind by injecting the exact defect (freeze
* RenderService.refreshRows, then write new content, so the buffer advances
* while the canvas cannot):
*
* 1. Canvas-vs-buffer ink sampling (the render-desync sentinel's method).
* `drawImage` on a non-preserveDrawingBuffer WebGL canvas hands back a
* re-rendered copy, so it reported 0 missing cells against 5263 cells of
* text the canvas had never drawn.
* 2. Screenshot comparison against a forced repaint. Playwright's screenshot
* drives a fresh compositor frame, which HEALS the stale paint before it
* is captured; and the "repair" calls the same repaint code the reveal
* already ran, so a defect shared by both shots cancels out.
*
* Pixels are the wrong layer for this: anything that reads them can trigger the
* repaint that hides the bug. terminal-reveal-draw-command-probe.spec.ts solves
* that by counting the WebGL draw commands a repaint issues, which cannot be
* healed after the fact — use it for paint questions and this spec for buffer,
* geometry, and PTY-size convergence.
*/
import { readFileSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import { PNG } from 'pngjs'
import type { Page, TestInfo } from '@stablyai/playwright-test'
import { test, expect } from './helpers/orca-app'
import { compareTerminalScreenshots } from './terminal-screenshot-diff'
import {
ensureTerminalVisible,
getActiveTabId,
switchToOtherWorktree,
switchToWorktree,
waitForActiveWorktree,
waitForSessionReady
} from './helpers/store'
import {
sendToTerminal,
waitForActiveTerminalManager,
waitForPaneIdentitySnapshot
} from './helpers/terminal'
import { waitForTabParked } from './helpers/terminal-hidden-parking'
const PARKING_DELAY_MS = Number(process.env.ORCA_E2E_TERMINAL_PARKING_DELAY_MS) || 500
test.use({
orcaAppExtraEnv: { ORCA_E2E_TERMINAL_PARKING_DELAY_MS: String(PARKING_DELAY_MS) }
})
const FIXTURE_PATH = path.join(__dirname, 'fixtures', 'opencode-altscreen-live-fixture.cjs')
const FRAME_RE = /OPENCODE_FRAME_(\d+)/g
const TAIL_RE = /OPENCODE_TAIL_(\d+)/g
// The fixture ticks ~16x/s; allow generous parse/delivery lag while still
// rejecting a frame frozen from before the hide.
const MAX_VISIBLE_FRAME_LAG = 60
type RevealProbe = {
ptyId: string | null
cols: number
rows: number
bufferType: string
proposed: { cols: number; rows: number } | null
appliedPtySize: { cols: number; rows: number } | null
screenRows: string[]
}
function latestMatch(text: string, pattern: RegExp): number {
let latest = -1
for (const match of text.matchAll(pattern)) {
latest = Math.max(latest, Number(match[1]))
}
return latest
}
function heartbeatFrame(heartbeatPath: string): number {
try {
return Number(readFileSync(heartbeatPath, 'utf8').trim())
} catch {
return -1
}
}
async function probeRevealedPane(page: Page, tabId: string): Promise<RevealProbe | null> {
return page.evaluate(
async ({ tabId }) => {
const manager = window.__paneManagers?.get(tabId)
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
if (!pane) {
return null
}
const ptyId = pane.container.dataset.ptyId ?? null
const terminal = pane.terminal
const buffer = terminal.buffer.active
const screenRows: string[] = []
for (let i = 0; i < terminal.rows; i += 1) {
const line = buffer.getLine(buffer.viewportY + i)
screenRows.push(line ? line.translateToString(true) : '')
}
let proposed: { cols: number; rows: number } | null = null
try {
proposed = pane.fitAddon.proposeDimensions() ?? null
} catch {
proposed = null
}
let appliedPtySize: { cols: number; rows: number } | null = null
try {
appliedPtySize = ptyId ? ((await window.api.pty.getSize(ptyId)) ?? null) : null
} catch {
appliedPtySize = null
}
return {
ptyId,
cols: terminal.cols,
rows: terminal.rows,
bufferType: buffer.type,
proposed,
appliedPtySize,
screenRows
}
},
{ tabId }
)
}
function describeProbe(probe: RevealProbe | null): string {
if (!probe) {
return 'pane not mounted'
}
return JSON.stringify(
{
cols: probe.cols,
rows: probe.rows,
bufferType: probe.bufferType,
proposed: probe.proposed,
appliedPtySize: probe.appliedPtySize,
screenHead: probe.screenRows.slice(0, 4),
screenTail: probe.screenRows.slice(-4)
},
null,
1
)
}
async function forceWebglOnActiveTab(page: Page): Promise<void> {
await page.evaluate(() => {
const state = window.__store?.getState()
if (!state?.settings) {
throw new Error('Store unavailable')
}
window.__store?.setState({
settings: { ...state.settings, terminalGpuAcceleration: 'on' }
})
const worktreeId = state.activeWorktreeId
const tabId =
state.activeTabType === 'terminal'
? state.activeTabId
: worktreeId
? (state.activeTabIdByWorktree?.[worktreeId] ?? null)
: null
window.__paneManagers?.get(tabId ?? '')?.setTerminalGpuAcceleration?.('on')
})
}
async function hasWebglPane(page: Page, tabId: string): Promise<boolean> {
return page.evaluate(
(tabId) =>
(window.__paneManagers?.get(tabId)?.getRenderingDiagnostics?.() ?? []).some(
(diagnostic) => diagnostic.hasWebgl
),
tabId
)
}
async function paneClipRect(
page: Page,
tabId: string
): Promise<{ x: number; y: number; width: number; height: number } | null> {
return page.evaluate((tabId) => {
const manager = window.__paneManagers?.get(tabId)
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
if (!pane) {
return null
}
const rect = pane.container.getBoundingClientRect()
if (rect.width < 10 || rect.height < 10) {
return null
}
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height }
}, tabId)
}
async function activateTerminalTab(page: Page, tabId: string): Promise<void> {
await page.evaluate((targetTabId) => {
const store = window.__store
if (!store) {
throw new Error('activateTerminalTab: window.__store is unavailable')
}
const state = store.getState()
state.setActiveTabType('terminal')
state.setActiveTab(targetTabId)
}, tabId)
await expect
.poll(() => getActiveTabId(page), {
timeout: 5_000,
message: `terminal tab ${tabId} did not become active`
})
.toBe(tabId)
}
async function createActiveTerminalTab(page: Page, worktreeId: string): Promise<string> {
const tabId = await page.evaluate((worktreeId) => {
const store = window.__store
if (!store) {
throw new Error('createActiveTerminalTab: window.__store is unavailable')
}
const state = store.getState()
const tab = state.createTab(worktreeId, undefined, undefined, { activate: true })
state.setActiveTab(tab.id)
state.setActiveTabType('terminal')
return tab.id
}, worktreeId)
await expect
.poll(() => getActiveTabId(page), {
timeout: 5_000,
message: 'newly created terminal tab did not become active'
})
.toBe(tabId)
await waitForActiveTerminalManager(page, 30_000)
await waitForPaneIdentitySnapshot(page, 1)
return tabId
}
async function withCpuThrottle<T>(page: Page, rate: number, run: () => Promise<T>): Promise<T> {
const session = await page.context().newCDPSession(page)
try {
await session.send('Emulation.setCPUThrottlingRate', { rate })
return await run()
} finally {
await session.send('Emulation.setCPUThrottlingRate', { rate: 1 }).catch(() => {})
await session.detach().catch(() => {})
}
}
type StreamingTabSetup = {
worktreeId: string
tabId: string
ptyId: string
heartbeatPath: string
stop: () => Promise<void>
}
// Why agent-marked: a real OpenCode tab carries launchAgent/telemetry, which
// flips the reveal into the live-agent reattach branches (mode-preserving
// resets, hidden startup query grammar, post-replay focus-in) — the branches
// the field bug lives behind.
async function startStreamingAltScreenTui(
page: Page,
testInfo: TestInfo,
options: { ticksPerSecond?: number } = {}
): Promise<StreamingTabSetup> {
await waitForSessionReady(page)
const worktreeId = await waitForActiveWorktree(page)
await ensureTerminalVisible(page)
const heartbeatPath = testInfo.outputPath(`opencode-altscreen-heartbeat-${Date.now()}.txt`)
const command = `node ${JSON.stringify(FIXTURE_PATH)} ${JSON.stringify(heartbeatPath)} ${options.ticksPerSecond ?? 16}`
const tabId = await page.evaluate(
({ worktreeId, command }) => {
const store = window.__store
if (!store) {
throw new Error('startStreamingAltScreenTui: window.__store is unavailable')
}
const state = store.getState()
// Why 'codex' and not 'opencode': the agent marker exists only to take the
// live-agent reattach branches, and codex is the kind the e2e environment
// reliably launches (same choice as the inline-TUI convergence spec). The
// TUI *shape* under test comes from the fixture, not from this marker.
const tab = state.createTab(worktreeId, undefined, undefined, { launchAgent: 'codex' })
state.queueTabStartupCommand(tab.id, {
command,
launchAgent: 'codex',
telemetry: {
agent_kind: 'codex',
launch_source: 'tab_bar_quick_launch',
request_kind: 'new'
}
})
state.setActiveTab(tab.id)
state.setActiveTabType('terminal')
return tab.id
},
{ worktreeId, command }
)
await expect
.poll(() => getActiveTabId(page), {
timeout: 5_000,
message: 'agent-marked streaming tab did not become active'
})
.toBe(tabId)
await waitForActiveTerminalManager(page, 30_000)
await waitForPaneIdentitySnapshot(page, 1)
// WebGL on: the paint-layer seams under test (atlas wipes, render-pause
// release) only exist on the GPU renderer path.
await forceWebglOnActiveTab(page)
await expect
.poll(
async () =>
latestMatch((await probeRevealedPane(page, tabId))?.screenRows.join('\n') ?? '', FRAME_RE),
{
timeout: 20_000,
message: 'alt-screen TUI fixture did not start streaming in the visible pane'
}
)
.toBeGreaterThan(5)
const ptyId = (await probeRevealedPane(page, tabId))?.ptyId
if (!ptyId) {
throw new Error('streaming tab did not bind a PTY')
}
return {
worktreeId,
tabId,
ptyId,
heartbeatPath,
stop: async () => {
await sendToTerminal(page, ptyId, '\x03').catch(() => {})
await page.waitForTimeout(100)
}
}
}
// Buffer-level convergence: xterm parsed the live frames. This is necessary but
// NOT sufficient — the field bug shows a correct buffer behind garbled pixels.
async function assertBufferConverged(
page: Page,
testInfo: TestInfo,
setup: StreamingTabSetup,
label: string
): Promise<RevealProbe> {
const { tabId, heartbeatPath } = setup
expect(heartbeatFrame(heartbeatPath), 'fixture stopped streaming while hidden').toBeGreaterThan(5)
let lastProbe: RevealProbe | null = null
await expect
.poll(
async () => {
lastProbe = await probeRevealedPane(page, tabId)
if (!lastProbe) {
return 'pane-not-mounted'
}
if (lastProbe.bufferType !== 'alternate') {
return `not-alt-screen bufferType=${lastProbe.bufferType}`
}
const screen = lastProbe.screenRows.join('\n')
const visibleFrame = latestMatch(screen, FRAME_RE)
const visibleTail = latestMatch(screen, TAIL_RE)
if (visibleTail < 0) {
return 'status-row-missing'
}
const liveFrame = heartbeatFrame(heartbeatPath)
if (visibleFrame < 0 || liveFrame - visibleFrame > MAX_VISIBLE_FRAME_LAG) {
return `stale-frame visible=${visibleFrame} live=${liveFrame}`
}
return 'converged'
},
{
timeout: 20_000,
message: `${label}: revealed buffer did not converge to the live alt-screen TUI`
}
)
.toBe('converged')
.catch(async (error) => {
testInfo.annotations.push({
type: `${label}-buffer-divergence`,
description: describeProbe(lastProbe)
})
throw error
})
const probe = await probeRevealedPane(page, tabId)
expect(probe, `${label}: pane disappeared after convergence`).not.toBeNull()
// Geometry: no stale-grid leg — xterm, fit proposal, and PTY must agree
// without any manual resize.
expect(probe!.proposed, `${label}: fit proposal diverges: ${describeProbe(probe)}`).toEqual({
cols: probe!.cols,
rows: probe!.rows
})
expect(
probe!.appliedPtySize,
`${label}: PTY applied size diverges: ${describeProbe(probe)}`
).toEqual({ cols: probe!.cols, rows: probe!.rows })
return probe!
}
/**
* SECONDARY, WEAK check: geometry-stable repaint equivalence.
*
* Freeze the TUI, screenshot, force a clean rebuild in-app (atlas clear + full
* refresh), screenshot again, and require the two to match.
*
* Why weak, despite looking decisive: the "repair" runs the same repaint code
* the reveal already ran, so a defect present in BOTH shots cancels out and the
* comparison reports success. Injecting a frozen renderer proved that blind
* spot. Treat a green result as "the reveal did not leave a repaint-visible
* difference", never as proof the canvas painted correctly.
*
* The sound paint oracles live elsewhere: terminal-reveal-draw-command-probe
* counts WebGL draw commands (unhealable after the fact), and
* terminal-reveal-latch-visual-evidence captures the stale pixels directly.
*/
async function assertRevealPixelsNeedNoRepair(
page: Page,
testInfo: TestInfo,
setup: StreamingTabSetup,
label: string
): Promise<void> {
const { tabId, ptyId } = setup
// Freeze the fixture in place: it stops emitting frames but stays on the alt
// screen holding its last frame. Quitting would leave a shell prompt, and a
// live stream would differ between the two shots for legitimate reasons.
await sendToTerminal(page, ptyId, 'ORCA_FREEZE_NOW').catch(() => {})
await page.waitForTimeout(2_000)
// Guard against a vacuous comparison: both shots must show a real TUI frame.
const frozenProbe = await probeRevealedPane(page, tabId)
expect(
frozenProbe?.bufferType,
`${label}: fixture left the alt screen before the paint check: ${describeProbe(frozenProbe)}`
).toBe('alternate')
expect(
latestMatch(frozenProbe?.screenRows.join('\n') ?? '', FRAME_RE),
`${label}: no TUI frame on screen for the paint check: ${describeProbe(frozenProbe)}`
).toBeGreaterThan(0)
const clip = await paneClipRect(page, tabId)
expect(clip, `${label}: pane rect unavailable for paint check`).not.toBeNull()
const revealed = await page.screenshot({ clip: clip! })
// The in-app equivalent of the user's manual resize repair, with the geometry
// untouched: clear the glyph atlas and force a full repaint from the buffer.
const rebuilt = await page.evaluate((tabId) => {
const manager = window.__paneManagers?.get(tabId)
if (!manager) {
return false
}
manager.resetWebglTextureAtlases?.()
manager.refreshAllPanes?.()
return true
}, tabId)
expect(rebuilt, `${label}: could not force a clean repaint`).toBe(true)
await page.evaluate(
() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))
)
await page.waitForTimeout(600)
const repairedClip = await paneClipRect(page, tabId)
expect(repairedClip, `${label}: pane rect unavailable after repaint`).not.toBeNull()
// Geometry must be identical, or the comparison is not apples-to-apples.
expect(repairedClip, `${label}: pane geometry moved during the repaint`).toEqual(clip)
const repaired = await page.screenshot({ clip: repairedClip! })
writeFileSync(testInfo.outputPath(`${label}-revealed.png`), revealed)
writeFileSync(testInfo.outputPath(`${label}-after-clean-repaint.png`), repaired)
// Anti-vacuity: a blank-vs-blank comparison would "match" trivially. The
// repainted shot is the known-good reference, so require real glyph ink.
const repairedInk = measureInkRatio(repaired)
expect(
repairedInk,
`${label}: repainted pane has no glyphs (ink ${repairedInk.toFixed(4)}) — ` +
'the screenshot is not capturing terminal content, so the comparison is vacuous'
).toBeGreaterThan(0.01)
const diff = compareTerminalScreenshots(repaired, revealed)
expect(
diff.matches,
`${label}: revealed canvas did not match a clean repaint of its own buffer ` +
`(diff ${diff.diffPixels}px, ratio ${diff.diffRatio.toFixed(4)}) — this is STA-2694`
).toBe(true)
}
// Fraction of pixels differing from the dominant (background) color.
function measureInkRatio(screenshot: Buffer): number {
const png = PNG.sync.read(screenshot)
const counts = new Map<number, number>()
for (let offset = 0; offset < png.data.length; offset += 4) {
const key =
((png.data[offset] ?? 0) << 16) |
((png.data[offset + 1] ?? 0) << 8) |
(png.data[offset + 2] ?? 0)
counts.set(key, (counts.get(key) ?? 0) + 1)
}
let backgroundKey = 0
let backgroundCount = -1
for (const [key, count] of counts) {
if (count > backgroundCount) {
backgroundKey = key
backgroundCount = count
}
}
const backgroundRed = (backgroundKey >> 16) & 0xff
const backgroundGreen = (backgroundKey >> 8) & 0xff
const backgroundBlue = backgroundKey & 0xff
let ink = 0
let total = 0
for (let offset = 0; offset < png.data.length; offset += 4) {
const distance =
Math.abs((png.data[offset] ?? 0) - backgroundRed) +
Math.abs((png.data[offset + 1] ?? 0) - backgroundGreen) +
Math.abs((png.data[offset + 2] ?? 0) - backgroundBlue)
total += 1
if (distance > 48) {
ink += 1
}
}
return total > 0 ? ink / total : 0
}
// Reads xterm's live DEC 2026 latch for the pane. While true, RenderService
// buffers every refresh instead of rendering it.
async function readSynchronizedOutputLatch(page: Page, tabId: string): Promise<boolean | null> {
return page.evaluate((tabId) => {
const manager = window.__paneManagers?.get(tabId)
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
const modes = (
pane?.terminal as unknown as
| { _core?: { coreService?: { decPrivateModes?: { synchronizedOutput?: boolean } } } }
| undefined
)?._core?.coreService?.decPrivateModes
return modes ? modes.synchronizedOutput === true : null
}, tabId)
}
async function freezeMidFrameAndSwitchWorktree(
page: Page,
setup: StreamingTabSetup
): Promise<string | null> {
return page.evaluate(
({ currentWorktreeId, ptyId, tabId }) => {
const store = window.__store
if (!store) {
throw new Error('window.__store is unavailable')
}
const otherWorktree = Object.values(store.getState().worktreesByRepo)
.flat()
.find((worktree) => worktree.id !== currentWorktreeId)
if (!otherWorktree) {
return null
}
window.api.pty.write(ptyId, 'ORCA_FREEZE_MID_FRAME')
return new Promise<string>((resolve, reject) => {
const deadline = performance.now() + 5_000
const switchWhenLatched = (): void => {
const manager = window.__paneManagers?.get(tabId)
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
const synchronizedOutput = (
pane?.terminal as unknown as
| {
_core?: {
coreService?: { decPrivateModes?: { synchronizedOutput?: boolean } }
}
}
| undefined
)?._core?.coreService?.decPrivateModes?.synchronizedOutput
if (synchronizedOutput === true) {
store.getState().setActiveWorktree(otherWorktree.id)
resolve(otherWorktree.id)
return
}
if (performance.now() >= deadline) {
reject(new Error('fixture did not leave a synchronized-output frame open'))
return
}
setTimeout(switchWhenLatched, 0)
}
switchWhenLatched()
})
},
{
currentWorktreeId: setup.worktreeId,
ptyId: setup.ptyId,
tabId: setup.tabId
}
)
}
async function streamWhileHidden(setup: StreamingTabSetup, minFrames: number): Promise<void> {
const heartbeatBefore = heartbeatFrame(setup.heartbeatPath)
await expect
.poll(() => heartbeatFrame(setup.heartbeatPath), {
timeout: 60_000,
message: 'fixture did not keep streaming while hidden/parked'
})
.toBeGreaterThan(heartbeatBefore + minFrames)
}
test.describe('OpenCode alt-screen reveal artifacts (STA-2694)', () => {
test('worktree switch away and back paints correctly without a manual resize', async ({
orcaPage
}, testInfo) => {
test.setTimeout(180_000)
const setup = await startStreamingAltScreenTui(orcaPage, testInfo)
try {
test.skip(
!(await hasWebglPane(orcaPage, setup.tabId)),
'WebGL renderer unavailable in this environment'
)
// The field action: switch away to another workspace (suspends rendering
// and releases WebGL), let the agent keep streaming, then come back.
const otherWorktreeId = await switchToOtherWorktree(orcaPage, setup.worktreeId)
test.skip(!otherWorktreeId, 'test session has a single worktree; cannot surface-hide')
await streamWhileHidden(setup, 120)
await withCpuThrottle(orcaPage, 6, async () => {
await switchToWorktree(orcaPage, setup.worktreeId)
await activateTerminalTab(orcaPage, setup.tabId)
await waitForActiveTerminalManager(orcaPage, 30_000)
await orcaPage.waitForTimeout(2_000)
})
await assertBufferConverged(orcaPage, testInfo, setup, 'worktree-return')
await assertRevealPixelsNeedNoRepair(orcaPage, testInfo, setup, 'worktree-return')
} finally {
await setup.stop()
}
})
test('parked tab reveal paints correctly without a manual resize', async ({
orcaPage
}, testInfo) => {
test.setTimeout(240_000)
const setup = await startStreamingAltScreenTui(orcaPage, testInfo, { ticksPerSecond: 20 })
try {
test.skip(
!(await hasWebglPane(orcaPage, setup.tabId)),
'WebGL renderer unavailable in this environment'
)
// Tab B hides tab A; the decoy then hides B so B takes the last-active
// exemption and tab A genuinely cold-parks (renderer torn down).
await createActiveTerminalTab(orcaPage, setup.worktreeId)
await createActiveTerminalTab(orcaPage, setup.worktreeId)
await waitForTabParked(orcaPage, setup.tabId, { parkDelayMs: PARKING_DELAY_MS })
await streamWhileHidden(setup, 150)
await withCpuThrottle(orcaPage, 6, async () => {
await activateTerminalTab(orcaPage, setup.tabId)
await waitForActiveTerminalManager(orcaPage, 30_000)
await orcaPage.waitForTimeout(3_000)
})
await assertBufferConverged(orcaPage, testInfo, setup, 'parked-reveal')
await assertRevealPixelsNeedNoRepair(orcaPage, testInfo, setup, 'parked-reveal')
} finally {
await setup.stop()
}
})
// The field condition the other tests miss: the agent is IDLE when you come
// back. Every test above keeps the TUI streaming across the reveal, so live
// frames repaint whatever the reveal got wrong — the defect heals itself
// before any assertion runs. A real OpenCode session sits waiting for input,
// so nothing arrives to heal it, and whatever the reveal painted is what the
// user stares at until they resize the window.
test('parked reveal of an IDLE alt-screen agent paints without a manual repair', async ({
orcaPage
}, testInfo) => {
test.setTimeout(240_000)
const setup = await startStreamingAltScreenTui(orcaPage, testInfo)
try {
test.skip(
!(await hasWebglPane(orcaPage, setup.tabId)),
'WebGL renderer unavailable in this environment'
)
// Go idle BEFORE hiding, cleanly between brackets — a settled agent
// holding its last full-screen frame.
await sendToTerminal(orcaPage, setup.ptyId, 'ORCA_FREEZE_NOW').catch(() => {})
await orcaPage.waitForTimeout(2_000)
const idleFrame = latestMatch(
(await probeRevealedPane(orcaPage, setup.tabId))?.screenRows.join('\n') ?? '',
FRAME_RE
)
expect(idleFrame, 'fixture never painted a frame before going idle').toBeGreaterThan(0)
// Cold-park the idle tab: renderer torn down, so the reveal must restore
// and repaint entirely from the snapshot with no live output to help.
await createActiveTerminalTab(orcaPage, setup.worktreeId)
await createActiveTerminalTab(orcaPage, setup.worktreeId)
await waitForTabParked(orcaPage, setup.tabId, { parkDelayMs: PARKING_DELAY_MS })
await orcaPage.waitForTimeout(3_000)
await withCpuThrottle(orcaPage, 6, async () => {
await activateTerminalTab(orcaPage, setup.tabId)
await waitForActiveTerminalManager(orcaPage, 30_000)
await orcaPage.waitForTimeout(3_000)
})
// The same frame must still be on screen — the agent produced nothing new.
const revealedProbe = await probeRevealedPane(orcaPage, setup.tabId)
expect(
revealedProbe?.bufferType,
`idle-parked-reveal: pane is not on the alt screen: ${describeProbe(revealedProbe)}`
).toBe('alternate')
expect(
latestMatch(revealedProbe?.screenRows.join('\n') ?? '', FRAME_RE),
`idle-parked-reveal: idle frame lost across the park: ${describeProbe(revealedProbe)}`
).toBeGreaterThan(0)
await assertRevealPixelsNeedNoRepair(orcaPage, testInfo, setup, 'idle-parked-reveal')
} finally {
await setup.stop()
}
})
// The LITERAL user action: "switch away to the desktop". That is an OS-level
// window hide/occlusion, not in-app navigation — it flips
// document.visibilityState, releases the WebGL context, and comes back
// through the window-wake recovery path rather than the worktree-reveal path.
// The pane never unmounts and its worktree never changes, so none of the
// reveal-repaint machinery the other tests exercise even runs.
test('@headful desktop switch away and back paints an idle agent without a manual repair', async ({
orcaPage,
electronApp
}, testInfo) => {
test.setTimeout(240_000)
const setup = await startStreamingAltScreenTui(orcaPage, testInfo)
try {
test.skip(
!(await hasWebglPane(orcaPage, setup.tabId)),
'WebGL renderer unavailable in this environment'
)
// Idle agent holding a full-screen frame — the field state on return.
await sendToTerminal(orcaPage, setup.ptyId, 'ORCA_FREEZE_NOW').catch(() => {})
await orcaPage.waitForTimeout(2_000)
// Switch away to the desktop: hide the window entirely.
await electronApp.evaluate(({ BrowserWindow }) => {
const window = BrowserWindow.getAllWindows()[0]
if (!window) {
throw new Error('No Electron window')
}
window.hide()
})
await orcaPage.waitForTimeout(4_000)
// ...and come back to it.
await electronApp.evaluate(({ BrowserWindow }) => {
const window = BrowserWindow.getAllWindows()[0]
if (!window) {
throw new Error('No Electron window')
}
window.show()
window.focus()
})
await orcaPage.waitForTimeout(3_000)
const revealedProbe = await probeRevealedPane(orcaPage, setup.tabId)
expect(
revealedProbe?.bufferType,
`desktop-return: pane is not on the alt screen: ${describeProbe(revealedProbe)}`
).toBe('alternate')
await assertRevealPixelsNeedNoRepair(orcaPage, testInfo, setup, 'desktop-return')
} finally {
await electronApp
.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0]?.show())
.catch(() => {})
await setup.stop()
}
})
// The mechanism behind STA-2694: OpenTUI-style TUIs bracket every repaint in
// `?2026h … ?2026l`, so a hide can land inside an open bracket. xterm then
// keeps synchronizedOutput latched, and RenderService checks that latch
// BEFORE rendering — so on reveal the forced repaint, the plain refresh
// fallback, and the glyph-atlas rebuild all render zero rows while the buffer
// is perfectly correct. A window resize "fixes" it only because SIGWINCH
// makes the TUI emit the closing `?2026l`.
test('reveal repaints a pane hidden inside an unclosed synchronized-output frame', async ({
orcaPage
}, testInfo) => {
test.setTimeout(180_000)
const setup = await startStreamingAltScreenTui(orcaPage, testInfo)
try {
// Switch in the renderer task that observes the latch; CI round trips can
// otherwise outlast xterm's one-second safety watchdog.
const otherWorktreeId = await freezeMidFrameAndSwitchWorktree(orcaPage, setup)
test.skip(!otherWorktreeId, 'test session has a single worktree; cannot surface-hide')
test.skip(
!(await readSynchronizedOutputLatch(orcaPage, setup.tabId)),
'the watchdog cleared the latch before the hide, so this run proves nothing'
)
await orcaPage.waitForTimeout(2_500)
await switchToWorktree(orcaPage, setup.worktreeId)
await activateTerminalTab(orcaPage, setup.tabId)
await waitForActiveTerminalManager(orcaPage, 30_000)
await orcaPage.waitForTimeout(2_500)
// The reveal must not leave the pane wedged behind the stale latch: while
// it holds, nothing this pane can do repaints a single row.
expect(
await readSynchronizedOutputLatch(orcaPage, setup.tabId),
'reveal left a stale synchronized-output latch, so every repaint is swallowed'
).toBe(false)
await assertRevealPixelsNeedNoRepair(orcaPage, testInfo, setup, 'mid-frame-hide')
} finally {
await setup.stop()
}
})
})