Files
hermes-agent/tests/pm/test_worker_stdio.py

70 lines
2.9 KiB
Python

"""Worker control traffic must never become a subprocess's standard input."""
from __future__ import annotations
import json
import os
from pathlib import Path
import subprocess
import textwrap
import pytest
from tests.pm._fixtures import isolated_python as isolated_python
@pytest.mark.parametrize("streaming", [False, True])
def test_worker_children_get_eof_while_control_pipe_stays_open(tmp_path, isolated_python, streaming):
root = Path(__file__).resolve().parents[2]
# Inject an operation, not a process mock: main owns the real protocol reader
# and the Python engine launches an actual child with inherited standard IO.
script = textwrap.dedent(f"""
import os, sys
from pathlib import Path
sys.path.insert(0, {str(root)!r})
from pm import build_operations, worker
from pm.environment import PythonEnvironment
def probe(cache, ci=False):
environment = PythonEnvironment(
uv=Path(sys.executable), python=Path(sys.executable),
destination=Path(cache) / 'unused', cache=Path(cache),
env=dict(os.environ), output=sys.stderr if {streaming!r} else None,
)
result = environment._run(
['-I', '-c', "import sys; assert sys.stdin.read() == ''; print('EOF_OK')"],
cwd=Path(cache), timeout=10,
)
if result.returncode:
raise RuntimeError(result.stderr)
assert 'EOF_OK' in result.stdout + result.stderr
return 'child completed'
build_operations.prune_cache = probe
worker.main()
""")
request = {
"id": "stdio-probe", "operation": "prune_cache",
"arguments": {"cache": str(tmp_path)}, "callbacks": [], "packages": [],
"context": {"repo": str(root), "lockfile": str(root / "pm/lock.json")},
}
env = {**os.environ, "HERMES_HOME": str(tmp_path / "home"),
"HERMES_RUNTIME_DIR": str(tmp_path / "tools")}
with (tmp_path / "diagnostics.log").open("w+") as diagnostics:
with subprocess.Popen([str(isolated_python), "-I", "-c", script],
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=diagnostics, text=True, env=env) as process:
assert process.stdin is not None and process.stdout is not None
try:
process.stdin.write(json.dumps(request) + "\n")
process.stdin.flush()
# communicate() would close stdin and erase the bug's precondition.
process.wait(timeout=30)
response = json.loads(process.stdout.read())
diagnostics.seek(0)
assert response.get("result") == "child completed", (response, diagnostics.read())
assert process.returncode == 0
finally:
if process.poll() is None:
process.kill()
process.wait(timeout=5)