Merge remote-tracking branch 'origin/master' into fix/pty-raw-ready-wait

master already stabilized the raw-mode SIGINT scenario in 65e0ffe03 and
3be090f1d with wider scenario-owned bounds and deliberately delayed child
readiness, so the merge takes master's test implementation. What this branch
keeps is the contract that explains why those bounds are required: the note
paragraph on the settle boundary in PtySendOperation.append, and the
waitForOutput comment recording that raising its deadline alone cannot
recover output the send never collected.
This commit is contained in:
Chinesezjc
2026-07-27 18:15:47 +08:00
353 changed files with 7981 additions and 2728 deletions

View File

@@ -41,7 +41,7 @@ function stubAgent(ctx: Context, rawId: string): Agent {
async function harness(
mode: 'danger-full-access' | 'workspace-write',
overrides: { idleSilenceMs?: number; timeoutMs?: number } = {},
timing: { idleSilenceMs?: number; timeoutMs?: number } = {},
) {
const root = mkdtempSync(join(tmpdir(), 'dsh-pty-local-'))
roots.push(root)
@@ -54,8 +54,8 @@ async function harness(
const fiber = await ctx.plugin(ptyLocal, {
pollIntervalMs: 10,
exactProbeAfterMs: 20,
idleSilenceMs: overrides.idleSilenceMs ?? 250,
timeoutMs: overrides.timeoutMs ?? 2_000,
idleSilenceMs: timing.idleSilenceMs ?? 250,
timeoutMs: timing.timeoutMs ?? 2_000,
disposeGraceMs: 500,
scrollbackLines: 100,
scrollbackMaxBytes: 32_768,
@@ -67,9 +67,11 @@ async function harness(
}
// 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() + 5_000
// observes a marker the child prints while the send is still active. A caller
// whose child is slow to print must raise the harness `timing` bounds too;
// extending this deadline alone cannot recover output the send never collected.
async function waitForOutput(operation: PtySendOperation, expected: string, timeoutMs = 2_000): Promise<void> {
const deadline = Date.now() + timeoutMs
let output = ''
while (!output.includes(expected) && Date.now() < deadline) {
output += operation.readOutput().delta
@@ -136,26 +138,25 @@ describe('pty-local real shell', () => {
expect(() => process.kill(pid, 0)).toThrow()
}, 10_000)
it('cancels a raw-mode foreground process with a real SIGINT', async () => {
// 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 })
it('cancels a slow-starting raw-mode foreground process with a real SIGINT', async () => {
const { ctx, agent } = await harness('danger-full-access', {
idleSilenceMs: 10_000,
timeoutMs: 15_000,
})
const created = await ctx.pty.spawn(agent, { type: 'shell' })
const controller = new AbortController()
const ready = 'RAW_READY'
// Delay readiness beyond the shared harness's short send bound so this
// process test owns enough slack for loaded macOS startup and shell echo.
// The interactive shell echoes the command, so only child output may contain the readiness marker.
const command = 'python3 -c \'import signal,sys,termios,time; signal.signal(signal.SIGINT, lambda *_: (print("SIGINT_SEEN", flush=True), sys.exit(0))); attrs=termios.tcgetattr(0); attrs[3] &= ~termios.ISIG; termios.tcsetattr(0, termios.TCSANOW, attrs); print("RAW_" + "READY", flush=True); time.sleep(60)\''
const command = 'python3 -c \'import signal,sys,termios,time; signal.signal(signal.SIGINT, lambda *_: (print("SIGINT_SEEN", flush=True), sys.exit(0))); attrs=termios.tcgetattr(0); attrs[3] &= ~termios.ISIG; termios.tcsetattr(0, termios.TCSANOW, attrs); time.sleep(2.1); print("RAW_" + "READY", flush=True); time.sleep(60)\''
expect(command).not.toContain(ready)
const foreground = ctx.pty.startSend(agent, created.sessionId, {
text: command,
submit: true,
signal: controller.signal,
})
await waitForOutput(foreground, ready)
await waitForOutput(foreground, ready, 15_000)
controller.abort()
const result = await foreground.done
expect(result.waitReason).toBe('stdin_read')