fix(issue-19): match the daemon's whole script path so spaced sandbox paths are reaped
Review round 2 (planner-reviewer-codex-01, NOT PASS P2):
- conftest `_is_subscriber_daemon_argv`: `ps -o args=` joins argv unquoted,
so the script path is taken from the shell word up to the first
`/reconcile.sh --subscribe` instead of as one whitespace token. A sandbox
or --basetemp path with a space no longer hides its daemon from the
fixture reaper and the session leak guard. A candidate with whitespace
counts only when it names an existing file, so a bash wrapper under the
root whose later argv carries the words is still excluded.
- tests: a whitespace-path selector regression and an owned live-process
cleanup check (both fail on c096cb4); the R-2 test also pins the bash
wrapper exclusion.
- docs: the polling fallback's signal handling is stated precisely in
monitor SKILL.md and VERSIONS.md (a running --once pass finishes first,
the interval's sleep child ends on its own); create SKILL.md says an
already-running daemon's PID is still reported under the opt-out;
VERSIONS selector wording and test count (14).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCGpJXhSZasJLU38ksDghe
This commit is contained in:
1 parent
c096cb455b
commit
8f9745b128
5 files changed
+95
-14
No files matched your search
@@ -58,7 +58,7 @@ def _run_helper(sandbox: Path, snippet: str, env=None) -> subprocess.CompletedPr
|
||||
|
||||
|
||||
def _daemons_for(sandbox: Path) -> list:
|
||||
"""Same argv-token selection as the conftest reaper (review R-2)."""
|
||||
"""Same argv-shape selection as the conftest reaper (review R-2, P2)."""
|
||||
from conftest import _is_subscriber_daemon_argv
|
||||
|
||||
out = subprocess.run(["pgrep", "-f", "reconcile.sh --subscribe"], capture_output=True, text=True).stdout.split()
|
||||
@@ -228,10 +228,76 @@ def test_reaper_selects_only_the_daemon_argv_shape(tmp_path):
|
||||
# brief-carrying wrapper that merely quotes the words and the path
|
||||
quoting = "python3 -m lib_py.delegation.cli submit --prompt 'note: reconcile.sh --subscribe under {}/.agents/skills/x'".format(root)
|
||||
assert not _is_subscriber_daemon_argv(quoting, root)
|
||||
# ...and one run by bash from under the root, its prompt ending in the daemon's words
|
||||
wrapper = "bash {0}/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job submit --prompt restart {1}".format(root, daemon)
|
||||
assert not _is_subscriber_daemon_argv(wrapper, root)
|
||||
# the real reconcile.sh in a different mode
|
||||
assert not _is_subscriber_daemon_argv(daemon.replace("--subscribe", "--once"), root)
|
||||
assert not _is_subscriber_daemon_argv("", root)
|
||||
|
||||
|
||||
def _spaced_monitor(tmp_path: Path):
|
||||
"""A monitor script under a sandbox path with a space in it, as a pytest
|
||||
`--basetemp` or temp dir may have (codex review P2)."""
|
||||
root = tmp_path / "pytest review" / "case"
|
||||
script = root / ".agents" / "skills" / "multi-agent-mux-monitor" / "scripts" / "reconcile.sh"
|
||||
script.parent.mkdir(parents=True)
|
||||
script.write_text("#!/usr/bin/env bash\nwhile :; do sleep 1; done\n", encoding="utf-8")
|
||||
script.chmod(0o755)
|
||||
return root, script
|
||||
|
||||
|
||||
def test_reaper_selects_daemons_under_whitespace_paths(tmp_path):
|
||||
"""`ps -o args=` does not quote: the script path must be matched whole, both
|
||||
at the sandbox root (fixture reaper) and at an ancestor (session guard)."""
|
||||
from conftest import _is_subscriber_daemon_argv
|
||||
|
||||
root, script = _spaced_monitor(tmp_path)
|
||||
daemon = "bash {} --subscribe --idle-timeout 0".format(script)
|
||||
assert _is_subscriber_daemon_argv(daemon + "\n", str(root))
|
||||
assert _is_subscriber_daemon_argv(daemon, str(tmp_path))
|
||||
assert _is_subscriber_daemon_argv("/bin/sh " + daemon.split(" ", 1)[1], str(root))
|
||||
# the text before the space names another root, not an ancestor
|
||||
assert not _is_subscriber_daemon_argv(daemon, str(tmp_path / "pytest"))
|
||||
# sibling root that this one is a string prefix of
|
||||
assert not _is_subscriber_daemon_argv(daemon.replace("/case/", "/case2/"), str(root))
|
||||
assert not _is_subscriber_daemon_argv(daemon.replace("--subscribe", "--once"), str(root))
|
||||
# wrappers carrying the words: python, and bash from under the spaced root
|
||||
quoting = "python3 -m lib_py.delegation.cli submit --prompt note: {}".format(daemon)
|
||||
assert not _is_subscriber_daemon_argv(quoting, str(root))
|
||||
wrapper = "bash {0}/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job submit --prompt restart {1}".format(root, daemon)
|
||||
assert not _is_subscriber_daemon_argv(wrapper, str(root))
|
||||
|
||||
|
||||
def test_reaper_removes_a_live_daemon_under_a_whitespace_path(tmp_path):
|
||||
"""The owned live-process half of P2: the reaper and the leak guard find a
|
||||
running `bash <spaced path>/reconcile.sh --subscribe` and take it down."""
|
||||
from conftest import _reap_subscriber_daemons, _subscriber_daemon_pids
|
||||
|
||||
root, script = _spaced_monitor(tmp_path)
|
||||
proc = subprocess.Popen(
|
||||
["bash", str(script), "--subscribe", "--idle-timeout", "0"],
|
||||
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
start_new_session=True,
|
||||
)
|
||||
try:
|
||||
deadline = time.time() + 5
|
||||
while time.time() < deadline and proc.pid not in _subscriber_daemon_pids(root):
|
||||
time.sleep(0.1)
|
||||
assert proc.pid in _subscriber_daemon_pids(root), "fixture reaper does not see the daemon"
|
||||
assert proc.pid in _subscriber_daemon_pids(tmp_path), "session leak guard does not see the daemon"
|
||||
assert proc.pid in _reap_subscriber_daemons(root)
|
||||
assert _wait_exit(proc, 5) is not None, "reaper left the daemon running"
|
||||
assert _subscriber_daemon_pids(root) == []
|
||||
finally:
|
||||
if proc.poll() is None:
|
||||
try:
|
||||
os.killpg(proc.pid, signal.SIGKILL)
|
||||
except OSError:
|
||||
pass
|
||||
proc.wait(timeout=5)
|
||||
|
||||
|
||||
def test_sandbox_reaper_terminates_daemons_anchored_in_the_sandbox(mam_sandbox, monkeypatch):
|
||||
from conftest import _reap_subscriber_daemons, _subscriber_daemon_pids
|
||||
from lib_py.delegation import daemon
|
||||
|
||||
Reference in new issue
Block a user