personal memory agent
1# SPDX-License-Identifier: AGPL-3.0-only
2# Copyright (c) 2026 sol pbc
3
4from __future__ import annotations
5
6import os
7import shutil
8import stat
9import subprocess
10from pathlib import Path
11
12import pytest
13
14pytestmark = pytest.mark.skipif(os.name != "posix", reason="POSIX launcher tests")
15
16REPO_ROOT = Path(__file__).resolve().parents[1]
17LAUNCHER_DIR = REPO_ROOT / "scripts" / "root-launchers"
18REINSTALL = "Reinstall solstone and solstone-core."
19
20
21def _make_executable(path: Path) -> None:
22 path.chmod(path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
23
24
25def _copy_launcher(bin_dir: Path, name: str = "sol") -> Path:
26 bin_dir.mkdir(parents=True, exist_ok=True)
27 launcher = bin_dir / name
28 shutil.copy2(LAUNCHER_DIR / name, launcher)
29 _make_executable(launcher)
30 return launcher
31
32
33def _write_core(bin_dir: Path, *, executable: bool = True) -> Path:
34 core = bin_dir / "solstone-core"
35 core.write_text(
36 "#!/bin/sh\n"
37 "printf 'cwd=%s\\n' \"$(pwd)\"\n"
38 "printf 'argv='\n"
39 'for arg in "$@"; do printf \'<%s>\' "$arg"; done\n'
40 "printf '\\nstdin='\n"
41 "cat\n"
42 "printf 'stub stderr\\n' >&2\n"
43 'exit "${STUB_EXIT:-23}"\n',
44 encoding="utf-8",
45 )
46 if executable:
47 _make_executable(core)
48 else:
49 core.chmod(0o644)
50 return core
51
52
53def _run(
54 program: Path,
55 args: list[str | bytes] | None = None,
56 *,
57 cwd: Path | None = None,
58 stdin: bytes = b"payload",
59 env: dict[str, str] | None = None,
60) -> subprocess.CompletedProcess[bytes]:
61 return subprocess.run(
62 [program, *(args or [])],
63 input=stdin,
64 stdout=subprocess.PIPE,
65 stderr=subprocess.PIPE,
66 cwd=cwd,
67 env=env,
68 check=False,
69 )
70
71
72def _run_with_argv0(
73 launcher_source: Path,
74 argv0: Path,
75 *,
76 cwd: Path | None = None,
77) -> subprocess.CompletedProcess[bytes]:
78 return subprocess.run(
79 ["/bin/sh", "-c", '. "$1"', argv0, launcher_source],
80 stdout=subprocess.PIPE,
81 stderr=subprocess.PIPE,
82 cwd=cwd,
83 check=False,
84 )
85
86
87def _arg_bytes(arg: str | bytes) -> bytes:
88 if isinstance(arg, bytes):
89 return arg
90 return os.fsencode(arg)
91
92
93def _stdout(cwd: Path, args: list[str | bytes], stdin: bytes = b"payload") -> bytes:
94 argv = b"".join(b"<" + _arg_bytes(arg) + b">" for arg in args)
95 return f"cwd={cwd}\n".encode() + b"argv=" + argv + b"\nstdin=" + stdin
96
97
98def _error(prefix: str, detail: str) -> bytes:
99 return f"{prefix}: native launcher {detail}. {REINSTALL}\n".encode()
100
101
102def _missing_core_error(prefix: str, core: Path) -> bytes:
103 return (
104 f"{prefix}: native solstone-core sibling is missing: {core}. {REINSTALL}\n"
105 ).encode()
106
107
108def _non_executable_core_error(prefix: str, core: Path) -> bytes:
109 return (
110 f"{prefix}: native solstone-core sibling is not executable: {core}. "
111 f"{REINSTALL}\n"
112 ).encode()
113
114
115def test_launcher_files_differ_only_by_identity_line() -> None:
116 sol = (LAUNCHER_DIR / "sol").read_bytes().splitlines()
117 solstone = (LAUNCHER_DIR / "solstone").read_bytes().splitlines()
118
119 diffs = [
120 (index, left, right)
121 for index, (left, right) in enumerate(zip(sol, solstone), start=1)
122 if left != right
123 ]
124
125 assert len(sol) == len(solstone)
126 assert diffs == [(4, b"id=sol", b"id=solstone")]
127
128
129def test_direct_invocation_preserves_process_contract(tmp_path: Path) -> None:
130 bin_dir = tmp_path / "bin"
131 launcher = _copy_launcher(bin_dir)
132 _write_core(bin_dir)
133 cwd = tmp_path / "cwd"
134 cwd.mkdir()
135 args: list[str | bytes] = ["one", "two words", b"raw-\xff"]
136
137 result = _run(launcher, args, cwd=cwd)
138
139 assert result.returncode == 23
140 assert result.stdout == _stdout(
141 cwd,
142 ["__solstone_identity=sol", *args],
143 )
144 assert result.stderr == b"stub stderr\n"
145
146
147def test_solstone_launcher_uses_solstone_identity(tmp_path: Path) -> None:
148 bin_dir = tmp_path / "bin"
149 launcher = _copy_launcher(bin_dir, "solstone")
150 _write_core(bin_dir)
151
152 result = _run(launcher, ["status"], cwd=tmp_path)
153
154 assert result.returncode == 23
155 assert result.stdout == _stdout(
156 tmp_path,
157 ["__solstone_identity=solstone", "status"],
158 )
159 assert result.stderr == b"stub stderr\n"
160
161
162def test_exit_status_is_propagated(tmp_path: Path) -> None:
163 bin_dir = tmp_path / "bin"
164 launcher = _copy_launcher(bin_dir)
165 _write_core(bin_dir)
166 env = dict(os.environ)
167 env["STUB_EXIT"] = "42"
168
169 result = _run(launcher, cwd=tmp_path, env=env)
170
171 assert result.returncode == 42
172 assert result.stdout == _stdout(tmp_path, ["__solstone_identity=sol"])
173 assert result.stderr == b"stub stderr\n"
174
175
176def test_absolute_multi_hop_symlink_resolves_to_launcher_dir(tmp_path: Path) -> None:
177 bin_dir = tmp_path / "bin"
178 launcher = _copy_launcher(bin_dir)
179 _write_core(bin_dir)
180 links = tmp_path / "links"
181 links.mkdir()
182 second = links / "second"
183 first = links / "first"
184 second.symlink_to(launcher)
185 first.symlink_to(second)
186
187 result = _run(first, ["ok"], cwd=tmp_path)
188
189 assert result.returncode == 23
190 assert result.stdout == _stdout(tmp_path, ["__solstone_identity=sol", "ok"])
191 assert result.stderr == b"stub stderr\n"
192
193
194def test_relative_multi_hop_symlink_resolves_to_launcher_dir(tmp_path: Path) -> None:
195 bin_dir = tmp_path / "bin"
196 launcher = _copy_launcher(bin_dir)
197 _write_core(bin_dir)
198 rel1 = tmp_path / "rel1"
199 rel2 = tmp_path / "rel2"
200 rel1.mkdir()
201 rel2.mkdir()
202 second = rel2 / "second"
203 first = rel1 / "first"
204 second.symlink_to(Path("..") / "bin" / launcher.name)
205 first.symlink_to(Path("..") / "rel2" / second.name)
206
207 result = _run(first, ["ok"], cwd=tmp_path)
208
209 assert result.returncode == 23
210 assert result.stdout == _stdout(tmp_path, ["__solstone_identity=sol", "ok"])
211 assert result.stderr == b"stub stderr\n"
212
213
214def test_path_containing_space_resolves(tmp_path: Path) -> None:
215 bin_dir = tmp_path / "path with space" / "bin"
216 launcher = _copy_launcher(bin_dir)
217 _write_core(bin_dir)
218
219 result = _run(launcher, ["ok"], cwd=tmp_path)
220
221 assert result.returncode == 23
222 assert result.stdout == _stdout(tmp_path, ["__solstone_identity=sol", "ok"])
223 assert result.stderr == b"stub stderr\n"
224
225
226def test_leading_dash_path_component_resolves(tmp_path: Path) -> None:
227 bin_dir = tmp_path / "-leading-dash" / "bin"
228 launcher = _copy_launcher(bin_dir)
229 _write_core(bin_dir)
230
231 result = _run(launcher, ["ok"], cwd=tmp_path)
232
233 assert result.returncode == 23
234 assert result.stdout == _stdout(tmp_path, ["__solstone_identity=sol", "ok"])
235 assert result.stderr == b"stub stderr\n"
236
237
238def test_dangling_link_exits_78_without_stdout(tmp_path: Path) -> None:
239 launcher_source = _copy_launcher(tmp_path / "source-bin")
240 bin_dir = tmp_path / "bin"
241 missing_dir = bin_dir / "missing"
242 missing_dir.mkdir(parents=True)
243 launcher = bin_dir / "sol"
244 launcher.symlink_to(Path("missing") / "sol")
245 dangling_target = bin_dir / "missing" / "sol"
246
247 result = _run_with_argv0(launcher_source, launcher, cwd=tmp_path)
248
249 assert result.returncode == 78
250 assert result.stdout == b""
251 assert result.stderr == _error(
252 "sol",
253 f"symlink is dangling: {dangling_target}",
254 )
255
256
257def test_link_cycle_exits_78_without_stdout(tmp_path: Path) -> None:
258 launcher_source = _copy_launcher(tmp_path / "source-bin")
259 first = tmp_path / "first"
260 second = tmp_path / "second"
261 first.symlink_to(second.name)
262 second.symlink_to(first.name)
263
264 result = _run_with_argv0(launcher_source, first, cwd=tmp_path)
265
266 assert result.returncode == 78
267 assert result.stdout == b""
268 assert result.stderr == _error("sol", "symlink cycle detected")
269
270
271def test_missing_sibling_core_exits_78_without_stdout_and_brands_command(
272 tmp_path: Path,
273) -> None:
274 bin_dir = tmp_path / "bin"
275 launcher = _copy_launcher(bin_dir, "solstone")
276 core = bin_dir / "solstone-core"
277
278 result = _run(launcher, cwd=tmp_path)
279
280 assert result.returncode == 78
281 assert result.stdout == b""
282 assert result.stderr == _missing_core_error("solstone", core)
283
284
285def test_non_executable_sibling_core_exits_78_without_stdout(tmp_path: Path) -> None:
286 bin_dir = tmp_path / "bin"
287 launcher = _copy_launcher(bin_dir)
288 core = _write_core(bin_dir, executable=False)
289
290 result = _run(launcher, cwd=tmp_path)
291
292 assert result.returncode == 78
293 assert result.stdout == b""
294 assert result.stderr == _non_executable_core_error("sol", core)