fix(process): let the yield-to-background kill test signal the reparented orphan
tests/tools/test_terminal_yield_to_background.py went red after the parent-first
teardown (f11499fdb3a): kill_process() returned {'status': 'error'} instead of
'killed' for a live backgrounded `bash -c 'echo started; sleep 60; echo done'`.
Root cause is the test harness, not the registry. _terminate_host_pid() now
SIGTERMs the shell before its descendants; a non-interactive bash dies at once
and `sleep 60` is reparented to init before the registry reaps it from the
descendant snapshot. tests/conftest.py's live-system guard allowlists a PID by
walking its parent chain up to the test process, so the reparented orphan looks
foreign and the guard raises RuntimeError inside psutil.Process.terminate().
That is not a psutil/OSError, so `suppress(gone)` lets it escape and
kill_process() wraps it as 'error'. Outside pytest the same path returns
'killed' and the orphan is gone (verified with a standalone probe).
Opt the test into real signal delivery with the established
@pytest.mark.live_system_guard_bypass (it already spawns and kills a real
process) and tighten it: capture the shell's descendants before the kill and
assert none survives, so the anti-orphan snapshot cleanup is covered end to end
instead of being hidden behind the harness. Assertions are unchanged otherwise.
This commit is contained in:
@@ -36,7 +36,13 @@ class _Agent(InterruptControlMixin):
|
||||
self._execution_thread_id = None
|
||||
|
||||
|
||||
@pytest.mark.live_system_guard_bypass
|
||||
def test_redirect_mid_command_yields_it_to_background_without_killing_it(tmp_path, monkeypatch):
|
||||
"""Real signal delivery is required: the registry SIGTERMs the shell first, so its
|
||||
``sleep`` child is reparented to init before the registry reaps it from the descendant
|
||||
snapshot — the conftest guard's parent-chain walk can no longer see it under the test PID."""
|
||||
import psutil
|
||||
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
agent = _Agent()
|
||||
res = {}
|
||||
@@ -65,9 +71,13 @@ def test_redirect_mid_command_yields_it_to_background_without_killing_it(tmp_pat
|
||||
assert os.path.exists(f"/proc/{r['pid']}")
|
||||
assert process_registry.poll(r["session_id"])["status"] == "running"
|
||||
assert not interrupt_mod.is_thread_yield_requested(t.ident)
|
||||
descendants = psutil.Process(r["pid"]).children(recursive=True)
|
||||
assert descendants, "the yielded shell should still own its sleep child"
|
||||
finally:
|
||||
killed = process_registry.kill_process(r["session_id"])
|
||||
assert killed["status"] == "killed"
|
||||
assert killed["status"] == "killed", killed
|
||||
# The shell exits on SIGTERM before its child does; the orphaned child must still be reaped.
|
||||
assert not any(process_registry._proc_alive(p) for p in descendants), "orphaned descendant survived kill"
|
||||
evt = process_registry.completion_queue.get(timeout=5)
|
||||
assert evt["session_id"] == r["session_id"]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user