fix: drain failed ACP launches
This commit is contained in:
@@ -178,13 +178,14 @@ export function launchAcpTestAgent(options: AcpTestLaunchOptions): LaunchedAcpTe
|
|||||||
// `closed` follows parser exhaustion. Capture both eagerly so a caller that
|
// `closed` follows parser exhaustion. Capture both eagerly so a caller that
|
||||||
// invokes close after process exit still joins the complete drain boundary.
|
// invokes close after process exit still joins the complete drain boundary.
|
||||||
const stdioClosed = new Promise<void>(resolve => child.once('close', () => { resolve() }))
|
const stdioClosed = new Promise<void>(resolve => child.once('close', () => { resolve() }))
|
||||||
const drained = Promise.all([stdioClosed, client.closed]).then(async () => {
|
const drained = Promise.allSettled([stdioClosed, client.closed]).then(async ([, clientResult]) => {
|
||||||
// The ACP SDK's readable loop dispatches client callbacks without awaiting
|
// The ACP SDK's readable loop dispatches client callbacks without awaiting
|
||||||
// them. Once `closed` settles no new callbacks can start, but callbacks
|
// them. Once `closed` settles no new callbacks can start, but callbacks
|
||||||
// already in flight still belong to this launch's teardown boundary.
|
// already in flight still belong to this launch's teardown boundary.
|
||||||
while (inFlightClientCallbacks.size > 0) {
|
while (inFlightClientCallbacks.size > 0) {
|
||||||
await Promise.allSettled([...inFlightClientCallbacks])
|
await Promise.allSettled([...inFlightClientCallbacks])
|
||||||
}
|
}
|
||||||
|
if (clientResult.status === 'rejected') throw clientResult.reason
|
||||||
})
|
})
|
||||||
// A caller may await a pending update without calling close(). Make natural
|
// A caller may await a pending update without calling close(). Make natural
|
||||||
// stream exhaustion terminal for those waiters too, but only after the
|
// stream exhaustion terminal for those waiters too, but only after the
|
||||||
@@ -206,6 +207,7 @@ export function launchAcpTestAgent(options: AcpTestLaunchOptions): LaunchedAcpTe
|
|||||||
try {
|
try {
|
||||||
await spawned
|
await spawned
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
await drained.catch(() => undefined)
|
||||||
closeUpdateStream()
|
closeUpdateStream()
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,8 +62,17 @@ describe('runScenario', () => {
|
|||||||
it('surfaces an asynchronous child spawn failure through startup and close', async () => {
|
it('surfaces an asynchronous child spawn failure through startup and close', async () => {
|
||||||
const { dir } = await scenario({})
|
const { dir } = await scenario({})
|
||||||
const launched = launchAcpTestAgent({ agent: AGENT, cwd: join(dir, 'missing') })
|
const launched = launchAcpTestAgent({ agent: AGENT, cwd: join(dir, 'missing') })
|
||||||
|
let stdioClosed = false
|
||||||
|
let clientClosed = false
|
||||||
|
launched.child.once('close', () => { stdioClosed = true })
|
||||||
|
void launched.client.closed.then(
|
||||||
|
() => { clientClosed = true },
|
||||||
|
() => { clientClosed = true },
|
||||||
|
)
|
||||||
await expect(launched.spawned).rejects.toMatchObject({ code: 'ENOENT' })
|
await expect(launched.spawned).rejects.toMatchObject({ code: 'ENOENT' })
|
||||||
await expect(launched.close()).rejects.toMatchObject({ code: 'ENOENT' })
|
await expect(launched.close()).rejects.toMatchObject({ code: 'ENOENT' })
|
||||||
|
expect(stdioClosed).toBe(true)
|
||||||
|
expect(clientClosed).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('centralizes ACP boot, captures, updates, fail-closed permissions, and shutdown', { timeout: 20_000 }, async () => {
|
it('centralizes ACP boot, captures, updates, fail-closed permissions, and shutdown', { timeout: 20_000 }, async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user