fix(acp): a CLI that stops reading before the first request reports its exit text

The union run flaked test_alias_keeps_the_former_gh_extension_diagnosis: a CLI that dies at once
(the old `gh copilot` extension) can close its stdin before ACPClient writes `initialize`, and the
write raised a bare BrokenPipeError, skipping the early-exit path that reports the CLI's stderr
(and the alias's gh-extension diagnosis). Pre-existing on main's CopilotACPClient. A broken pipe on
a request write now waits briefly for the exit and falls into that path. Test drives the race
deterministically (the fake CLI closes stdin before the client's first write).
This commit is contained in:
Teknium committed 2026-10-25 23:01:26 -07:00
1 parent 8dddcc0101
commit 129f205a0e
2 files changed
+37 -2

No files matched your search

+6 -2
View File
@@ -451,8 +451,12 @@ class ACPClient:
def _request(method: str, params: dict[str, Any], *, text_parts: list[str] | None = None,
reasoning_parts: list[str] | None = None) -> Any:
request_id = next(request_ids)
proc.stdin.write(json.dumps({"jsonrpc": "2.0", "id": request_id, "method": method, "params": params}) + "\n")
proc.stdin.flush()
try:
proc.stdin.write(json.dumps({"jsonrpc": "2.0", "id": request_id, "method": method, "params": params}) + "\n")
proc.stdin.flush()
except BrokenPipeError: # the CLI stopped reading (dying): report its exit text, not a bare EPIPE
with contextlib.suppress(subprocess.TimeoutExpired):
proc.wait(timeout=5)
deadline = session_deadline
while time.monotonic() < deadline and proc.poll() is None:
try:
+31
View File
@@ -14,6 +14,7 @@ import json
import os
import subprocess
import sys
import time
from pathlib import Path
from unittest.mock import patch
@@ -304,6 +305,36 @@ def test_cli_death_is_reported_as_a_crash_not_a_timeout(tmp_path):
pass
_STDIN_CLOSING_ACP_CLI = """import os, sys, time
os.close(0) # stops reading: the client's first write hits a closed pipe
open(sys.argv[1], "w").close()
time.sleep(0.3)
print('fatal: agent cannot start', file=sys.stderr)
sys.exit(2)
"""
def test_cli_that_stops_reading_is_reported_with_its_exit_text(tmp_path):
"""A CLI dying before it reads the first request: the write raised a bare BrokenPipeError, so the
user never saw the CLI's own crash text (and the error read as a client bug)."""
server = tmp_path / "stdin_closing_acp.py"
server.write_text(_STDIN_CLOSING_ACP_CLI, encoding="utf-8")
closed = tmp_path / "stdin-closed"
client = _AcmeClient(command=sys.executable, args=[str(server), str(closed)], acp_cwd=str(tmp_path))
real_spawn = client._spawn
def spawn_then_wait_for_the_close():
proc = real_spawn()
deadline = time.monotonic() + 30
while not closed.exists() and time.monotonic() < deadline:
time.sleep(0.01)
return proc
client._spawn = spawn_then_wait_for_the_close
with pytest.raises(RuntimeError, match="exited early: fatal: agent cannot start"):
with client._session(30):
pass
def test_model_discovery_does_not_allow_file_requests(tmp_path):
"""``list_models`` opens a session only to read the catalog: an fs request on that path is refused."""
target = tmp_path / "should-not-be-read.txt"