personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

solstone / tests / test_supervisor_orphan_sweep.py
7.0 kB 193 lines
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3 4import signal 5from pathlib import Path 6 7import pytest 8 9from solstone.think import supervisor 10from solstone.think.providers.mlx_server import MLX_SERVER_PROCESS_NAME 11 12TEST_JOURNAL = Path("/journal/test") 13 14 15class _FakeProcess: 16 def __init__( 17 self, 18 *, 19 pid: int, 20 name: str = "journal:sense", 21 ppid: int = 1, 22 username: str = "jer", 23 name_error: Exception | None = None, 24 ppid_error: Exception | None = None, 25 username_error: Exception | None = None, 26 ): 27 self.pid = pid 28 self._name = name 29 self._ppid = ppid 30 self._username = username 31 self._name_error = name_error 32 self._ppid_error = ppid_error 33 self._username_error = username_error 34 35 def name(self) -> str: 36 if self._name_error: 37 raise self._name_error 38 return self._name 39 40 def ppid(self) -> int: 41 if self._ppid_error: 42 raise self._ppid_error 43 return self._ppid 44 45 def username(self) -> str: 46 if self._username_error: 47 raise self._username_error 48 return self._username 49 50 51class TestOrphanSweep: 52 def _patch_common(self, monkeypatch, procs): 53 kills = [] 54 monkeypatch.setattr(supervisor.sys, "platform", "linux") 55 monkeypatch.setattr(supervisor.getpass, "getuser", lambda: "jer") 56 monkeypatch.setattr(supervisor.psutil, "process_iter", lambda _attrs: procs) 57 monkeypatch.setattr(supervisor, "_candidate_journal", lambda proc: TEST_JOURNAL) 58 monkeypatch.setattr( 59 supervisor.os, "kill", lambda pid, sig: kills.append((pid, sig)) 60 ) 61 return kills 62 63 @pytest.mark.parametrize( 64 "proctitle", 65 [ 66 "journal:sense", 67 "journal:cortex", 68 "journal:convey", 69 "journal:spl", 70 "journal:think", 71 "journal:heartbeat", 72 "journal:identity", 73 "journal:providers", 74 "journal:facet-candidates", 75 "llama-server", 76 MLX_SERVER_PROCESS_NAME, 77 ], 78 ) 79 def test_sweepable_orphan_proctitles_are_sigtermed(self, monkeypatch, proctitle): 80 procs = [_FakeProcess(pid=111, name=proctitle)] 81 kills = self._patch_common(monkeypatch, procs) 82 monkeypatch.setattr(supervisor.psutil, "pid_exists", lambda _pid: False) 83 84 assert supervisor._sweep_orphaned_sol_processes(journal=TEST_JOURNAL) == 1 85 assert kills == [(111, signal.SIGTERM)] 86 87 @pytest.mark.parametrize( 88 "proctitle", 89 ["sol:call", "solstone:convey", "journal", "python"], 90 ) 91 def test_non_sweepable_orphan_proctitles_are_ignored(self, monkeypatch, proctitle): 92 procs = [_FakeProcess(pid=111, name=proctitle)] 93 kills = self._patch_common(monkeypatch, procs) 94 95 assert supervisor._sweep_orphaned_sol_processes(journal=TEST_JOURNAL) == 0 96 assert kills == [] 97 98 def test_non_matching_processes_are_ignored(self, monkeypatch): 99 monkeypatch.setattr(supervisor.os, "getpid", lambda: 555) 100 procs = [ 101 _FakeProcess(pid=111, username="other"), 102 _FakeProcess(pid=112, name="llama-server", username="other"), 103 _FakeProcess(pid=222, ppid=2), 104 _FakeProcess(pid=223, name="llama-server", ppid=2), 105 _FakeProcess(pid=333, name="python"), 106 _FakeProcess(pid=444, name="solstone:convey"), 107 _FakeProcess(pid=555), 108 ] 109 kills = self._patch_common(monkeypatch, procs) 110 111 assert supervisor._sweep_orphaned_sol_processes(journal=TEST_JOURNAL) == 0 112 assert kills == [] 113 114 def test_survivors_after_grace_are_sigkilled(self, monkeypatch): 115 procs = [_FakeProcess(pid=111), _FakeProcess(pid=222)] 116 kills = self._patch_common(monkeypatch, procs) 117 monkeypatch.setattr(supervisor.psutil, "pid_exists", lambda pid: pid == 222) 118 monkeypatch.setattr(supervisor.time, "sleep", lambda _seconds: None) 119 120 assert ( 121 supervisor._sweep_orphaned_sol_processes( 122 journal=TEST_JOURNAL, 123 grace=0.0, 124 ) 125 == 2 126 ) 127 assert kills == [ 128 (111, signal.SIGTERM), 129 (222, signal.SIGTERM), 130 (222, signal.SIGKILL), 131 ] 132 133 def test_process_access_errors_are_swallowed(self, monkeypatch): 134 procs = [ 135 _FakeProcess(pid=111, name_error=supervisor.psutil.NoSuchProcess(pid=111)), 136 _FakeProcess( 137 pid=222, 138 username_error=supervisor.psutil.AccessDenied(pid=222), 139 ), 140 _FakeProcess(pid=333), 141 ] 142 kills = self._patch_common(monkeypatch, procs) 143 monkeypatch.setattr(supervisor.psutil, "pid_exists", lambda _pid: False) 144 145 assert supervisor._sweep_orphaned_sol_processes(journal=TEST_JOURNAL) == 1 146 assert kills == [(333, signal.SIGTERM)] 147 148 @pytest.mark.parametrize("platform", ["linux", "darwin", "freebsd"]) 149 def test_runs_on_all_platforms(self, monkeypatch, platform): 150 procs = [_FakeProcess(pid=111)] 151 kills = self._patch_common(monkeypatch, procs) 152 monkeypatch.setattr(supervisor.sys, "platform", platform) 153 monkeypatch.setattr(supervisor.psutil, "pid_exists", lambda _pid: False) 154 155 assert supervisor._sweep_orphaned_sol_processes(journal=TEST_JOURNAL) == 1 156 assert kills == [(111, signal.SIGTERM)] 157 158 def test_candidate_in_different_journal_is_skipped(self, monkeypatch): 159 procs = [_FakeProcess(pid=111), _FakeProcess(pid=222, name="llama-server")] 160 kills = self._patch_common(monkeypatch, procs) 161 monkeypatch.setattr( 162 supervisor, 163 "_candidate_journal", 164 lambda proc: Path("/journal/other"), 165 ) 166 167 assert supervisor._sweep_orphaned_sol_processes(journal=TEST_JOURNAL) == 0 168 assert kills == [] 169 170 @pytest.mark.parametrize( 171 "reason", 172 ["access_denied", "missing_key", "malformed_value"], 173 ) 174 def test_unknown_journal_candidate_is_skipped(self, monkeypatch, reason): 175 procs = [_FakeProcess(pid=111)] 176 kills = self._patch_common(monkeypatch, procs) 177 monkeypatch.setattr(supervisor, "_candidate_journal", lambda proc: None) 178 179 assert supervisor._sweep_orphaned_sol_processes(journal=TEST_JOURNAL) == 0 180 assert kills == [] 181 182 def test_same_journal_candidate_is_swept(self, monkeypatch): 183 procs = [_FakeProcess(pid=111)] 184 kills = self._patch_common(monkeypatch, procs) 185 monkeypatch.setattr( 186 supervisor, 187 "_candidate_journal", 188 lambda proc: TEST_JOURNAL, 189 ) 190 monkeypatch.setattr(supervisor.psutil, "pid_exists", lambda _pid: False) 191 192 assert supervisor._sweep_orphaned_sol_processes(journal=TEST_JOURNAL) == 1 193 assert kills == [(111, signal.SIGTERM)]