fix(pty): retain send ownership through cancellation

This commit is contained in:
Tianyi Cui
2026-07-29 10:52:37 +08:00
parent 4d63552d00
commit 0fec058a48
11 changed files with 87 additions and 15 deletions
@@ -340,6 +340,60 @@ describe('LocalPtySession readiness and output', () => {
await operation.done
})
it('retains a canceled send until asynchronous foreground signalling settles', async () => {
vi.useFakeTimers()
const terminal = new FakeTerminal()
const inspector = new FakeInspector()
const session = makeSession(terminal, inspector, config())
await initialize(session, terminal)
const signalGate = Promise.withResolvers<undefined>()
terminal.signalForeground = async (signal) => {
await signalGate.promise
const foreground = await terminal.inspectForeground()
if (foreground === undefined) throw new Error('cannot resolve foreground')
inspector.signalGroup(foreground.processGroupId, signal)
return foreground.processGroupId
}
const operation = session.startSend({ text: 'first', submit: true })
await Promise.resolve()
await Promise.resolve()
expect(operation.cancel()).toBe(true)
terminal.emitData('\x1b]133;D;130\x07dsh> ')
await vi.advanceTimersByTimeAsync(10)
expect(() => session.startSend({ text: 'successor', submit: true })).toThrow('active send')
signalGate.resolve(undefined)
await vi.advanceTimersByTimeAsync(10)
await operation.done
expect(inspector.groups).toContainEqual([456, 'SIGINT'])
expect(inspector.groups).not.toContainEqual([789, 'SIGINT'])
})
it('does not resume cancellation polling after the terminal exits during signalling', async () => {
vi.useFakeTimers()
const terminal = new FakeTerminal()
const inspector = new FakeInspector()
const session = makeSession(terminal, inspector, config())
await initialize(session, terminal)
const signalGate = Promise.withResolvers<undefined>()
terminal.signalForeground = async () => {
await signalGate.promise
return 456
}
const operation = session.startSend({ text: 'first', submit: true })
await Promise.resolve()
await Promise.resolve()
expect(operation.cancel()).toBe(true)
terminal.emitExit(0)
await expect(operation.done).resolves.toMatchObject({ waitReason: 'session_exit' })
signalGate.resolve(undefined)
await vi.advanceTimersByTimeAsync(10)
expect(session.status()).toEqual({ kind: 'exited', exitCode: 0, signal: null })
})
it('retains send ownership after timeout until an asynchronous provider write settles', async () => {
vi.useFakeTimers()
const terminal = new FakeTerminal()