fix(pty): retain sends through cancellation polling
This commit is contained in:
@@ -412,7 +412,7 @@ export class LocalPtySession implements PtyBackendSession {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const foreground = await this.terminal.inspectForeground()
|
const foreground = await this.terminal.inspectForeground()
|
||||||
if (this.active !== operation || this.closing) return
|
if (this.active !== operation || this.closing || this.interrupting === operation) return
|
||||||
const idleFor = Date.now() - this.lastOutputAt
|
const idleFor = Date.now() - this.lastOutputAt
|
||||||
if (this.promptSeen && foreground !== undefined && this.shellPgid === undefined) {
|
if (this.promptSeen && foreground !== undefined && this.shellPgid === undefined) {
|
||||||
this.shellPgid = foreground.processGroupId
|
this.shellPgid = foreground.processGroupId
|
||||||
@@ -439,7 +439,7 @@ export class LocalPtySession implements PtyBackendSession {
|
|||||||
this.settleActive('inferred_idle')
|
this.settleActive('inferred_idle')
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if (this.active === operation && !this.closing) this.failActive(error)
|
if (this.active === operation && !this.closing && this.interrupting !== operation) this.failActive(error)
|
||||||
} finally {
|
} finally {
|
||||||
this.polling = false
|
this.polling = false
|
||||||
const active = this.active
|
const active = this.active
|
||||||
|
|||||||
@@ -406,6 +406,7 @@ describe('LocalPtySession readiness and output', () => {
|
|||||||
await Promise.resolve()
|
await Promise.resolve()
|
||||||
await vi.advanceTimersByTimeAsync(10)
|
await vi.advanceTimersByTimeAsync(10)
|
||||||
expect(inspections).toBe(2)
|
expect(inspections).toBe(2)
|
||||||
|
await vi.advanceTimersByTimeAsync(50)
|
||||||
expect(operation.cancel()).toBe(true)
|
expect(operation.cancel()).toBe(true)
|
||||||
let settled = false
|
let settled = false
|
||||||
void operation.done.then(() => { settled = true })
|
void operation.done.then(() => { settled = true })
|
||||||
@@ -423,6 +424,54 @@ describe('LocalPtySession readiness and output', () => {
|
|||||||
expect((await operation.done).waitReason).toBe('session_exit')
|
expect((await operation.done).waitReason).toBe('session_exit')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('does not let an in-flight readiness failure release a canceled send before signalling settles', async () => {
|
||||||
|
vi.useFakeTimers()
|
||||||
|
const terminal = new FakeTerminal()
|
||||||
|
const inspector = new FakeInspector()
|
||||||
|
const session = makeSession(terminal, inspector, config())
|
||||||
|
await initialize(session, terminal)
|
||||||
|
|
||||||
|
const readiness = Promise.withResolvers<never>()
|
||||||
|
let inspections = 0
|
||||||
|
terminal.inspectForeground = async () => {
|
||||||
|
inspections += 1
|
||||||
|
if (inspections === 1) return { processGroupId: 456, inputWaiting: false }
|
||||||
|
if (inspections === 2) return await readiness.promise
|
||||||
|
return { processGroupId: 456, inputWaiting: true }
|
||||||
|
}
|
||||||
|
const signalling = Promise.withResolvers<undefined>()
|
||||||
|
const signalled = Promise.withResolvers<number>()
|
||||||
|
terminal.signalForeground = async (signal) => {
|
||||||
|
await signalling.promise
|
||||||
|
const foreground = await terminal.inspectForeground()
|
||||||
|
if (foreground === undefined) throw new Error('cannot resolve foreground')
|
||||||
|
inspector.signalGroup(foreground.processGroupId, signal)
|
||||||
|
signalled.resolve(foreground.processGroupId)
|
||||||
|
return foreground.processGroupId
|
||||||
|
}
|
||||||
|
|
||||||
|
const operation = session.startSend({ text: 'first', submit: true })
|
||||||
|
await Promise.resolve()
|
||||||
|
await Promise.resolve()
|
||||||
|
await vi.advanceTimersByTimeAsync(10)
|
||||||
|
expect(inspections).toBe(2)
|
||||||
|
expect(operation.cancel()).toBe(true)
|
||||||
|
let settled = false
|
||||||
|
void operation.done.then(() => { settled = true })
|
||||||
|
|
||||||
|
readiness.reject(new Error('inspection failed during cancellation'))
|
||||||
|
await Promise.resolve()
|
||||||
|
await Promise.resolve()
|
||||||
|
expect(settled).toBe(false)
|
||||||
|
expect(() => session.startSend({ text: 'successor', submit: true })).toThrow(PtyError)
|
||||||
|
|
||||||
|
signalling.resolve(undefined)
|
||||||
|
expect(await signalled.promise).toBe(456)
|
||||||
|
expect(inspector.groups).toContainEqual([456, 'SIGINT'])
|
||||||
|
await session.close('test complete')
|
||||||
|
expect((await operation.done).waitReason).toBe('session_exit')
|
||||||
|
})
|
||||||
|
|
||||||
it('signals only after an in-flight provider write lands', async () => {
|
it('signals only after an in-flight provider write lands', async () => {
|
||||||
vi.useFakeTimers()
|
vi.useFakeTimers()
|
||||||
const terminal = new FakeTerminal()
|
const terminal = new FakeTerminal()
|
||||||
|
|||||||
Reference in New Issue
Block a user