orca/tests/e2e/terminal-codex-cursor-jitte...

835 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { mkdirSync, rmSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import type { Page, TestInfo } from '@stablyai/playwright-test'
import { test, expect } from './helpers/orca-app'
import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store'
import { waitForActiveTerminalManager } from './helpers/terminal'
import { analyzeRasterCursorCells, type RasterCursorCell } from './terminal-cursor-raster-probe'
type ShellCase = {
label: string
shellOverride: 'powershell.exe' | 'cmd.exe' | 'wsl.exe'
codexCommand: string
}
type CaptureTarget = {
tabId: string
ptyId: string
clip: { x: number; y: number; width: number; height: number }
cellWidth: number
cellHeight: number
rows: number
cols: number
cursorX: number
cursorY: number
suppressed: boolean
renderer: 'webgl' | 'dom'
windowsPty: { backend?: string; buildNumber?: number } | null
marker: CursorMarker | null
cursorDebug: CursorDebug
}
type CursorMarker = {
pixelCount: number
minX: number
minY: number
maxX: number
maxY: number
centerX: number
centerY: number
cellX: number
cellY: number
}
type CursorDebug = {
cursorCount: number
cursorLayerCount: number
cursorCanvasCount: number
firstCursorClass: string
firstCursorDisplay: string
firstCursorVisibility: string
firstCursorOpacity: string
firstCursorRect: { x: number; y: number; width: number; height: number } | null
coreCursorHidden: boolean | null
coreCursorInitialized: boolean | null
terminalKeys: string[]
terminalPrototypeKeys: string[]
}
type TerminalWithInternalCore = {
_core?: {
coreService?: {
isCursorHidden: boolean
isCursorInitialized: boolean
}
}
}
type ScreenSnapshot = {
label: string
cursorX: number
cursorY: number
coreCursorHidden: boolean | null
suppressed: boolean
renderer: 'webgl' | 'dom'
marker: CursorMarker | null
windowsPty: { backend?: string; buildNumber?: number } | null
lines: { row: number; text: string }[]
}
type QueuedMessageFrame = ScreenSnapshot & {
index: number
at: number
elapsedMs: number
rasterCursorCells: RasterCursorCell[]
rasterWorkingCursorCells: RasterCursorCell[]
rasterNonInputCursorCells: RasterCursorCell[]
}
type RasterFrameInput = {
buffer: Buffer
at: number
elapsedMs: number
}
type CursorReproWindow = Window & {
__cursorReproOriginalPtyWrite?: (ptyId: string, data: string) => void
__cursorReproConptyDa1ReplyCount?: number
__cursorReproRawChunks?: { id: string; data: string; at: number }[]
__cursorReproRawUnsubscribe?: () => void
}
const CURSOR_THEME = {
cursor: '#23ff45',
cursorAccent: '#001000',
foreground: '#f4f4f5',
background: '#050505',
green: '#44aa66',
brightGreen: '#58c979'
}
const CODEX_TUI_READY_RE = /Ask Codex|OpenAI/i
const CODEX_TRUST_PROMPT_RE = /Do you trust the contents of this directory\?/i
const CODEX_UPDATE_PROMPT_RE = /Update available|Skip until next version/i
const CODEX_WORKING_STATUS_RE =
/W[\s\S]{0,20}o[\s\S]{0,20}r[\s\S]{0,20}k[\s\S]{0,20}i[\s\S]{0,20}n[\s\S]{0,20}g[\s\S]{0,40}\(/i
const QUEUED_CURSOR_SAMPLE_INTERVAL_MS = 5
const QUEUED_CURSOR_CAPTURE_MS = 6_000
const ARTIFACT_DIR = path.join(process.cwd(), '.tmp', 'cursor-jitter-repro')
const CONPTY_DA1_RESPONSE = '\x1b[?61;4c'
const CODEX_REPO_PROMPT = 'tell me about this repo'
const SHELL_CASES: ShellCase[] = [
{
label: 'powershell-codex-attempt-1',
shellOverride: 'powershell.exe',
codexCommand: 'codex --dangerously-bypass-approvals-and-sandbox --dangerously-bypass-hook-trust'
}
]
function isVisibleWorkingCursorFrame(frame: ScreenSnapshot): boolean {
if (!frame.marker || frame.coreCursorHidden !== false) {
return false
}
const cursorLine = frame.lines.find((line) => line.row === frame.cursorY)?.text ?? ''
return /Working/i.test(cursorLine)
}
function workingRows(snapshot: ScreenSnapshot): Set<number> {
return new Set(
snapshot.lines.filter((line) => /Working/i.test(line.text)).map((line) => line.row)
)
}
function isQueuedInputLine(text: string): boolean {
return (
/\bs\b/.test(text) &&
(text.includes('>') || text.includes('') || text.includes('\u00e2\u20ac\u00ba'))
)
}
function isInputCursorRow(snapshot: ScreenSnapshot, row: number): boolean {
const text = snapshot.lines.find((line) => line.row === row)?.text ?? ''
const trimmed = text.trimStart()
return isQueuedInputLine(text) || trimmed.startsWith('')
}
function isPromptCursorFrame(frame: ScreenSnapshot): boolean {
if (!frame.marker || frame.coreCursorHidden !== false) {
return false
}
return isInputCursorRow(frame, frame.marker.cellY)
}
function isUnexpectedVisibleCursorFrame(frame: ScreenSnapshot): boolean {
if (!frame.marker || frame.coreCursorHidden !== false) {
return false
}
return !isInputCursorRow(frame, frame.marker.cellY)
}
function quotePowerShellSingleQuoted(value: string): string {
return `'${value.replaceAll("'", "''")}'`
}
async function createShellTab(
page: Page,
shellOverride: ShellCase['shellOverride']
): Promise<string> {
return page.evaluate((shellOverride) => {
const store = window.__store
if (!store) {
throw new Error('window.__store unavailable')
}
const state = store.getState()
const worktreeId = state.activeWorktreeId
if (!worktreeId) {
throw new Error('No active worktree')
}
store.setState({
settings: { ...state.settings!, terminalWindowsShell: shellOverride }
})
const tab = store.getState().createTab(worktreeId, undefined, shellOverride, {
activate: true
})
store.getState().setActiveTab(tab.id)
return tab.id
}, shellOverride)
}
async function waitForTabPanePtyId(page: Page, tabId: string, timeoutMs: number): Promise<string> {
await expect
.poll(
() =>
page.evaluate((tabId) => {
const manager = window.__paneManagers?.get(tabId)
const activePane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
return activePane?.container?.dataset?.ptyId ?? null
}, tabId),
{
timeout: timeoutMs,
message: `Terminal pane did not receive a PTY binding for tab ${tabId}`
}
)
.not.toBeNull()
const ptyId = await page.evaluate((tabId) => {
const manager = window.__paneManagers?.get(tabId)
const activePane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
return activePane?.container?.dataset?.ptyId ?? null
}, tabId)
if (!ptyId) {
throw new Error(`Terminal pane lost PTY binding for tab ${tabId}`)
}
return ptyId
}
async function getTerminalContentForTab(
page: Page,
tabId: string,
charLimit: number
): Promise<string> {
return page.evaluate(
({ tabId, charLimit }) => {
const manager = window.__paneManagers?.get(tabId)
const activePane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
const text = activePane?.serializeAddon?.serialize?.() ?? ''
return text.slice(-charLimit)
},
{ tabId, charLimit }
)
}
async function prepareCodexTerminal(
page: Page,
shellCase: ShellCase
): Promise<{ tabId: string; ptyId: string }> {
const tabId = await createShellTab(page, shellCase.shellOverride)
await waitForActiveTerminalManager(page, 30_000)
const ptyId = await waitForTabPanePtyId(page, tabId, 30_000)
await applyCursorProbeTheme(page, tabId)
const launchCommand =
shellCase.shellOverride === 'powershell.exe'
? `Set-Location -LiteralPath ${quotePowerShellSingleQuoted(process.cwd())}; ${shellCase.codexCommand}`
: shellCase.codexCommand
await page.keyboard.insertText(launchCommand)
await page.keyboard.press('Enter')
await dismissCodexTrustPromptIfPresent(page, tabId)
await dismissCodexUpdatePromptIfPresent(page, tabId)
try {
await expect
.poll(
async () => CODEX_TUI_READY_RE.test(await getTerminalContentForTab(page, tabId, 8_000)),
{
timeout: 45_000,
message: `${shellCase.label} Codex TUI did not render`
}
)
.toBe(true)
} catch (error) {
writeFileSync(
path.join(ARTIFACT_DIR, `${shellCase.label}-codex-launch-terminal.txt`),
await getTerminalContentForTab(page, tabId, 12_000)
)
throw error
}
await applyCursorProbeTheme(page, tabId)
return { tabId, ptyId }
}
async function dismissCodexTrustPromptIfPresent(page: Page, tabId: string): Promise<void> {
const deadline = Date.now() + 12_000
while (Date.now() < deadline) {
const content = await getTerminalContentForTab(page, tabId, 8_000)
if (CODEX_TUI_READY_RE.test(content) && !CODEX_TRUST_PROMPT_RE.test(content)) {
return
}
if (CODEX_TRUST_PROMPT_RE.test(content)) {
await page.keyboard.press('Enter')
return
}
await page.waitForTimeout(250)
}
}
async function installPtyWriteDiagnostics(page: Page): Promise<void> {
await page.evaluate((da1Response) => {
const reproWindow = window as CursorReproWindow
if (!reproWindow.__cursorReproOriginalPtyWrite) {
reproWindow.__cursorReproOriginalPtyWrite = window.api.pty.write.bind(window.api.pty)
window.api.pty.write = (ptyId, data) => {
if (data === da1Response) {
reproWindow.__cursorReproConptyDa1ReplyCount =
(reproWindow.__cursorReproConptyDa1ReplyCount ?? 0) + 1
}
reproWindow.__cursorReproOriginalPtyWrite!(ptyId, data)
}
}
reproWindow.__cursorReproConptyDa1ReplyCount = 0
}, CONPTY_DA1_RESPONSE)
}
async function installPtyOutputDiagnostics(page: Page): Promise<void> {
await page.evaluate(() => {
const reproWindow = window as CursorReproWindow
reproWindow.__cursorReproRawUnsubscribe?.()
reproWindow.__cursorReproRawChunks = []
reproWindow.__cursorReproRawUnsubscribe = window.api.pty.onData((payload) => {
reproWindow.__cursorReproRawChunks!.push({
id: payload.id,
data: payload.data,
at: Date.now()
})
if (reproWindow.__cursorReproRawChunks!.length > 2_000) {
reproWindow.__cursorReproRawChunks!.shift()
}
})
})
}
async function readPtyOutputDiagnostics(
page: Page
): Promise<{ id: string; data: string; at: number }[]> {
return page.evaluate(() => {
const reproWindow = window as CursorReproWindow
return reproWindow.__cursorReproRawChunks ?? []
})
}
async function readScreenSnapshot(
page: Page,
label: string,
tabId: string,
ptyId: string
): Promise<ScreenSnapshot> {
const target = await readCaptureTarget(page, tabId, ptyId)
const lines = await readScreenLines(page, tabId)
return {
label,
cursorX: target.cursorX,
cursorY: target.cursorY,
coreCursorHidden: target.cursorDebug.coreCursorHidden,
suppressed: target.suppressed,
renderer: target.renderer,
marker: target.marker,
windowsPty: target.windowsPty,
lines
}
}
async function readScreenLines(
page: Page,
tabId: string
): Promise<{ row: number; text: string }[]> {
return page.evaluate((tabId) => {
const manager = window.__paneManagers?.get(tabId)
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
if (!pane) {
throw new Error(`No active pane for tab ${tabId}`)
}
const terminal = pane.terminal
const result: { row: number; text: string }[] = []
const viewportY = terminal.buffer.active.viewportY
for (let row = 0; row < terminal.rows; row += 1) {
const text = terminal.buffer.active.getLine(viewportY + row)?.translateToString(true) ?? ''
if (text.trim()) {
result.push({ row, text })
}
}
return result
}, tabId)
}
function escapePtyChunk(data: string): string {
return data
.split('\u001b')
.join('\\e')
.split('\r')
.join('\\r')
.split('\n')
.join('\\n')
.split('\u0007')
.join('\\a')
}
function formatPtyChunks(chunks: { data: string; at: number }[]): string {
const startedAt = chunks[0]?.at ?? 0
return chunks
.map(
(chunk, index) =>
`@${chunk.at} +${chunk.at - startedAt}ms chunk ${index}\n${escapePtyChunk(chunk.data)}`
)
.join('\n--- chunk ---\n')
}
async function dismissCodexUpdatePromptIfPresent(page: Page, tabId: string): Promise<void> {
const deadline = Date.now() + 12_000
while (Date.now() < deadline) {
const content = await getTerminalContentForTab(page, tabId, 8_000)
if (CODEX_TUI_READY_RE.test(content) && !CODEX_UPDATE_PROMPT_RE.test(content)) {
return
}
if (CODEX_UPDATE_PROMPT_RE.test(content)) {
await page.keyboard.type('3')
await page.keyboard.press('Enter')
return
}
await page.waitForTimeout(250)
}
}
async function applyCursorProbeTheme(page: Page, tabId: string): Promise<void> {
await page.evaluate(
({ tabId, theme }) => {
const manager = window.__paneManagers?.get(tabId)
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
if (!pane) {
throw new Error(`No active pane for tab ${tabId}`)
}
pane.terminal.options.cursorStyle = 'bar'
pane.terminal.options.cursorBlink = true
pane.terminal.options.theme = {
...pane.terminal.options.theme,
...theme
}
pane.terminal.refresh(0, pane.terminal.rows - 1)
pane.terminal.focus()
},
{ tabId, theme: CURSOR_THEME }
)
}
async function readCaptureTarget(page: Page, tabId: string, ptyId: string): Promise<CaptureTarget> {
return page.evaluate(
({ tabId, ptyId }) => {
const manager = window.__paneManagers?.get(tabId)
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
if (!pane) {
throw new Error(`No active pane for tab ${tabId}`)
}
const screen = pane.container.querySelector<HTMLElement>('.xterm-screen')
if (!screen) {
throw new Error('xterm screen element not found')
}
const rect = screen.getBoundingClientRect()
const terminal = pane.terminal
const terminalCore = (terminal as unknown as TerminalWithInternalCore)._core
const cellWidth = rect.width / terminal.cols
const cellHeight = rect.height / terminal.rows
const cursorElement = pane.container.querySelector<HTMLElement>('.xterm-cursor')
const cursorRect = cursorElement?.getBoundingClientRect()
const cursorStyle = cursorElement ? window.getComputedStyle(cursorElement) : null
const cursorVisible =
!!cursorElement &&
!!cursorRect &&
cursorRect.width > 0 &&
cursorRect.height > 0 &&
cursorStyle?.display !== 'none' &&
cursorStyle?.visibility !== 'hidden' &&
Number(cursorStyle?.opacity ?? '1') > 0
const marker =
cursorVisible && cursorRect
? {
pixelCount: Math.max(1, Math.round(cursorRect.width * cursorRect.height)),
minX: cursorRect.left - rect.left,
minY: cursorRect.top - rect.top,
maxX: cursorRect.right - rect.left,
maxY: cursorRect.bottom - rect.top,
centerX: cursorRect.left - rect.left + cursorRect.width / 2,
centerY: cursorRect.top - rect.top + cursorRect.height / 2,
cellX: Math.max(0, Math.floor((cursorRect.left - rect.left) / cellWidth)),
cellY: Math.max(0, Math.floor((cursorRect.top - rect.top) / cellHeight))
}
: null
const cursorCanvas = pane.container.querySelector<HTMLCanvasElement>(
'.xterm-cursor-layer canvas'
)
const canvasLayer = cursorCanvas?.closest<HTMLElement>('.xterm-cursor-layer')
const canvasLayerStyle = canvasLayer ? window.getComputedStyle(canvasLayer) : null
const canvasMarker = (() => {
if (
!cursorCanvas ||
canvasLayerStyle?.display === 'none' ||
canvasLayerStyle?.visibility === 'hidden'
) {
return null
}
const context = cursorCanvas.getContext('2d', { willReadFrequently: true })
if (!context) {
return null
}
const image = context.getImageData(0, 0, cursorCanvas.width, cursorCanvas.height)
let pixelCount = 0
let minX = Number.POSITIVE_INFINITY
let minY = Number.POSITIVE_INFINITY
let maxX = 0
let maxY = 0
let sumX = 0
let sumY = 0
for (let y = 0; y < cursorCanvas.height; y += 1) {
for (let x = 0; x < cursorCanvas.width; x += 1) {
const offset = (y * cursorCanvas.width + x) * 4
const red = image.data[offset] ?? 0
const green = image.data[offset + 1] ?? 0
const blue = image.data[offset + 2] ?? 0
const alpha = image.data[offset + 3] ?? 0
if (red < 180 || green > 100 || blue < 180 || alpha < 120) {
continue
}
pixelCount += 1
minX = Math.min(minX, x)
minY = Math.min(minY, y)
maxX = Math.max(maxX, x)
maxY = Math.max(maxY, y)
sumX += x
sumY += y
}
}
if (pixelCount < 4) {
return null
}
const canvasCellWidth = cursorCanvas.width / terminal.cols
const canvasCellHeight = cursorCanvas.height / terminal.rows
const centerX = sumX / pixelCount
const centerY = sumY / pixelCount
return {
pixelCount,
minX,
minY,
maxX,
maxY,
centerX,
centerY,
cellX: Math.max(0, Math.floor(centerX / canvasCellWidth)),
cellY: Math.max(0, Math.floor(centerY / canvasCellHeight))
}
})()
return {
tabId,
ptyId,
clip: {
x: Math.floor(rect.x),
y: Math.floor(rect.y),
width: Math.max(1, Math.floor(rect.width)),
height: Math.max(1, Math.floor(rect.height))
},
cellWidth,
cellHeight,
rows: terminal.rows,
cols: terminal.cols,
cursorX: terminal.buffer.active.cursorX,
cursorY: terminal.buffer.active.cursorY,
suppressed: false,
renderer: pane.webglAddon ? 'webgl' : 'dom',
windowsPty: (terminal.options.windowsPty ?? null) as {
backend?: string
buildNumber?: number
} | null,
marker: marker ?? canvasMarker,
cursorDebug: {
cursorCount: pane.container.querySelectorAll('.xterm-cursor').length,
cursorLayerCount: pane.container.querySelectorAll('.xterm-cursor-layer').length,
cursorCanvasCount: pane.container.querySelectorAll('.xterm-cursor-layer canvas').length,
firstCursorClass: cursorElement?.className ?? '',
firstCursorDisplay: cursorStyle?.display ?? '',
firstCursorVisibility: cursorStyle?.visibility ?? '',
firstCursorOpacity: cursorStyle?.opacity ?? '',
firstCursorRect: cursorRect
? {
x: cursorRect.x,
y: cursorRect.y,
width: cursorRect.width,
height: cursorRect.height
}
: null,
coreCursorHidden: terminalCore?.coreService?.isCursorHidden ?? null,
coreCursorInitialized: terminalCore?.coreService?.isCursorInitialized ?? null,
terminalKeys: Object.keys(terminal).slice(0, 20),
terminalPrototypeKeys: Object.getOwnPropertyNames(Object.getPrototypeOf(terminal)).slice(
0,
30
)
}
}
},
{ tabId, ptyId }
)
}
async function captureQueuedMessageFrames(
page: Page,
label: string,
tabId: string,
ptyId: string,
testInfo: TestInfo
): Promise<QueuedMessageFrame[]> {
const target = await readCaptureTarget(page, tabId, ptyId)
const viewport = await page.evaluate(() => ({
width: window.innerWidth,
height: window.innerHeight
}))
const rasterInputs: RasterFrameInput[] = []
const screenSamples: QueuedMessageFrame[] = []
const startedAt = Date.now()
let suspiciousScreenshotCount = 0
const cdp = await page.context().newCDPSession(page)
cdp.on('Page.screencastFrame', (params) => {
rasterInputs.push({
buffer: Buffer.from(params.data, 'base64'),
at: Date.now(),
elapsedMs: Date.now() - startedAt
})
void cdp.send('Page.screencastFrameAck', { sessionId: params.sessionId }).catch(() => {})
})
await cdp.send('Page.enable').catch(() => {})
await cdp.send('Page.startScreencast', { format: 'png', everyNthFrame: 1 })
try {
while (Date.now() - startedAt < QUEUED_CURSOR_CAPTURE_MS) {
screenSamples.push({
...(await readScreenSnapshot(page, label, tabId, ptyId)),
index: screenSamples.length,
at: Date.now(),
elapsedMs: Date.now() - startedAt,
rasterCursorCells: [],
rasterWorkingCursorCells: [],
rasterNonInputCursorCells: []
})
await page.waitForTimeout(QUEUED_CURSOR_SAMPLE_INTERVAL_MS)
}
} finally {
await cdp.send('Page.stopScreencast').catch(() => {})
await cdp.detach().catch(() => {})
}
const fallbackSample = screenSamples[0] ?? {
...(await readScreenSnapshot(page, label, tabId, ptyId)),
index: 0,
at: Date.now(),
elapsedMs: Date.now() - startedAt,
rasterCursorCells: [],
rasterWorkingCursorCells: [],
rasterNonInputCursorCells: []
}
const frames: QueuedMessageFrame[] = []
for (const [index, input] of rasterInputs.entries()) {
const sample = screenSamples.reduce(
(nearest, candidate) =>
Math.abs(candidate.elapsedMs - input.elapsedMs) <
Math.abs(nearest.elapsedMs - input.elapsedMs)
? candidate
: nearest,
fallbackSample
)
const rasterCursorCells = analyzeRasterCursorCells(input.buffer, target, viewport)
const rasterWorkingCursorCells = rasterCursorCells.filter((cell) =>
workingRows(sample).has(cell.cellY)
)
const rasterNonInputCursorCells = rasterCursorCells.filter(
(cell) => !isInputCursorRow(sample, cell.cellY)
)
if (
(rasterWorkingCursorCells.length > 0 || rasterNonInputCursorCells.length > 0) &&
suspiciousScreenshotCount < 8
) {
const filename = `queued-message-raster-suspicious-cursor-${index}.png`
await testInfo.attach(filename, {
body: input.buffer,
contentType: 'image/png'
})
writeFileSync(path.join(ARTIFACT_DIR, filename), input.buffer)
suspiciousScreenshotCount += 1
}
frames.push({
...sample,
index,
at: input.at,
elapsedMs: input.elapsedMs,
rasterCursorCells,
rasterWorkingCursorCells,
rasterNonInputCursorCells
})
}
return frames
}
test.describe('Codex terminal cursor jitter repro', () => {
test('keeps queued-message cursor out of the Working status row in native Windows Codex @headful', async ({
orcaPage
}, testInfo) => {
test.skip(process.platform !== 'win32', 'native Windows cursor repro only runs on Windows')
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
await ensureTerminalVisible(orcaPage)
await installPtyWriteDiagnostics(orcaPage)
rmSync(ARTIFACT_DIR, { recursive: true, force: true })
mkdirSync(ARTIFACT_DIR, { recursive: true })
const shellCase = SHELL_CASES[0]!
const { tabId, ptyId } = await prepareCodexTerminal(orcaPage, shellCase)
await installPtyOutputDiagnostics(orcaPage)
await orcaPage.keyboard.type(CODEX_REPO_PROMPT)
await orcaPage.waitForTimeout(250)
await orcaPage.keyboard.press('Enter')
await orcaPage.waitForTimeout(1_000)
if (!CODEX_WORKING_STATUS_RE.test(await getTerminalContentForTab(orcaPage, tabId, 8_000))) {
await orcaPage.keyboard.press('Enter')
}
await orcaPage.waitForTimeout(3_000)
const submittedContent = await getTerminalContentForTab(orcaPage, tabId, 8_000)
writeFileSync(path.join(ARTIFACT_DIR, 'queued-message-after-submit.txt'), submittedContent)
await expect
.poll(
async () =>
CODEX_WORKING_STATUS_RE.test(await getTerminalContentForTab(orcaPage, tabId, 8_000)),
{
timeout: 30_000,
message: 'Codex did not enter Working state'
}
)
.toBe(true)
await applyCursorProbeTheme(orcaPage, tabId)
const workingOnlyFrames = await captureQueuedMessageFrames(
orcaPage,
`${shellCase.label}-no-input`,
tabId,
ptyId,
testInfo
)
await orcaPage.keyboard.insertText('s')
await expect
.poll(
async () =>
(await readScreenLines(orcaPage, tabId)).some((line) => isQueuedInputLine(line.text)),
{
timeout: 5_000,
message: 'queued input did not appear before cursor capture'
}
)
.toBe(true)
const frames = await captureQueuedMessageFrames(
orcaPage,
shellCase.label,
tabId,
ptyId,
testInfo
)
const snapshot = await readScreenSnapshot(orcaPage, shellCase.label, tabId, ptyId)
const rawChunks = await readPtyOutputDiagnostics(orcaPage)
const visibleWorkingOnlyCursorFrames = workingOnlyFrames.filter(isPromptCursorFrame)
const unexpectedWorkingOnlyCursorFrames = workingOnlyFrames.filter(
isUnexpectedVisibleCursorFrame
)
const visibleWorkingCursorFrames = frames.filter(isVisibleWorkingCursorFrame)
const unexpectedQueuedCursorFrames = frames.filter(isUnexpectedVisibleCursorFrame)
const rasterWorkingCursorFrames = frames.filter(
(frame) => frame.rasterWorkingCursorCells.length > 0
)
await testInfo.attach('queued-message-cursor-frames.json', {
body: JSON.stringify(frames, null, 2),
contentType: 'application/json'
})
await testInfo.attach('working-no-input-cursor-frames.json', {
body: JSON.stringify(workingOnlyFrames, null, 2),
contentType: 'application/json'
})
await testInfo.attach('queued-message-screen.json', {
body: JSON.stringify(snapshot, null, 2),
contentType: 'application/json'
})
await testInfo.attach('queued-message-pty-chunks.txt', {
body: formatPtyChunks(rawChunks),
contentType: 'text/plain'
})
writeFileSync(
path.join(ARTIFACT_DIR, 'queued-message-screen.json'),
`${JSON.stringify(snapshot, null, 2)}\n`
)
writeFileSync(
path.join(ARTIFACT_DIR, 'queued-message-cursor-frames.json'),
`${JSON.stringify(frames, null, 2)}\n`
)
writeFileSync(
path.join(ARTIFACT_DIR, 'working-no-input-cursor-frames.json'),
`${JSON.stringify(workingOnlyFrames, null, 2)}\n`
)
writeFileSync(
path.join(ARTIFACT_DIR, 'queued-message-pty-chunks.txt'),
`${formatPtyChunks(rawChunks)}\n`
)
const queuedInputFrames = frames.filter((frame) =>
frame.lines.some((line) => isQueuedInputLine(line.text))
)
expect(
queuedInputFrames.length,
'queued input should be captured before checking cursor placement'
).toBeGreaterThan(0)
expect(
visibleWorkingOnlyCursorFrames,
'Codex should expose its prompt cursor during the no-input capture so the repro can detect misplaced cursor restores'
).not.toEqual([])
expect(
unexpectedWorkingOnlyCursorFrames,
`visible cursor should only appear on Codex prompt rows while Working: ${JSON.stringify(unexpectedWorkingOnlyCursorFrames)}`
).toEqual([])
expect(
visibleWorkingCursorFrames,
`cursor should not be visibly parked on Working row: ${JSON.stringify(visibleWorkingCursorFrames)}`
).toEqual([])
expect(
rasterWorkingCursorFrames,
`rasterized terminal should not paint cursor-colored pixels on Working row: ${JSON.stringify(rasterWorkingCursorFrames)}`
).toEqual([])
expect(
unexpectedQueuedCursorFrames,
`visible cursor should only appear on Codex prompt rows after queuing input: ${JSON.stringify(unexpectedQueuedCursorFrames)}`
).toEqual([])
})
})