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:
teknium1
2026-09-16 22:52:27 -07:00
committed by Teknium
parent 019a34346c
commit 3ea0089efb

View File

@@ -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"]