From 3ea0089efbc1dc375a0da6bcdbd826e572127fca Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Wed, 16 Sep 2026 22:52:27 -0700 Subject: [PATCH] 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. --- tests/tools/test_terminal_yield_to_background.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/tools/test_terminal_yield_to_background.py b/tests/tools/test_terminal_yield_to_background.py index 1dec580af7..8ab242cb1c 100644 --- a/tests/tools/test_terminal_yield_to_background.py +++ b/tests/tools/test_terminal_yield_to_background.py @@ -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"]