fix(runtime): close teardown gaps and simplify framing

This commit is contained in:
Tianyi Cui
2026-07-29 13:51:55 +08:00
parent 045e8462ee
commit 4fecc54998
20 changed files with 164 additions and 36 deletions
@@ -13,6 +13,7 @@ class FakePty {
readonly kills: string[] = []
autoExitOnKill = true
throwKill = false
onKill?: () => void
private readonly dataListeners = new Set<(data: string) => void>()
private readonly exitListeners = new Set<(event: { exitCode: number; signal?: number }) => void>()
@@ -39,6 +40,7 @@ class FakePty {
kill(signal?: string): void {
if (this.throwKill) throw new Error('process raced')
this.kills.push(signal ?? 'SIGHUP')
this.onKill?.()
if (this.autoExitOnKill) this.emitExit(0, signal === 'SIGKILL' ? 9 : 15)
}
@@ -213,6 +215,46 @@ describe('LocalTerminalHandle', () => {
expect(pty.kills).toEqual(['SIGTERM'])
})
it('sweeps a same-session descendant forked while the shell handles TERM', async () => {
const pty = new FakePty()
const inspector = new FakeInspector()
const late = { pid: 124, started: 'shell-term-trap' }
pty.onKill = () => {
inspector.sessionMembers = [late]
inspector.alive.add(late.pid)
}
const handle = new LocalTerminalHandle(pty.asPty(), inspector, 10)
handle.terminate()
await handle.waitForExit()
expect(inspector.processes).toEqual([[late.pid, 'SIGTERM']])
expect(pty.kills).toEqual(['SIGTERM'])
})
it('keeps a failed post-shell sweep retryable until its survivor leaves', async () => {
vi.useFakeTimers()
const pty = new FakePty()
const inspector = new FakeInspector()
const late = { pid: 124, started: 'shell-term-survivor' }
inspector.removeOnSignal = false
pty.onKill = () => {
inspector.sessionMembers = [late]
inspector.alive.add(late.pid)
}
const handle = new LocalTerminalHandle(pty.asPty(), inspector, 10)
handle.terminate()
const failed = expect(handle.waitForExit()).rejects.toThrow('surviving pids: 124')
await vi.advanceTimersByTimeAsync(25)
await failed
inspector.alive.delete(late.pid)
handle.terminate()
expect(await handle.waitForExit()).toBe(true)
expect(inspector.processes).toEqual([[late.pid, 'SIGTERM'], [late.pid, 'SIGKILL']])
})
it('retains captured descendants after reparenting', async () => {
vi.useFakeTimers()
const pty = new FakePty()