test(issue-19): run the sandbox reaper through an import-time Popen
The reaper executes in fixture teardown, before monkeypatch restores a
test's patches; G5 forbids subprocess.run/Popen for the test's duration and
subprocess.run resolves Popen through the module at call time, so the
teardown tripped the spy. Bind the Popen class at import instead.
Full suite at 38d8ccb: 870 passed, 1 error (this teardown), 0 leaked daemons.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
This commit is contained in:
1 parent
38d8ccb316
commit
4da4a50682
1 file changed
+22
-24
+22
-24
@@ -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")
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user