a84a4b52aa
Continuous Integration / Pre-commit Linter (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.10) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.11) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.12) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.10) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.11) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.12) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.14) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Waiting to run
Copybara PR Handler / close-imported-pr (push) Waiting to run
The dev-server test-run endpoint spawned pytest inside a fire-and-forget asyncio.create_task() and piped its output through an unbounded asyncio.Queue. Nothing owned that task, so a client that disconnected mid-run left pytest, its descendants, and the output pump running until the server itself exited, and the queue could grow without bound while no consumer was draining it. The response iterator now owns the subprocess for its whole lifetime: it spawns pytest, reads bounded chunks straight off the pipe so the client applies natural backpressure, and terminates the process tree in a finally block. Termination reaches descendants rather than just the direct child - on POSIX pytest is started as its own process-group leader and signalled with os.killpg, and on Windows it runs in a new process group torn down with taskkill /T. Cleanup escalates from a graceful signal to a forced kill after a bounded wait, and falls back to signalling the direct child if the process group turns out not to exist. Cleanup runs under an anyio shield, so the cancel scope the server cancels on client disconnect cannot interrupt it partway. The shield covers the common case, where the disconnect arrives while the iterator is parked reading pytest output or awaiting process exit. It is not a guarantee on every path: if the disconnect lands while the iterator is suspended at a yield, the async generator is dropped rather than cancelled, and its finally block runs at async-generator finalization instead. That finalization does happen under CPython, but its timing is not deterministic. Behavior change: disconnecting from the test-output stream now aborts the in-flight pytest run. Previously the run continued to completion in the background after the client went away. Nothing persists the result of a run - the output is only streamed - so a background completion was unobservable, but a caller that relied on starting a run and hanging up must now keep the response stream open until it ends. The endpoint path, its parameters, and the streamed byte content are unchanged. Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 970107514