From 08694d18b84ef8941ae5b9289ac8110ba963e1fd Mon Sep 17 00:00:00 2001 From: ciregenz Date: Fri, 7 Aug 2026 21:00:44 -0700 Subject: [PATCH] [eric] apps: the ghost reaper matches workspace paths case-insensitively, so a packaged app's openswarm vs OpenSwarm casing no longer spares orphans --- backend/apps/outputs/reap_ghost_runtimes.py | 7 +++++-- backend/tests/test_reap_ghost_runtimes.py | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/backend/apps/outputs/reap_ghost_runtimes.py b/backend/apps/outputs/reap_ghost_runtimes.py index 62485fd2..6391aa1f 100644 --- a/backend/apps/outputs/reap_ghost_runtimes.py +++ b/backend/apps/outputs/reap_ghost_runtimes.py @@ -64,7 +64,10 @@ def find_ghost_runtime_pids() -> List[int]: then filtered by walking each candidate's ancestry: if a live backend is anywhere above it, it is someone's working app and is left alone. """ - needle = os.path.abspath(WORKSPACE_DIR) + # Case-FOLDED needle. macOS's default filesystem is case-insensitive, so a process may report + # `.../openswarm/...` while our resolved path is `.../OpenSwarm/...`: the same folder, but a + # case-sensitive `in` check misses it and the ghost survives (found live on a packaged smoke). + needle = os.path.abspath(WORKSPACE_DIR).casefold() try: out = subprocess.run(["ps", "-eo", "pid=,args="], capture_output=True, text=True, timeout=8) except Exception: @@ -80,7 +83,7 @@ def find_ghost_runtime_pids() -> List[int]: ghosts: List[int] = [] for line in (out.stdout or "").splitlines(): line = line.strip() - if needle not in line: + if needle not in line.casefold(): continue head = line.split(None, 1) if not head or not head[0].isdigit(): diff --git a/backend/tests/test_reap_ghost_runtimes.py b/backend/tests/test_reap_ghost_runtimes.py index bdf5ce22..4def1293 100644 --- a/backend/tests/test_reap_ghost_runtimes.py +++ b/backend/tests/test_reap_ghost_runtimes.py @@ -118,3 +118,18 @@ def test_indeterminate_ancestry_is_never_a_ghost(monkeypatch): ghosts = rg.find_ghost_runtime_pids() assert 999 not in ghosts, "pid absent from the ppid map was treated as a ghost" assert ghosts == [300], "a genuinely orphaned pid (walks to init, no backend) still reaps" + + +def test_ghost_matched_despite_path_case_difference(monkeypatch): + """macOS is case-insensitive, so a process can report .../openswarm/... while our resolved path + is .../OpenSwarm/... (same folder). A case-sensitive match missed the ghost entirely; found live + on a packaged smoke where 8 orphans survived a reap that logged 0.""" + from backend.apps.outputs import reap_ghost_runtimes as rg + ws = rg.os.path.abspath(rg.WORKSPACE_DIR) + lower_ws = ws.replace("OpenSwarm", "openswarm").replace("Openswarm", "openswarm") + monkeypatch.setattr(rg, "p_live_backend_pids", lambda: {50}) + monkeypatch.setattr(rg, "p_ppid_map", lambda: {700: 1}) # orphan reparented to init + class P_Out: + stdout = f"50 python -m uvicorn backend.main:app\n700 bash {lower_ws}/ws-x/backend/run.sh\n" + monkeypatch.setattr(rg.subprocess, "run", lambda *a, **k: P_Out()) + assert rg.find_ghost_runtime_pids() == [700], "a case-different path must still match the ghost"