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:
GodopuandClaude Fable 5.1 committed 2026-09-13 16:09:28 +09:00
1 parent 38d8ccb316
commit 4da4a50682
1 file changed
+22 -24
+22 -24
View File
@@ -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")