Skip to content

Commit a1dcfd0

Browse files
committed
test(acp_client): run teardown regressions on native Windows
Tolerate an absent killpg attribute and always close launched clients. Record real taskkill results on Windows after an ACP handshake, retain the POSIX simulation, and run both regressions in Windows CI.
1 parent be55e81 commit a1dcfd0

2 files changed

Lines changed: 44 additions & 27 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,9 @@ jobs:
401401
- name: Test the registered launcher command
402402
run: uv run --frozen --python 3.13 --no-sync pytest tests/test_cli_agents_commands.py::test_register_pins_the_resolved_row_into_the_host_roster -q -n 0 --tb=short
403403

404+
- name: Test Windows ACP teardown
405+
run: uv run --frozen --python 3.13 --no-sync pytest tests/test_subagent_acp.py -k close_without_killpg -q -n 0 --tb=short
406+
404407
core-wheel:
405408
name: kernel wheel smoke
406409
runs-on: ubuntu-latest

‎tests/test_subagent_acp.py‎

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3175,25 +3175,31 @@ async def test_a_request_after_the_child_died_still_names_the_exit_and_the_reaso
31753175
await client.request("initialize", acp_protocol.initialize_params(), timeout=10)
31763176

31773177

3178-
def _without_killpg(monkeypatch: pytest.MonkeyPatch) -> list[list[str]]:
3178+
def _without_killpg(monkeypatch: pytest.MonkeyPatch) -> list[tuple[list[str], int]]:
31793179
"""Windows as ``close`` sees it: no ``os.killpg``, and a ``taskkill`` on PATH.
31803180
3181-
The ``taskkill`` runs are recorded rather than made.
3181+
Record real ``taskkill`` runs on Windows and simulate them on POSIX.
31823182
"""
31833183
import shutil
31843184
import subprocess
31853185

31863186
real_which = shutil.which
3187-
ran: list[list[str]] = []
3188-
3189-
def run(argv: list[str], **_kw: Any) -> subprocess.CompletedProcess:
3190-
ran.append(list(argv))
3191-
return subprocess.CompletedProcess(argv, 0)
3192-
3193-
monkeypatch.delattr(os, "killpg")
3194-
monkeypatch.setattr(
3195-
shutil, "which", lambda name: r"C:\Windows\System32\taskkill.exe" if name == "taskkill" else real_which(name)
3196-
)
3187+
real_run = subprocess.run
3188+
native = not hasattr(os, "killpg")
3189+
taskkill = real_which("taskkill") if native else r"C:\Windows\System32\taskkill.exe"
3190+
if taskkill is None:
3191+
pytest.skip("taskkill is unavailable")
3192+
ran: list[tuple[list[str], int]] = []
3193+
3194+
def run(argv: list[str], **kw: Any) -> subprocess.CompletedProcess:
3195+
if argv[0] != taskkill:
3196+
return real_run(argv, **kw)
3197+
result = real_run(argv, **kw) if native else subprocess.CompletedProcess(argv, 0)
3198+
ran.append((list(argv[1:]), result.returncode))
3199+
return result
3200+
3201+
monkeypatch.delattr(os, "killpg", raising=False)
3202+
monkeypatch.setattr(shutil, "which", lambda name: taskkill if name == "taskkill" else real_which(name))
31973203
monkeypatch.setattr(subprocess, "run", run)
31983204
return ran
31993205

@@ -3205,18 +3211,23 @@ async def test_close_without_killpg_ends_the_tree_through_taskkill(monkeypatch:
32053211
its transport all still there. ``taskkill /T`` reaches what the launcher
32063212
started, which a bare ``Process.kill`` does not.
32073213
"""
3214+
from raven.acp_client import protocol
32083215
from raven.acp_client.client import AcpClient
32093216

32103217
cfg = stub_config("windows-close")
32113218
client = await AcpClient.launch(name="windows-close", command=cfg.command, env=dict(cfg.env))
3212-
proc = client._proc
3213-
ran = _without_killpg(monkeypatch)
3219+
try:
3220+
await client.request("initialize", protocol.initialize_params(), timeout=15)
3221+
proc = client._proc
3222+
ran = _without_killpg(monkeypatch)
32143223

3215-
await client.close()
3224+
await client.close()
32163225

3217-
assert ran == [[r"C:\Windows\System32\taskkill.exe", "/T", "/F", "/PID", str(proc.pid)]]
3218-
assert proc.returncode is not None, "the Process.kill after it must still stop the launcher"
3219-
assert client.closed and not client.alive
3226+
assert ran == [(["/T", "/F", "/PID", str(proc.pid)], 0)]
3227+
assert proc.returncode is not None, "close must stop the launcher"
3228+
assert client.closed and not client.alive
3229+
finally:
3230+
await client.close()
32203231

32213232

32223233
async def test_close_without_killpg_kills_nothing_by_the_pid_of_an_exited_launcher(
@@ -3227,17 +3238,20 @@ async def test_close_without_killpg_kills_nothing_by_the_pid_of_an_exited_launch
32273238

32283239
cfg = stub_config("abort", mode="abort")
32293240
client = await AcpClient.launch(name="abort", command=cfg.command, env=dict(cfg.env))
3230-
for _ in range(200):
3231-
if client._proc.returncode is not None:
3232-
break
3233-
await asyncio.sleep(0.025)
3234-
assert client._proc.returncode is not None, "the stub must be gone before the close, or this tests nothing"
3235-
ran = _without_killpg(monkeypatch)
3241+
try:
3242+
for _ in range(200):
3243+
if client._proc.returncode is not None:
3244+
break
3245+
await asyncio.sleep(0.025)
3246+
assert client._proc.returncode is not None, "the stub must be gone before the close, or this tests nothing"
3247+
ran = _without_killpg(monkeypatch)
32363248

3237-
await client.close()
3249+
await client.close()
32383250

3239-
assert ran == []
3240-
assert client.closed
3251+
assert ran == []
3252+
assert client.closed
3253+
finally:
3254+
await client.close()
32413255

32423256

32433257
@pytest.mark.skipif(not hasattr(os, "killpg"), reason="process groups are POSIX")

0 commit comments

Comments
 (0)