test(pty-local): keep the raw-mode send active until python3 prints its marker

The darwin-parity job failed intermittently on the SIGINT test with the
operation buffer holding only the echoed command line, never RAW_READY. The
harness sets idleSilenceMs to 250, so when a cold python3 start stays silent
past that bound the send settles as inferred_idle; PtySendOperation.append then
drops all later output, and the marker reaches only the scrollback.

Give the harness per-test idleSilenceMs/timeoutMs overrides and let this
scenario raise both above interpreter startup latency, so the readiness marker
lands inside the send it belongs to. waitForOutput's own deadline and the test
timeout grow to match the new bounds.

The product timings are unchanged; the pty Agent Note records why a test that
waits on an operation must outlast the child's startup.
This commit is contained in:
Chinesezjc
2026-07-27 13:45:04 +08:00
parent 79eb3a9035
commit e56afd718d
4 changed files with 24 additions and 9 deletions

View File

@@ -39,7 +39,10 @@ function stubAgent(ctx: Context, rawId: string): Agent {
}
}
async function harness(mode: 'danger-full-access' | 'workspace-write') {
async function harness(
mode: 'danger-full-access' | 'workspace-write',
overrides: { idleSilenceMs?: number; timeoutMs?: number } = {},
) {
const root = mkdtempSync(join(tmpdir(), 'dsh-pty-local-'))
roots.push(root)
const ctx = new Context()
@@ -51,8 +54,8 @@ async function harness(mode: 'danger-full-access' | 'workspace-write') {
const fiber = await ctx.plugin(ptyLocal, {
pollIntervalMs: 10,
exactProbeAfterMs: 20,
idleSilenceMs: 250,
timeoutMs: 2000,
idleSilenceMs: overrides.idleSilenceMs ?? 250,
timeoutMs: overrides.timeoutMs ?? 2_000,
disposeGraceMs: 500,
scrollbackLines: 100,
scrollbackMaxBytes: 32_768,
@@ -63,8 +66,10 @@ async function harness(mode: 'danger-full-access' | 'workspace-write') {
return { ctx, root, agent, fiber, sandbox: ctx.sandbox as PassthroughSandbox }
}
// PtySendOperation.append drops output once the operation settles, so this only
// observes a marker the child prints while the send is still active.
async function waitForOutput(operation: PtySendOperation, expected: string): Promise<void> {
const deadline = Date.now() + 2_000
const deadline = Date.now() + 5_000
let output = ''
while (!output.includes(expected) && Date.now() < deadline) {
output += operation.readOutput().delta
@@ -132,7 +137,13 @@ describe('pty-local real shell', () => {
}, 10_000)
it('cancels a raw-mode foreground process with a real SIGINT', async () => {
const { ctx, agent } = await harness('danger-full-access')
// A cold `python3` start can stay silent for longer than the 250 ms default
// this harness uses, which settles the send as inferred_idle before the
// interpreter prints its readiness marker; the marker then reaches only the
// scrollback and waitForOutput sees the echoed command line alone. Raise the
// silence bound, and the absolute bound above it, so process startup cannot
// end the send it belongs to.
const { ctx, agent } = await harness('danger-full-access', { idleSilenceMs: 4_000, timeoutMs: 6_000 })
const created = await ctx.pty.spawn(agent, { type: 'shell' })
const controller = new AbortController()
const ready = 'RAW_READY'
@@ -155,5 +166,5 @@ describe('pty-local real shell', () => {
expect(after.viewport).toContain('AFTER_SIGINT')
expect(after.waitReason).toBe('stdin_read')
await ctx.pty.kill(agent, created.sessionId)
}, 10_000)
}, 20_000)
})