personal memory agent
at main 302 lines 11 kB view raw
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"