fix(subagent): close terminal teardown races

This commit is contained in:
pku-xht
2026-08-04 20:26:45 +08:00
parent f96acad438
commit 451c2929ed
6 changed files with 135 additions and 30 deletions

View File

@@ -1,4 +1,4 @@
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
import { chmodSync, existsSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
@@ -190,6 +190,72 @@ describe('disposeAcpChild (the backend-owned teardown ladder over seam verbs)',
await expect(disposeAcpChild(never, 20, 20)).rejects.toThrow(/did not exit within its dispose windows/)
})
it('keeps an oversized finite escalation window instead of collapsing it to one millisecond', async () => {
vi.useFakeTimers()
try {
let waitCount = 0
let reportExited!: (exited: boolean) => void
const terminate = vi.fn()
const waitForExit = vi.fn((signal?: AbortSignal) => {
waitCount += 1
return new Promise<boolean>((resolve) => {
signal?.addEventListener('abort', () => { resolve(false) }, { once: true })
if (waitCount === 2) reportExited = resolve
})
})
const child: Parameters<typeof disposeAcpChild>[0] = {
pid: 1,
stdin: undefined,
stdout: undefined,
stderr: undefined,
collected: {},
done: new Promise(() => {}),
terminate,
waitForExit,
}
const disposal = disposeAcpChild(child, 0.25, Number.MAX_VALUE)
await vi.advanceTimersByTimeAsync(1)
expect(terminate).toHaveBeenCalledOnce()
expect(waitForExit).toHaveBeenCalledTimes(2)
const escalationSignal = waitForExit.mock.calls[1]?.[0]
await vi.advanceTimersByTimeAsync(1)
expect(escalationSignal?.aborted).toBe(false)
reportExited(true)
await expect(disposal).resolves.toBeUndefined()
expect(vi.getTimerCount()).toBe(0)
} finally {
vi.useRealTimers()
}
})
it('chains a doubled grace beyond one Node timer segment', async () => {
vi.useFakeTimers()
try {
const waitForExit = vi.fn((signal?: AbortSignal) => new Promise<boolean>((resolve) => {
signal?.addEventListener('abort', () => { resolve(false) }, { once: true })
}))
const child: Parameters<typeof disposeAcpChild>[0] = {
pid: 1,
stdin: undefined,
stdout: undefined,
stderr: undefined,
collected: {},
done: new Promise(() => {}),
terminate: vi.fn(),
waitForExit,
}
const disposal = disposeAcpChild(child, 0.25, 1_073_741_823.75)
const rejected = expect(disposal).rejects.toThrow(/did not exit within its dispose windows/)
await vi.advanceTimersByTimeAsync(1)
await vi.advanceTimersByTimeAsync(2_147_483_647)
expect(waitForExit).toHaveBeenCalledTimes(3)
await vi.advanceTimersByTimeAsync(1)
await rejected
} finally {
vi.useRealTimers()
}
})
it('observes a spawn-level rejection and returns without a process to reap', async () => {
const child = spawnSubprocess({
argv: ['bash', '-c', 'true'],