orca/tests/e2e/terminal-pty-readiness.ts

103 lines
3.1 KiB
TypeScript

import { randomUUID } from 'node:crypto'
import type { Page } from '@stablyai/playwright-test'
import { expect } from '@stablyai/playwright-test'
import { sendToTerminal } from './helpers/terminal'
import { nodeTerminalCommand } from './terminal-node-command'
import { buildFreshShellProbeInputSequence } from './terminal-probe-input-sequence'
type TerminalPtyReadinessWindow = Window & {
__paneManagers?: Map<
string,
{
getPanes?: () => {
container?: HTMLElement
serializeAddon?: { serialize?: () => string }
}[]
}
>
}
export async function getTerminalContentForPtyId(
page: Page,
ptyId: string,
charLimit: number
): Promise<string> {
return page.evaluate(
({ ptyId, charLimit }) => {
const paneManagers = (window as TerminalPtyReadinessWindow).__paneManagers
for (const manager of paneManagers?.values() ?? []) {
for (const pane of manager.getPanes?.() ?? []) {
if (pane.container?.dataset?.ptyId === ptyId) {
return (pane.serializeAddon?.serialize?.() ?? '').slice(-charLimit)
}
}
}
return ''
},
{ ptyId, charLimit }
)
}
export async function waitForPtyPaneMounted(
page: Page,
ptyId: string,
timeoutMs: number
): Promise<void> {
await expect
.poll(
() =>
page.evaluate((ptyId) => {
const paneManagers = (window as TerminalPtyReadinessWindow).__paneManagers
for (const manager of paneManagers?.values() ?? []) {
if (
manager
.getPanes?.()
.some((pane) => pane.container?.dataset?.ptyId === ptyId && pane.serializeAddon)
) {
return true
}
}
return false
}, ptyId),
{
timeout: timeoutMs,
message: `terminal pane for PTY ${ptyId} was not mounted before shell probing`
}
)
.toBe(true)
}
function encodedMarkerCommand(marker: string): string {
const encoded = Buffer.from(marker, 'utf8').toString('base64')
return `${nodeTerminalCommand([
'-e',
`console.log(Buffer.from('${encoded}', 'base64').toString('utf8'))`
])}\r`
}
export async function waitForPtyShellEcho(
page: Page,
ptyId: string,
timeoutMs: number
): Promise<void> {
const marker = `ORCA_PTY_READY_${randomUUID()}`
const deadline = Date.now() + timeoutMs
await waitForPtyPaneMounted(page, ptyId, Math.min(10_000, timeoutMs))
while (Date.now() < deadline) {
// Why: terminal scrollback includes command echo. Encode the marker inside
// the node snippet so seeing the plain marker proves the shell executed it.
for (const input of buildFreshShellProbeInputSequence(encodedMarkerCommand(marker))) {
await sendToTerminal(page, ptyId, input)
}
const probeDeadline = Date.now() + Math.min(3_000, Math.max(0, deadline - Date.now()))
while (Date.now() < probeDeadline) {
if ((await getTerminalContentForPtyId(page, ptyId, 30_000)).includes(marker)) {
return
}
await page.waitForTimeout(100)
}
}
throw new Error(`PTY shell for ${ptyId} never echoed readiness marker within ${timeoutMs}ms`)
}