mirror of
https://github.com/stablyai/orca.git
synced 2026-10-16 16:03:26 +00:00
Skip plain hidden terminal renderer writes
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { shouldSkipHiddenRendererOutput } from './hidden-renderer-skip-eligibility'
|
||||
|
||||
describe('shouldSkipHiddenRendererOutput', () => {
|
||||
it('skips hidden plain ASCII output when a snapshot restore is available', () => {
|
||||
expect(
|
||||
shouldSkipHiddenRendererOutput({
|
||||
foreground: false,
|
||||
canRestoreHiddenOutput: true,
|
||||
startupRendererQueryWindowActive: false,
|
||||
synchronizedOutputActive: false,
|
||||
data: 'line one\r\nline two\tok\n'
|
||||
})
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps visible or non-restorable output on the live renderer path', () => {
|
||||
expect(
|
||||
shouldSkipHiddenRendererOutput({
|
||||
foreground: true,
|
||||
canRestoreHiddenOutput: true,
|
||||
startupRendererQueryWindowActive: false,
|
||||
synchronizedOutputActive: false,
|
||||
data: 'visible\r\n'
|
||||
})
|
||||
).toBe(false)
|
||||
expect(
|
||||
shouldSkipHiddenRendererOutput({
|
||||
foreground: false,
|
||||
canRestoreHiddenOutput: false,
|
||||
startupRendererQueryWindowActive: false,
|
||||
synchronizedOutputActive: false,
|
||||
data: 'hidden\r\n'
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps startup query windows and terminal-control chunks live', () => {
|
||||
expect(
|
||||
shouldSkipHiddenRendererOutput({
|
||||
foreground: false,
|
||||
canRestoreHiddenOutput: true,
|
||||
startupRendererQueryWindowActive: true,
|
||||
synchronizedOutputActive: false,
|
||||
data: 'plain\r\n'
|
||||
})
|
||||
).toBe(false)
|
||||
expect(
|
||||
shouldSkipHiddenRendererOutput({
|
||||
foreground: false,
|
||||
canRestoreHiddenOutput: true,
|
||||
startupRendererQueryWindowActive: false,
|
||||
synchronizedOutputActive: true,
|
||||
data: 'plain row inside synchronized frame\r\n'
|
||||
})
|
||||
).toBe(false)
|
||||
expect(
|
||||
shouldSkipHiddenRendererOutput({
|
||||
foreground: false,
|
||||
canRestoreHiddenOutput: true,
|
||||
startupRendererQueryWindowActive: false,
|
||||
synchronizedOutputActive: false,
|
||||
data: '\x1b]0;title\x07'
|
||||
})
|
||||
).toBe(false)
|
||||
expect(
|
||||
shouldSkipHiddenRendererOutput({
|
||||
foreground: false,
|
||||
canRestoreHiddenOutput: true,
|
||||
startupRendererQueryWindowActive: false,
|
||||
synchronizedOutputActive: false,
|
||||
data: '\x1b[?2026hredraw\x1b[?2026l'
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps rewrite and unicode chunks live', () => {
|
||||
expect(
|
||||
shouldSkipHiddenRendererOutput({
|
||||
foreground: false,
|
||||
canRestoreHiddenOutput: true,
|
||||
startupRendererQueryWindowActive: false,
|
||||
synchronizedOutputActive: false,
|
||||
data: 'progress 10%\rprogress 20%'
|
||||
})
|
||||
).toBe(false)
|
||||
expect(
|
||||
shouldSkipHiddenRendererOutput({
|
||||
foreground: false,
|
||||
canRestoreHiddenOutput: true,
|
||||
startupRendererQueryWindowActive: false,
|
||||
synchronizedOutputActive: false,
|
||||
data: 'emoji 😀\r\n'
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
export type HiddenRendererSkipEligibility = {
|
||||
foreground: boolean
|
||||
canRestoreHiddenOutput: boolean
|
||||
startupRendererQueryWindowActive: boolean
|
||||
synchronizedOutputActive: boolean
|
||||
data: string
|
||||
}
|
||||
|
||||
function isAllowedPlainHiddenOutputCode(code: number): boolean {
|
||||
if (code === 0x09 || code === 0x0a) {
|
||||
return true
|
||||
}
|
||||
return code >= 0x20 && code <= 0x7e
|
||||
}
|
||||
|
||||
function containsOnlyPlainHiddenOutput(data: string): boolean {
|
||||
for (let index = 0; index < data.length; index++) {
|
||||
const code = data.charCodeAt(index)
|
||||
if (code === 0x0d) {
|
||||
if (data.charCodeAt(index + 1) !== 0x0a) {
|
||||
return false
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (!isAllowedPlainHiddenOutputCode(code)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export function shouldSkipHiddenRendererOutput({
|
||||
foreground,
|
||||
canRestoreHiddenOutput,
|
||||
startupRendererQueryWindowActive,
|
||||
synchronizedOutputActive,
|
||||
data
|
||||
}: HiddenRendererSkipEligibility): boolean {
|
||||
if (
|
||||
foreground ||
|
||||
!canRestoreHiddenOutput ||
|
||||
startupRendererQueryWindowActive ||
|
||||
synchronizedOutputActive ||
|
||||
data.length === 0
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return containsOnlyPlainHiddenOutput(data)
|
||||
}
|
||||
@@ -2945,7 +2945,7 @@ describe('connectPanePty', () => {
|
||||
expect(transport.sendInput).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps non-visible local PTY bytes on the live xterm path for release', async () => {
|
||||
it('keeps hidden terminal-control bytes on the live xterm path', async () => {
|
||||
const pendingTimeouts: (() => void)[] = []
|
||||
const originalSetTimeout = globalThis.setTimeout
|
||||
globalThis.setTimeout = vi.fn((fn: () => void) => {
|
||||
@@ -2975,14 +2975,15 @@ describe('connectPanePty', () => {
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
expect(capturedDataCallback.current).not.toBeNull()
|
||||
capturedDataCallback.current?.('hello\r\n')
|
||||
expect(pane.terminal.write).not.toHaveBeenCalledWith('hello\r\n')
|
||||
const controlOutput = '\x1b[2J\x1b[Hhello\r\n'
|
||||
capturedDataCallback.current?.(controlOutput)
|
||||
expect(pane.terminal.write).not.toHaveBeenCalledWith(controlOutput)
|
||||
|
||||
for (const fn of pendingTimeouts) {
|
||||
fn()
|
||||
}
|
||||
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith('hello\r\n')
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith(controlOutput)
|
||||
} finally {
|
||||
globalThis.setTimeout = originalSetTimeout
|
||||
}
|
||||
@@ -3306,7 +3307,7 @@ describe('connectPanePty', () => {
|
||||
binding.dispose()
|
||||
})
|
||||
|
||||
it('writes ordinary hidden output live instead of proactively restoring a snapshot', async () => {
|
||||
it('restores plain hidden output from the main snapshot when the pane returns', async () => {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const transport = createMockTransport('pty-id')
|
||||
const capturedDataCallback: {
|
||||
@@ -3347,20 +3348,24 @@ describe('connectPanePty', () => {
|
||||
})
|
||||
await flushAsyncTicks(20)
|
||||
|
||||
expect(getMainBufferSnapshot).not.toHaveBeenCalled()
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith(hidden)
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith(live, expect.any(Function))
|
||||
expect(getMainBufferSnapshot).toHaveBeenCalledWith('pty-id', { scrollbackRows: 5000 })
|
||||
expect(pane.terminal.write).not.toHaveBeenCalledWith(hidden)
|
||||
expect(pane.terminal.write).not.toHaveBeenCalledWith(live, expect.any(Function))
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith(
|
||||
expect.stringContaining(`snapshot-with-${hidden}`),
|
||||
expect.any(Function)
|
||||
)
|
||||
disposable.dispose()
|
||||
})
|
||||
|
||||
it('writes ordinary hidden remote runtime output live instead of restoring a snapshot', async () => {
|
||||
it('restores plain hidden remote runtime output from its serialized snapshot', async () => {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const transport = createMockTransport('remote:env-1@@terminal-1')
|
||||
const capturedDataCallback: {
|
||||
current: ((data: string, meta?: { seq?: number; rawLength?: number }) => void) | null
|
||||
} = { current: null }
|
||||
transport.serializeBuffer = vi.fn().mockResolvedValue({
|
||||
data: 'remote snapshot\r\n',
|
||||
data: 'remote snapshot with hidden remote output\r\n',
|
||||
cols: 120,
|
||||
rows: 40,
|
||||
seq: 40,
|
||||
@@ -3394,13 +3399,16 @@ describe('connectPanePty', () => {
|
||||
await flushAsyncTicks(20)
|
||||
|
||||
expect(getMainBufferSnapshot).not.toHaveBeenCalled()
|
||||
expect(transport.serializeBuffer).not.toHaveBeenCalled()
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith(hidden)
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith(live, expect.any(Function))
|
||||
expect(transport.serializeBuffer).toHaveBeenCalledWith({ scrollbackRows: 5000 })
|
||||
expect(pane.terminal.write).not.toHaveBeenCalledWith(hidden)
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith(
|
||||
expect.stringContaining('remote snapshot with hidden remote output'),
|
||||
expect.any(Function)
|
||||
)
|
||||
disposable.dispose()
|
||||
})
|
||||
|
||||
it('keeps inactive split-pane hidden output live instead of deferring snapshot restore', async () => {
|
||||
it('defers inactive split-pane plain hidden output restore until the pane returns', async () => {
|
||||
const { resetHiddenOutputRestoreSchedulerForTests } =
|
||||
await import('./hidden-output-restore-scheduler')
|
||||
let disposable: { dispose: () => void } | null = null
|
||||
@@ -3452,9 +3460,12 @@ describe('connectPanePty', () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 30))
|
||||
await flushAsyncTicks(20)
|
||||
|
||||
expect(getMainBufferSnapshot).not.toHaveBeenCalled()
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith(hidden)
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith(live, expect.any(Function))
|
||||
expect(getMainBufferSnapshot).toHaveBeenCalledWith('pty-id', { scrollbackRows: 5000 })
|
||||
expect(pane.terminal.write).not.toHaveBeenCalledWith(hidden)
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith(
|
||||
expect.stringContaining('inactive snapshot'),
|
||||
expect.any(Function)
|
||||
)
|
||||
} finally {
|
||||
disposable?.dispose()
|
||||
resetHiddenOutputRestoreSchedulerForTests()
|
||||
@@ -3519,7 +3530,7 @@ describe('connectPanePty', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('does not retry remote snapshots for ordinary hidden runtime output', async () => {
|
||||
it('retries null remote snapshots for skipped plain hidden runtime output', async () => {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const transport = createMockTransport('remote:env-1@@terminal-1')
|
||||
const capturedDataCallback: {
|
||||
@@ -3555,7 +3566,7 @@ describe('connectPanePty', () => {
|
||||
})
|
||||
await flushAsyncTicks(20)
|
||||
|
||||
expect(transport.serializeBuffer).not.toHaveBeenCalled()
|
||||
expect(transport.serializeBuffer).toHaveBeenCalledTimes(1)
|
||||
expect(pane.terminal.write).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('Orca skipped hidden terminal output'),
|
||||
expect.any(Function)
|
||||
@@ -3568,8 +3579,11 @@ describe('connectPanePty', () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 80))
|
||||
await flushAsyncTicks(20)
|
||||
|
||||
expect(transport.serializeBuffer).not.toHaveBeenCalled()
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith(firstLive, expect.any(Function))
|
||||
expect(transport.serializeBuffer).toHaveBeenCalledTimes(2)
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith(
|
||||
expect.stringContaining('remote recovered snapshot'),
|
||||
expect.any(Function)
|
||||
)
|
||||
disposable.dispose()
|
||||
})
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ import {
|
||||
normalizeAgentProviderSession
|
||||
} from '../../../../shared/agent-session-resume'
|
||||
import { isWslUncPath } from '../../../../shared/wsl-paths'
|
||||
import { shouldSkipHiddenRendererOutput } from './hidden-renderer-skip-eligibility'
|
||||
|
||||
const pendingSpawnByPaneKey = new Map<string, Promise<string | null>>()
|
||||
const SSH_SESSION_EXPIRED_ERROR = 'SSH_SESSION_EXPIRED'
|
||||
@@ -622,6 +623,7 @@ export function connectPanePty(
|
||||
let pendingTerminalBellNotification = false
|
||||
let reattachIdleAgentCursorResetTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let synchronizedForegroundOutputActive = false
|
||||
let synchronizedHiddenOutputActive = false
|
||||
// Why: idle callbacks are registered before the deferred PTY output plumbing
|
||||
// exists. Start with the shared scheduler, then switch to the PTY writer
|
||||
// below so hidden-tab resets keep backlog-recovery callbacks and byte order.
|
||||
@@ -2193,16 +2195,6 @@ export function connectPanePty(
|
||||
}
|
||||
}
|
||||
|
||||
function shouldSkipHiddenRendererOutput(foreground: boolean, data: string): boolean {
|
||||
void foreground
|
||||
void data
|
||||
// Why: release correctness beats the hidden-output perf optimization.
|
||||
// Real OpenCode tables still corrupt after workspace switching when PTY
|
||||
// bytes bypass the renderer, so keep hidden panes on the live xterm path
|
||||
// and leave snapshot skipping for a later perf branch.
|
||||
return false
|
||||
}
|
||||
|
||||
function skipHiddenRendererOutput(data: string): void {
|
||||
respondToSkippedMode2031Subscribe(data)
|
||||
markHiddenOutputRestoreNeeded()
|
||||
@@ -2607,7 +2599,20 @@ export function connectPanePty(
|
||||
const foreground = shouldWritePtyOutputForeground(deps.isVisibleRef.current)
|
||||
const restoreAppliesToCurrentPty =
|
||||
hiddenOutputRestorePtyId !== null && transport.getPtyId() === hiddenOutputRestorePtyId
|
||||
if (shouldSkipHiddenRendererOutput(foreground, data)) {
|
||||
const synchronizedHiddenOutput =
|
||||
!foreground &&
|
||||
(synchronizedHiddenOutputActive ||
|
||||
containsSynchronizedOutputStart(data) ||
|
||||
containsSynchronizedOutputEnd(data))
|
||||
if (
|
||||
shouldSkipHiddenRendererOutput({
|
||||
foreground,
|
||||
canRestoreHiddenOutput: canUseHiddenOutputSnapshot(transport.getPtyId()),
|
||||
startupRendererQueryWindowActive: isHiddenStartupRendererQueryWindowActive(),
|
||||
synchronizedOutputActive: synchronizedHiddenOutput,
|
||||
data
|
||||
})
|
||||
) {
|
||||
skipHiddenRendererOutput(data)
|
||||
} else if (
|
||||
(hiddenOutputRestoreNeeded || hiddenOutputRestoreInFlight) &&
|
||||
@@ -2623,6 +2628,12 @@ export function connectPanePty(
|
||||
} else {
|
||||
writePtyOutputToXterm(data, foreground)
|
||||
}
|
||||
if (!foreground) {
|
||||
synchronizedHiddenOutputActive = shouldSynchronizedOutputRemainActive(
|
||||
data,
|
||||
synchronizedHiddenOutputActive
|
||||
)
|
||||
}
|
||||
|
||||
schedulePendingStartupCommandDelivery()
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import type { Page, TestInfo } from '@stablyai/playwright-test'
|
||||
import { expect } from '@stablyai/playwright-test'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { rmSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import {
|
||||
type HiddenPressureOutputMode,
|
||||
writePressureOutputScript
|
||||
} from './artificial-opencode-hidden-pressure-script'
|
||||
import {
|
||||
ensureTerminalVisible,
|
||||
getActiveWorktreeId,
|
||||
@@ -77,38 +81,6 @@ type HiddenPressureAckGate = {
|
||||
const MAX_HIDDEN_RESTORE_LATENCY_MS = 1_500
|
||||
const MAIN_RENDERER_PRESSURE_TARGET_CHARS = 2 * 1024 * 1024
|
||||
|
||||
export function pressureOutputScript(runId: string): string {
|
||||
return `
|
||||
const paneIndex = process.argv[2] ?? '0'
|
||||
const targetChars = Number(process.argv[3] ?? '0')
|
||||
const delayMs = Number(process.argv[4] ?? '0')
|
||||
const header = 'OPENCODE_PRESSURE_START_${runId}_' + paneIndex + '\\n'
|
||||
const chunkBody = '#'.repeat(8192)
|
||||
let written = 0
|
||||
process.stdout.write(header)
|
||||
function writeMore() {
|
||||
let canContinue = true
|
||||
while (canContinue && written < targetChars) {
|
||||
const frame = String(written).padStart(8, '0')
|
||||
const chunk = '\\x1b[?2026h\\x1b[1;1Hpressure pane=' + paneIndex + ' frame=' + frame + ' ' + chunkBody + '\\x1b[?2026l\\n'
|
||||
written += chunk.length
|
||||
canContinue = process.stdout.write(chunk)
|
||||
}
|
||||
if (written < targetChars) {
|
||||
process.stdout.once('drain', writeMore)
|
||||
return
|
||||
}
|
||||
process.stdout.write('OPENCODE_PRESSURE_DONE_${runId}_' + paneIndex + '\\n')
|
||||
}
|
||||
setTimeout(writeMore, Number.isFinite(delayMs) && delayMs > 0 ? delayMs : 0)
|
||||
`
|
||||
}
|
||||
|
||||
export function writePressureOutputScript(scriptPath: string, runId: string): void {
|
||||
mkdirSync(path.dirname(scriptPath), { recursive: true })
|
||||
writeFileSync(scriptPath, pressureOutputScript(runId))
|
||||
}
|
||||
|
||||
export async function runHiddenRealPtyPressureScenario<
|
||||
TMeasurement extends HiddenPressureMeasurement,
|
||||
TDebug extends HiddenPressureDebug,
|
||||
@@ -120,6 +92,7 @@ export async function runHiddenRealPtyPressureScenario<
|
||||
annotationSuffix,
|
||||
hiddenPaneCount,
|
||||
pressureOutputChars,
|
||||
pressureOutputMode = 'tui',
|
||||
pressureStartDelayMs,
|
||||
testInfo,
|
||||
testRepoPath,
|
||||
@@ -129,6 +102,7 @@ export async function runHiddenRealPtyPressureScenario<
|
||||
annotationSuffix?: string
|
||||
hiddenPaneCount: number
|
||||
pressureOutputChars: number
|
||||
pressureOutputMode?: HiddenPressureOutputMode
|
||||
pressureStartDelayMs: number
|
||||
testInfo: TestInfo
|
||||
testRepoPath: string
|
||||
@@ -158,7 +132,7 @@ export async function runHiddenRealPtyPressureScenario<
|
||||
`.orca-opencode-hidden-pressure-load-${runId}.mjs`
|
||||
)
|
||||
deps.writeInteractivePromptScript(typingScriptPath, runId)
|
||||
writePressureOutputScript(pressureScriptPath, runId)
|
||||
writePressureOutputScript(pressureScriptPath, runId, pressureOutputMode)
|
||||
|
||||
await deps.resetTerminalPtyOutputDebug(orcaPage)
|
||||
await deps.holdTerminalAckGate(
|
||||
@@ -197,8 +171,13 @@ export async function runHiddenRealPtyPressureScenario<
|
||||
ackGate
|
||||
)
|
||||
|
||||
expect(debug?.hiddenRendererSkipCount ?? 0).toBe(0)
|
||||
expect(debug?.hiddenRendererSkippedChars ?? 0).toBe(0)
|
||||
if (pressureOutputMode === 'plain') {
|
||||
expect(debug?.hiddenRendererSkipCount ?? 0).toBeGreaterThan(0)
|
||||
expect(debug?.hiddenRendererSkippedChars ?? 0).toBeGreaterThan(0)
|
||||
} else {
|
||||
expect(debug?.hiddenRendererSkipCount ?? 0).toBe(0)
|
||||
expect(debug?.hiddenRendererSkippedChars ?? 0).toBe(0)
|
||||
}
|
||||
expect(pressureBeforeTyping.peakPendingChars).toBeGreaterThan(0)
|
||||
expect(pressureBeforeTyping.ackGatedFlushSkipCount).toBeGreaterThan(0)
|
||||
expect(mainPressure?.peakRendererInFlightChars ?? 0).toBeGreaterThanOrEqual(
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { mkdirSync, writeFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
export type HiddenPressureOutputMode = 'tui' | 'plain'
|
||||
|
||||
export function pressureOutputScript(runId: string, mode: HiddenPressureOutputMode): string {
|
||||
const headerPrefix = mode === 'plain' ? '' : '\\x1b[0m'
|
||||
const donePrefix = mode === 'plain' ? '' : '\\x1b[0m'
|
||||
const chunkExpression =
|
||||
mode === 'plain'
|
||||
? "'plain pressure pane=' + paneIndex + ' frame=' + frame + ' ' + chunkBody + '\\n'"
|
||||
: "'\\x1b[?2026h\\x1b[1;1Hpressure pane=' + paneIndex + ' frame=' + frame + ' ' + chunkBody + '\\x1b[?2026l\\n'"
|
||||
return `
|
||||
const paneIndex = process.argv[2] ?? '0'
|
||||
const targetChars = Number(process.argv[3] ?? '0')
|
||||
const delayMs = Number(process.argv[4] ?? '0')
|
||||
const header = '${headerPrefix}OPENCODE_PRESSURE_START_${runId}_' + paneIndex + '\\n'
|
||||
const chunkBody = '#'.repeat(8192)
|
||||
let written = 0
|
||||
process.stdout.write(header)
|
||||
function writeMore() {
|
||||
let canContinue = true
|
||||
while (canContinue && written < targetChars) {
|
||||
const frame = String(written).padStart(8, '0')
|
||||
const chunk = ${chunkExpression}
|
||||
written += chunk.length
|
||||
canContinue = process.stdout.write(chunk)
|
||||
}
|
||||
if (written < targetChars) {
|
||||
process.stdout.once('drain', writeMore)
|
||||
return
|
||||
}
|
||||
process.stdout.write('${donePrefix}OPENCODE_PRESSURE_DONE_${runId}_' + paneIndex + '\\n')
|
||||
}
|
||||
setTimeout(writeMore, Number.isFinite(delayMs) && delayMs > 0 ? delayMs : 0)
|
||||
`
|
||||
}
|
||||
|
||||
export function writePressureOutputScript(
|
||||
scriptPath: string,
|
||||
runId: string,
|
||||
mode: HiddenPressureOutputMode
|
||||
): void {
|
||||
mkdirSync(path.dirname(scriptPath), { recursive: true })
|
||||
writeFileSync(scriptPath, pressureOutputScript(runId, mode))
|
||||
}
|
||||
@@ -727,7 +727,8 @@ test.describe('Artificial OpenCode terminal load', () => {
|
||||
testRepoPath: string,
|
||||
testInfo: TestInfo,
|
||||
hiddenPaneCount: number,
|
||||
annotationSuffix?: string
|
||||
annotationSuffix?: string,
|
||||
pressureOutputMode?: 'tui' | 'plain'
|
||||
): Promise<void> {
|
||||
await runHiddenRealPtyPressureScenario({
|
||||
orcaPage,
|
||||
@@ -735,6 +736,7 @@ test.describe('Artificial OpenCode terminal load', () => {
|
||||
annotationSuffix,
|
||||
hiddenPaneCount,
|
||||
pressureOutputChars: PRESSURE_OUTPUT_CHARS,
|
||||
pressureOutputMode,
|
||||
pressureStartDelayMs: HIDDEN_PRESSURE_START_DELAY_MS,
|
||||
testInfo,
|
||||
deps: {
|
||||
@@ -764,6 +766,19 @@ test.describe('Artificial OpenCode terminal load', () => {
|
||||
HIDDEN_PRESSURE_PANES
|
||||
)
|
||||
})
|
||||
test('skips renderer writes for plain hidden PTY output while preserving restore', async ({
|
||||
orcaPage,
|
||||
testRepoPath
|
||||
}, testInfo) => {
|
||||
await runConfiguredHiddenRealPtyPressureScenario(
|
||||
orcaPage,
|
||||
testRepoPath,
|
||||
testInfo,
|
||||
HIDDEN_PRESSURE_PANES,
|
||||
'-plain',
|
||||
'plain'
|
||||
)
|
||||
})
|
||||
for (const paneCount of SCALE_HIDDEN_PRESSURE_PANES) {
|
||||
test(`keeps hidden restore responsive with ${paneCount} ACK-backpressured real PTYs`, async ({
|
||||
orcaPage,
|
||||
|
||||
@@ -84,7 +84,17 @@ async function resetHiddenDebug(page: Page): Promise<void> {
|
||||
|
||||
function writeHiddenFrameScript(scriptPath: string, runId: string): void {
|
||||
const frames = Array.from({ length: 25 }, (_, frame) => tuiFrame(runId, frame))
|
||||
writeFileSync(scriptPath, `process.stdout.write(${JSON.stringify(frames.join(''))})\n`)
|
||||
writeFileSync(
|
||||
scriptPath,
|
||||
`setTimeout(() => process.stdout.write(${JSON.stringify(frames.join(''))}), 250)\n`
|
||||
)
|
||||
}
|
||||
|
||||
function writeLowRiskFrameScript(scriptPath: string, frame: string): void {
|
||||
writeFileSync(
|
||||
scriptPath,
|
||||
`setTimeout(() => process.stdout.write(${JSON.stringify(frame)}), 250)\n`
|
||||
)
|
||||
}
|
||||
|
||||
async function writeHiddenFrames(page: Page, ptyId: string, scriptPath: string): Promise<void> {
|
||||
@@ -229,13 +239,20 @@ test.describe('Hidden terminal TUI visual restore', () => {
|
||||
writeHiddenFrameScript(scriptPath, runId)
|
||||
await resetHiddenDebug(orcaPage)
|
||||
await writeHiddenFrames(orcaPage, hiddenPane.ptyId, scriptPath)
|
||||
await resetHiddenDebug(orcaPage)
|
||||
|
||||
await expect
|
||||
.poll(async () => (await readHiddenDebug(orcaPage))?.hiddenRendererSkipCount ?? 0, {
|
||||
timeout: 10_000,
|
||||
message: 'visually rich hidden TUI output should stay on the live xterm path'
|
||||
})
|
||||
.toBe(0)
|
||||
.toBeLessThanOrEqual(1)
|
||||
await expect
|
||||
.poll(async () => (await readHiddenDebug(orcaPage))?.hiddenRendererSkippedChars ?? 0, {
|
||||
timeout: 10_000,
|
||||
message: 'only incidental hidden shell prompt text may skip after the TUI exits'
|
||||
})
|
||||
.toBeLessThan(512)
|
||||
|
||||
await switchToWorktree(orcaPage, secondWorktreeId)
|
||||
await ensureTerminalVisible(orcaPage)
|
||||
@@ -273,8 +290,9 @@ test.describe('Hidden terminal TUI visual restore', () => {
|
||||
rmSync(scriptPath, { force: true })
|
||||
})
|
||||
|
||||
test('keeps newer live output correct after hidden output stayed live', async ({
|
||||
orcaPage
|
||||
test('keeps newer live output correct after plain hidden output restores', async ({
|
||||
orcaPage,
|
||||
testRepoPath
|
||||
}, testInfo: TestInfo) => {
|
||||
await waitForSessionReady(orcaPage)
|
||||
const firstWorktreeId = await waitForActiveWorktree(orcaPage)
|
||||
@@ -308,18 +326,18 @@ test.describe('Hidden terminal TUI visual restore', () => {
|
||||
const hiddenFrame = lowRiskRestoreFrame(runId, 40)
|
||||
const liveFrame = lowRiskRestoreFrame(runId, 41)
|
||||
const finalMarker = `VISUAL_RESTORE_FINAL_${runId}_41`
|
||||
const scriptPath = path.join(testRepoPath, `.orca-low-risk-hidden-${runId}.mjs`)
|
||||
writeLowRiskFrameScript(scriptPath, hiddenFrame)
|
||||
await resetHiddenDebug(orcaPage)
|
||||
await sendToTerminal(orcaPage, hiddenPane.ptyId, `node ${JSON.stringify(scriptPath)}\r`)
|
||||
await resetHiddenDebug(orcaPage)
|
||||
await injectPaneData(orcaPage, paneKey, hiddenFrame, {
|
||||
seq: hiddenFrame.length,
|
||||
rawLength: hiddenFrame.length
|
||||
})
|
||||
|
||||
await expect
|
||||
.poll(async () => (await readHiddenDebug(orcaPage))?.hiddenRendererSkipCount ?? 0, {
|
||||
timeout: 10_000,
|
||||
message: 'hidden injected output should stay on the live xterm path for release'
|
||||
message: 'plain hidden injected output should skip renderer writes'
|
||||
})
|
||||
.toBe(0)
|
||||
.toBeGreaterThan(0)
|
||||
|
||||
await switchToWorktree(orcaPage, secondWorktreeId)
|
||||
await ensureTerminalVisible(orcaPage)
|
||||
@@ -332,7 +350,7 @@ test.describe('Hidden terminal TUI visual restore', () => {
|
||||
await expect
|
||||
.poll(() => getTerminalContent(orcaPage, 12_000), {
|
||||
timeout: 10_000,
|
||||
message: 'newer live TUI frame did not render after hidden output stayed live'
|
||||
message: 'newer live TUI frame did not render after hidden output restored'
|
||||
})
|
||||
.toContain(finalMarker)
|
||||
|
||||
@@ -346,7 +364,7 @@ test.describe('Hidden terminal TUI visual restore', () => {
|
||||
await expect
|
||||
.poll(() => readTuiCursorState(orcaPage), {
|
||||
timeout: 5_000,
|
||||
message: 'live TUI cursor stayed hidden after hidden output stayed live'
|
||||
message: 'live TUI cursor stayed hidden after hidden output restored'
|
||||
})
|
||||
.toMatchObject({
|
||||
hidden: false,
|
||||
@@ -358,9 +376,10 @@ test.describe('Hidden terminal TUI visual restore', () => {
|
||||
path: screenshotPath,
|
||||
contentType: 'image/png'
|
||||
})
|
||||
rmSync(scriptPath, { force: true })
|
||||
})
|
||||
|
||||
test('keeps hidden terminal side effects live while hidden output stays live', async ({
|
||||
test('keeps hidden terminal side effects live while hidden output may restore', async ({
|
||||
orcaPage
|
||||
}) => {
|
||||
await waitForSessionReady(orcaPage)
|
||||
@@ -396,12 +415,6 @@ test.describe('Hidden terminal TUI visual restore', () => {
|
||||
await resetHiddenDebug(orcaPage)
|
||||
await writeHiddenSideEffectBurst(orcaPage, hiddenPane.ptyId, hiddenTitle, marker)
|
||||
|
||||
await expect
|
||||
.poll(async () => (await readHiddenDebug(orcaPage))?.hiddenRendererSkipCount ?? 0, {
|
||||
timeout: 10_000,
|
||||
message: 'hidden side-effect output should stay on the live xterm path for release'
|
||||
})
|
||||
.toBe(0)
|
||||
await expect
|
||||
.poll(() => getRuntimePaneTitle(orcaPage, hiddenSnapshot.tabId, hiddenPane.numericPaneId), {
|
||||
timeout: 10_000,
|
||||
|
||||
Reference in New Issue
Block a user