Files
orca/config/scripts/windows-pty-table-stress.cjs
T
4c38cabb4e fix(windows): synchronize native terminal table lifetime (#23257)
* fix(i18n): restore diff note draft catalog entries

* fix(windows): synchronize native terminal table lifetime

* fix(ci): preserve focused Playwright file selection

* test(windows): retain PTY stress failure evidence

* test: isolate hourly identity inputs from release version changes

* test(ci): require focused commands on every platform

Assert each golden platform step and its focused arguments before deduplicating Playwright discovery probes.

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Co-authored-by: Neil <neil@stably.ai>
2026-09-26 15:11:17 -07:00

157 lines
5.2 KiB
JavaScript

'use strict'
const assert = require('node:assert/strict')
const { createHash } = require('node:crypto')
const { readFileSync, writeSync } = require('node:fs')
const { dirname, resolve } = require('node:path')
function report(phase, details = {}) {
writeSync(1, `${JSON.stringify({ phase, hostPid: process.pid, ...details })}\n`)
}
async function exerciseTable() {
assert.equal(process.platform, 'win32', 'This probe requires real Windows ConPTY')
const rounds = Number(process.env.ORCA_PTY_TABLE_STRESS_ROUNDS ?? 8)
assert.ok(Number.isInteger(rounds) && rounds > 0 && rounds <= 2000)
const pty = require('node-pty')
const nativePath = require.resolve('node-pty/lib/utils')
const loaded = require(nativePath).loadNativeModule('conpty')
const native = loaded.module
const addonPath = resolve(dirname(nativePath), loaded.dir, 'conpty.node')
report('native', {
addonPath,
sha256: createHash('sha256').update(readFileSync(addonPath)).digest('hex'),
node: process.version,
rounds
})
// Unlike production's fallback, this crash probe requires a host that permits nested jobs.
const hostJobAssigned = native.assignCurrentProcessToJob()
report('host-job-precondition', { assigned: hostJobAssigned })
assert.equal(hostJobAssigned, true, 'Probe precondition: host must permit a crash-cleanup job')
const spawned = []
function spawn(round, slot) {
report('spawn', { round, slot })
const proc = pty.spawn(process.env.ComSpec || 'cmd.exe', ['/d', '/q'], {
cwd: process.cwd(),
cols: 80,
rows: 24,
useConptyDll: true
})
const record = { proc, output: '', exited: false, closed: false }
const marker = `ORCA_PTY_READY_${spawned.length}`
let resolveReady
record.ready = new Promise((resolve) => {
resolveReady = resolve
})
record.exit = new Promise((resolveExit) => {
proc.onExit((event) => {
record.exited = true
resolveReady(false)
resolveExit(event)
})
})
proc.onData((chunk) => {
record.output = (record.output + chunk).slice(-2048)
if (record.output.includes(marker)) {
resolveReady(true)
}
})
spawned.push(record)
// Escaping one letter keeps echoed input from satisfying the output marker.
proc.write(`echo ${marker.replace('READY', 'REA^DY')}\r`)
return record
}
async function waitForReady(records) {
let timer
try {
await Promise.race([
Promise.all(
records.map(async (record) => {
assert.equal(await record.ready, true, `Shell exited before ready: ${record.output}`)
})
),
new Promise((_, reject) => {
timer = setTimeout(() => {
const transcripts = records.map(({ proc, output }) => ({ pid: proc.pid, output }))
reject(new Error(`PTY readiness timed out: ${JSON.stringify(transcripts)}`))
}, 15_000)
})
])
} finally {
clearTimeout(timer)
}
for (const { proc } of records) {
const members = native.listJobProcessIds(proc._pty, proc.pid)
assert.ok(members?.includes(proc.pid), `Ready shell ${proc.pid} must retain its job`)
}
report('ready', { shellPids: records.map(({ proc }) => proc.pid) })
}
function close(record, round, slot) {
report('kill', { round, slot, shellPid: record.proc.pid })
record.proc.kill()
record.closed = true
}
let failure
try {
const survivor = spawn(-1, -1)
await waitForReady([survivor])
const slots = []
for (let round = 0; round < rounds; round += 1) {
for (let slot = 0; slot < 3; slot += 1) {
if (slots[slot]) {
close(slots[slot], round, slot)
}
// The next lookup/insertion overlaps the previous shell's native exit watcher.
slots[slot] = spawn(round, slot)
report('resize-clear-list', { round, slot, shellPid: survivor.proc.pid })
survivor.proc.resize(80 + (round % 2), 24)
survivor.proc.clear()
const members = native.listJobProcessIds(survivor.proc._pty, survivor.proc.pid)
assert.ok(members?.includes(survivor.proc.pid), 'A live survivor must retain its job')
}
// A ready terminal runs kill/resize/clear immediately instead of deferring them.
await waitForReady(slots)
}
assert.equal(survivor.exited, false, survivor.output)
report('overlap-complete', { terminals: spawned.length })
} catch (error) {
failure = { error }
} finally {
for (const record of spawned) {
if (!record.closed) {
close(record, -1, -1)
}
}
let timer
try {
await Promise.race([
Promise.all(spawned.map((record) => record.exit)),
new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error('PTY exit callbacks did not drain')), 15_000)
})
])
} catch (error) {
if (failure) {
report('drain-error', { message: error.stack })
} else {
failure = { error }
}
} finally {
clearTimeout(timer)
}
}
if (failure) {
throw failure.error
}
report('complete', { terminals: spawned.length })
}
exerciseTable().catch((error) => {
report('error', { message: error.stack })
process.exitCode = 1
})