diff --git a/tests/conftest.py b/tests/conftest.py index 6b1798c..51f5d55 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,9 +4,27 @@ import shutil import json import uuid import sqlite3 +import subprocess as _subprocess from pathlib import Path import pytest +# Issue #19: the sandbox reaper runs in fixture teardown, before `monkeypatch` +# undoes a test's patches. Bind the real Popen class once at import so a test +# that spies on or forbids subprocess.run/Popen (G5) cannot break the reaper. +# (subprocess.run resolves `Popen` through the module at call time, so binding +# `run` alone is not enough.) +_REAL_POPEN = _subprocess.Popen + + +def _run_real(cmd): + """stdout of `cmd` via the import-time Popen; '' on any OS error.""" + try: + proc = _REAL_POPEN(cmd, stdout=_subprocess.PIPE, stderr=_subprocess.DEVNULL, text=True) + out, _ = proc.communicate(timeout=10) + return out + except (OSError, _subprocess.SubprocessError): + return "" + SKILLS_DIR = str(Path(__file__).resolve().parent.parent / ".agents" / "skills") if SKILLS_DIR not in sys.path: sys.path.insert(0, SKILLS_DIR) @@ -19,29 +37,14 @@ def _subscriber_daemon_pids(root): detached into its own session with `--idle-timeout 0`. A sandbox that ran it must reap it, or every suite run leaves ~30 daemons holding wildcard subscriptions on the default public broker behind.""" - import subprocess as _sp root = str(root) - try: - out = _sp.run(["pgrep", "-f", "reconcile.sh --subscribe"], - capture_output=True, text=True).stdout.split() - except OSError: - return [] found = [] - for pid in out: - try: - args = _sp.run(["ps", "-o", "args=", "-p", pid], - capture_output=True, text=True).stdout - except OSError: - continue - if root in args: + for pid in _run_real(["pgrep", "-f", "reconcile.sh --subscribe"]).split(): + if root in _run_real(["ps", "-o", "args=", "-p", pid]): found.append(int(pid)) kids = [] for pid in found: - try: - kids += [int(k) for k in _sp.run(["pgrep", "-P", str(pid)], - capture_output=True, text=True).stdout.split()] - except (OSError, ValueError): - pass + kids += [int(k) for k in _run_real(["pgrep", "-P", str(pid)]).split() if k.isdigit()] return found + kids @@ -73,12 +76,7 @@ def _pid_alive(pid): except PermissionError: return True # a zombie still answers kill(0); ps state Z means it is gone for our purposes - import subprocess as _sp - try: - state = _sp.run(["ps", "-o", "stat=", "-p", str(pid)], - capture_output=True, text=True).stdout.strip() - except OSError: - return True + state = _run_real(["ps", "-o", "stat=", "-p", str(pid)]).strip() return bool(state) and not state.startswith("Z")