personal memory agent
1# SPDX-License-Identifier: AGPL-3.0-only
2# Copyright (c) 2026 sol pbc
3
4"""Tests for sol.py unified CLI."""
5
6import sys
7from unittest.mock import MagicMock, patch
8
9import pytest
10
11import sol
12
13
14class TestResolveCommand:
15 """Tests for resolve_command() function."""
16
17 def test_resolve_known_command(self):
18 """Test resolving a known command from registry."""
19 module_path, preset_args = sol.resolve_command("import")
20 assert module_path == "think.importers.cli"
21 assert preset_args == []
22
23 def test_resolve_direct_module_path(self):
24 """Test resolving a direct module path with dot."""
25 module_path, preset_args = sol.resolve_command("think.importers.cli")
26 assert module_path == "think.importers.cli"
27 assert preset_args == []
28
29 def test_resolve_nested_module_path(self):
30 """Test resolving a deeply nested module path."""
31 module_path, preset_args = sol.resolve_command("observe.linux.observer")
32 assert module_path == "observe.linux.observer"
33 assert preset_args == []
34
35 def test_resolve_unknown_command_raises(self):
36 """Test that unknown command raises ValueError."""
37 with pytest.raises(ValueError) as exc_info:
38 sol.resolve_command("nonexistent")
39 assert "Unknown command: nonexistent" in str(exc_info.value)
40
41 def test_resolve_alias_with_preset_args(self):
42 """Test resolving an alias that includes preset arguments."""
43 # Add a test alias
44 sol.ALIASES["test-alias"] = ("think.indexer", ["--rescan"])
45 try:
46 module_path, preset_args = sol.resolve_command("test-alias")
47 assert module_path == "think.indexer"
48 assert preset_args == ["--rescan"]
49 finally:
50 del sol.ALIASES["test-alias"]
51
52 def test_alias_takes_precedence_over_command(self):
53 """Test that aliases override commands with same name."""
54 # Add an alias that shadows a command
55 sol.ALIASES["import"] = ("think.cluster", ["--force"])
56 try:
57 module_path, preset_args = sol.resolve_command("import")
58 assert module_path == "think.cluster"
59 assert preset_args == ["--force"]
60 finally:
61 del sol.ALIASES["import"]
62
63
64class TestRunCommand:
65 """Tests for run_command() function."""
66
67 def test_run_command_success(self):
68 """Test running a command that exits cleanly."""
69 mock_module = MagicMock()
70 mock_module.main = MagicMock()
71
72 with patch("importlib.import_module", return_value=mock_module):
73 exit_code = sol.run_command("test.module")
74 assert exit_code == 0
75 mock_module.main.assert_called_once()
76
77 def test_run_command_with_system_exit(self):
78 """Test running a command that calls sys.exit(0)."""
79 mock_module = MagicMock()
80 mock_module.main = MagicMock(side_effect=SystemExit(0))
81
82 with patch("importlib.import_module", return_value=mock_module):
83 exit_code = sol.run_command("test.module")
84 assert exit_code == 0
85
86 def test_run_command_with_nonzero_exit(self):
87 """Test running a command that calls sys.exit(1)."""
88 mock_module = MagicMock()
89 mock_module.main = MagicMock(side_effect=SystemExit(1))
90
91 with patch("importlib.import_module", return_value=mock_module):
92 exit_code = sol.run_command("test.module")
93 assert exit_code == 1
94
95 def test_run_command_with_string_exit(self, capsys):
96 """Test running a command that raises SystemExit with a string message."""
97 mock_module = MagicMock()
98 mock_module.main = MagicMock(side_effect=SystemExit("Error: something failed"))
99
100 with patch("importlib.import_module", return_value=mock_module):
101 exit_code = sol.run_command("test.module")
102 assert exit_code == 1
103
104 captured = capsys.readouterr()
105 assert "Error: something failed" in captured.err
106
107 def test_run_command_import_error(self):
108 """Test handling ImportError for nonexistent module."""
109 with patch(
110 "importlib.import_module", side_effect=ImportError("No module named 'fake'")
111 ):
112 exit_code = sol.run_command("fake.module")
113 assert exit_code == 1
114
115 def test_run_command_no_main_function(self):
116 """Test handling module without main() function."""
117 mock_module = MagicMock(spec=[]) # No 'main' attribute
118
119 with patch("importlib.import_module", return_value=mock_module):
120 exit_code = sol.run_command("test.module")
121 assert exit_code == 1
122
123
124class TestGetStatus:
125 """Tests for get_status() function."""
126
127 def test_status_with_override(self, monkeypatch, tmp_path):
128 """Test status when journal override is set and exists."""
129 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path))
130
131 status = sol.get_status()
132 assert status["journal_path"] == str(tmp_path)
133 assert status["journal_source"] == "override"
134 assert status["journal_exists"] is True
135
136 def test_status_with_nonexistent_journal(self, monkeypatch, tmp_path):
137 """Test status when override points to nonexistent dir."""
138 nonexistent = tmp_path / "nonexistent"
139 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(nonexistent))
140
141 status = sol.get_status()
142 assert status["journal_path"] == str(nonexistent)
143 assert status["journal_source"] == "override"
144 assert status["journal_exists"] is False
145
146 def test_status_without_override(self, monkeypatch):
147 """Test status when no override is set uses project root."""
148 monkeypatch.delenv("_SOLSTONE_JOURNAL_OVERRIDE", raising=False)
149 status = sol.get_status()
150 assert status["journal_path"].endswith("/journal")
151 assert status["journal_source"] == "project"
152 assert isinstance(status["journal_exists"], bool)
153
154
155class TestMain:
156 """Tests for main() function."""
157
158 def test_main_no_args_shows_help(self, monkeypatch, capsys):
159 """Test that running with no args shows help."""
160 monkeypatch.setattr(sys, "argv", ["sol"])
161 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", "/tmp/test")
162
163 sol.main()
164
165 captured = capsys.readouterr()
166 assert "sol - solstone unified CLI" in captured.out
167 assert "Usage: sol <command>" in captured.out
168
169 def test_main_help_flag(self, monkeypatch, capsys):
170 """Test --help flag shows help."""
171 monkeypatch.setattr(sys, "argv", ["sol", "--help"])
172 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", "/tmp/test")
173
174 sol.main()
175
176 captured = capsys.readouterr()
177 assert "sol - solstone unified CLI" in captured.out
178
179 def test_main_help_command_without_question(self, monkeypatch, capsys):
180 """Test bare 'help' command shows static help."""
181 monkeypatch.setattr(sys, "argv", ["sol", "help"])
182 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", "/tmp/test")
183
184 sol.main()
185
186 captured = capsys.readouterr()
187 assert "sol - solstone unified CLI" in captured.out
188
189 def test_main_version_flag(self, monkeypatch, capsys):
190 """Test --version flag shows version."""
191 monkeypatch.setattr(sys, "argv", ["sol", "--version"])
192
193 sol.main()
194
195 captured = capsys.readouterr()
196 assert "sol (solstone)" in captured.out
197
198 def test_main_path_flag(self, monkeypatch, capsys):
199 """Test --path flag prints resolved journal path."""
200 monkeypatch.setattr(sys, "argv", ["sol", "--path"])
201 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", "/tmp/test-journal")
202
203 sol.main()
204
205 captured = capsys.readouterr()
206 assert captured.out.strip() == "/tmp/test-journal"
207
208 def test_main_path_flag_default(self, monkeypatch, capsys):
209 """Test --path prints project root journal when no override set."""
210 monkeypatch.setattr(sys, "argv", ["sol", "--path"])
211 monkeypatch.delenv("_SOLSTONE_JOURNAL_OVERRIDE", raising=False)
212 sol.main()
213
214 captured = capsys.readouterr()
215 path = captured.out.strip()
216 assert path != ""
217 assert path.endswith("/journal")
218
219 def test_main_root_command(self, monkeypatch, capsys):
220 """Test 'root' command prints the project root directory."""
221 monkeypatch.setattr(sys, "argv", ["sol", "root"])
222
223 sol.main()
224
225 captured = capsys.readouterr()
226 path = captured.out.strip()
227 assert path != ""
228 # root should NOT end with /journal — that's --path
229 assert not path.endswith("/journal")
230 # should be a parent of the journal path
231 assert path.endswith("/solstone") or "/solstone" in path
232
233 def test_main_unknown_command_exits(self, monkeypatch):
234 """Test that unknown command exits with code 1."""
235 monkeypatch.setattr(sys, "argv", ["sol", "unknown-command"])
236
237 with pytest.raises(SystemExit) as exc_info:
238 sol.main()
239 assert exc_info.value.code == 1
240
241 def test_main_adjusts_sys_argv(self, monkeypatch):
242 """Test that sys.argv is adjusted for subcommand."""
243 monkeypatch.setattr(sys, "argv", ["sol", "import", "--day", "20250101"])
244
245 captured_argv = []
246
247 def mock_main():
248 captured_argv.extend(sys.argv)
249
250 mock_module = MagicMock()
251 mock_module.main = mock_main
252
253 with patch("importlib.import_module", return_value=mock_module):
254 with pytest.raises(SystemExit):
255 sol.main()
256
257 assert captured_argv[0] == "sol import"
258 assert "--day" in captured_argv
259 assert "20250101" in captured_argv
260
261 def test_main_help_command_with_question_dispatches(self, monkeypatch):
262 """Test 'help' with extra args dispatches to help module."""
263 monkeypatch.setattr(sys, "argv", ["sol", "help", "how", "do", "I", "search"])
264
265 captured_argv = []
266
267 def mock_main():
268 captured_argv.extend(sys.argv)
269
270 mock_module = MagicMock()
271 mock_module.main = mock_main
272
273 with patch("importlib.import_module", return_value=mock_module):
274 with pytest.raises(SystemExit):
275 sol.main()
276
277 assert captured_argv[0] == "sol help"
278 assert "how" in captured_argv
279 assert "search" in captured_argv
280
281
282class TestCommandRegistry:
283 """Tests for command registry completeness."""
284
285 def test_all_commands_have_modules(self):
286 """Test that all registered commands point to valid module paths."""
287 for cmd, module_path in sol.COMMANDS.items():
288 assert "." in module_path, f"Command '{cmd}' has invalid module path"
289
290 def test_groups_contain_valid_commands(self):
291 """Test that all commands in groups exist in registry."""
292 for group_name, commands in sol.GROUPS.items():
293 for cmd in commands:
294 assert cmd in sol.COMMANDS, (
295 f"Command '{cmd}' in group '{group_name}' not in registry"
296 )
297
298 def test_critical_commands_registered(self):
299 """Test that critical commands are registered."""
300 critical = ["import", "agents", "dream", "indexer", "transcribe", "help"]
301 for cmd in critical:
302 assert cmd in sol.COMMANDS, f"Critical command '{cmd}' not registered"