fix: drain ACP test launcher streams

This commit is contained in:
Tianyi Cui
2026-07-14 07:22:22 +08:00
parent e2a5a160d3
commit 0b492e6e62
4 changed files with 58 additions and 3 deletions
@@ -18,6 +18,7 @@
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { readdirSync } from 'node:fs'
import { spawn } from 'node:child_process'
import { dirname, join } from 'node:path'
import { randomUUID } from 'node:crypto'
import { createInterface } from 'node:readline'
@@ -50,6 +51,8 @@ interface Behavior {
echoWorkspace?: boolean
/** Write a line to stderr on boot (spec-side stderr-capture assertions). */
stderrNote?: string
/** Let a short-lived descendant retain stdio and emit one final ACP update plus stderr line after this parent exits. */
lateInheritedOutput?: boolean
/** Session logs to persist on stdin EOF. */
logs?: ScriptedLog[]
/** Leave a stray FILE directly under the sessions root (harvest must skip it). */
@@ -256,6 +259,24 @@ function flushLogsAndExit(): void {
writeFileSync(join(sessionsRoot, 'bucket-noise', 'notes.txt'), 'not a session log\n')
}
if (behavior.deleteSessionsRoot === true) rmSync(sessionsRoot, { recursive: true, force: true })
if (behavior.lateInheritedOutput === true) {
const frame = JSON.stringify({
jsonrpc: '2.0',
method: 'session/update',
params: {
sessionId,
update: {
sessionUpdate: 'agent_message_chunk',
content: { type: 'text', text: 'late inherited stdout' },
},
},
})
const code = [
`setTimeout(() => process.stdout.write(${JSON.stringify(`${frame}\n`)}), 50)`,
`setTimeout(() => process.stderr.write(${JSON.stringify('late inherited stderr\n')}), 75)`,
].join(';')
spawn(process.execPath, ['-e', code], { stdio: ['ignore', 1, 2] }).unref()
}
process.exit(0)
}
@@ -91,6 +91,27 @@ describe('runScenario', () => {
expect(exited).toBe(true)
})
it('waits for inherited stdio and buffered ACP parsing after the parent exits', { timeout: 20_000 }, async () => {
const { dir, fixtureFile } = await scenario({ lateInheritedOutput: true })
const launched = launchAcpTestAgent({
agent: AGENT,
cwd: dir,
env: { DSH_SNAPSHOT_FILE: fixtureFile },
})
await launched.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
await launched.client.newSession({ cwd: dir, mcpServers: [] })
const lateUpdate = launched.waitForUpdate(update =>
update.sessionUpdate === 'agent_message_chunk'
&& update.content.type === 'text'
&& update.content.text === 'late inherited stdout')
await launched.close()
await expect(lateUpdate).resolves.toMatchObject({ sessionUpdate: 'agent_message_chunk' })
expect(launched.rawStdout()).toContain('late inherited stdout')
expect(launched.stderr()).toContain('late inherited stderr')
})
it('drives a full turn: initialize (terminal caps), session, prompt, permission stub, harvest', { timeout: 20_000 }, async () => {
const { fixtureFile } = await scenario({
permissionProbe: true,