personal memory agent
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)]