From 44664551cfa83012ba1358851d67ef221bb951a9 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Mon, 6 Jul 2026 01:54:09 +0800 Subject: [PATCH] test: wait for ACP stdout frame under e2e parallelism --- examples/acp-agent/tests/acp.e2e.ts | 51 +++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/examples/acp-agent/tests/acp.e2e.ts b/examples/acp-agent/tests/acp.e2e.ts index 06ef962743..3245fe1750 100644 --- a/examples/acp-agent/tests/acp.e2e.ts +++ b/examples/acp-agent/tests/acp.e2e.ts @@ -92,6 +92,46 @@ function spawnAcpAgent(cwd: string, env: NodeJS.ProcessEnv = process.env): Spawn let spawned: Spawned | undefined let workdir: string | undefined +function hasStdoutLine(out: string[]): boolean { + return out.join('').split('\n').some(line => line.trim().length > 0) +} + +async function waitForStdoutLine(child: ChildProcessWithoutNullStreams, out: string[], stderr: string[], timeoutMs: number): Promise { + await new Promise((resolve, reject) => { + const cleanup = () => { + clearTimeout(timeout) + child.stdout.off('data', onData) + child.off('exit', onExit) + child.off('error', onError) + } + const pass = () => { + cleanup() + resolve() + } + const fail = (reason: string) => { + cleanup() + reject(new Error(`${reason}; stderr: ${stderr.join('')}`)) + } + const onData = () => { + if (hasStdoutLine(out)) pass() + } + const onExit = (code: number | null, signal: NodeJS.Signals | null) => { + fail(`ACP child exited before emitting a stdout frame (code ${code ?? 'null'}, signal ${signal ?? 'null'})`) + } + const onError = (error: Error) => { + fail(`ACP child failed before emitting a stdout frame: ${error.message}`) + } + const timeout = setTimeout(() => { + fail(`ACP child did not emit a stdout frame within ${timeoutMs}ms`) + }, timeoutMs) + + child.stdout.on('data', onData) + child.on('exit', onExit) + child.on('error', onError) + onData() + }) +} + afterEach(async () => { if (spawned) { spawned.child.kill('SIGKILL') @@ -114,16 +154,21 @@ describe('acp-agent over real stdio (no key required)', () => { stdio: ['pipe', 'pipe', 'pipe'], }) const out: string[] = [] + const stderr: string[] = [] child.stdout.setEncoding('utf8') + child.stderr.setEncoding('utf8') child.stdout.on('data', (c: string) => out.push(c)) + child.stderr.on('data', (c: string) => stderr.push(c)) // Send a single initialize request as a newline-delimited JSON-RPC frame. const req = JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} } }) child.stdin.write(req + '\n') - // Give it a moment to boot + reply, then inspect stdout. - await new Promise(r => setTimeout(r, 4000)) - child.kill('SIGKILL') + try { + await waitForStdoutLine(child, out, stderr, 15_000) + } finally { + child.kill('SIGKILL') + } const lines = out.join('').split('\n').filter(l => l.trim().length > 0) expect(lines.length).toBeGreaterThan(0)