personal memory agent
0
fork

Configure Feed

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

Refactor agent tests and move integration tests

- Add mock_all_backends() helper to reduce test fragility
- Remove tests that depend on default backend behavior
- Make backend explicit in test requests
- Move test_apps.py to tests/integration/ (requires real server)
- Add .full-installed to gitignore

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+26 -149
+1
.gitignore
··· 16 16 *.log 17 17 config*.py 18 18 .installed 19 + .full-installed
+24 -148
tests/test_agents_ndjson.py
··· 7 7 import json 8 8 import sys 9 9 from io import StringIO 10 - from unittest.mock import AsyncMock, MagicMock, patch 10 + from unittest.mock import MagicMock, patch 11 11 12 12 import pytest 13 13 ··· 28 28 29 29 async def mock_run_agent(config, on_event=None): 30 30 """Mock run_agent function for testing.""" 31 - # Extract values from config 32 31 prompt = config.get("prompt", "") 33 - backend = config.get("backend", "openai") 32 + backend = config.get("backend", "") 34 33 model = config.get("model", "") 35 34 persona = config.get("persona", "default") 36 35 37 - # Emit events through the callback if provided 38 36 if on_event: 39 37 on_event( 40 38 { ··· 56 54 return f"Response to: {prompt}" 57 55 58 56 57 + def mock_all_backends(monkeypatch): 58 + """Mock all backend modules uniformly with mock_run_agent. 59 + 60 + This ensures tests are not fragile to changes in default backend. 61 + """ 62 + for backend_name in ("openai", "anthropic", "google", "claude"): 63 + mock_module = MagicMock() 64 + mock_module.run_agent = mock_run_agent 65 + monkeypatch.setitem(sys.modules, f"muse.{backend_name}", mock_module) 66 + 67 + monkeypatch.setitem(sys.modules, "agents", MagicMock()) 68 + 69 + 59 70 def test_ndjson_single_request(mock_journal, monkeypatch, capsys): 60 71 """Test processing a single NDJSON request from stdin.""" 61 - # Mock stdin with NDJSON data 62 72 ndjson_input = json.dumps( 63 73 { 64 74 "prompt": "What is 2+2?", ··· 72 82 73 83 monkeypatch.setattr("sys.stdin", StringIO(ndjson_input)) 74 84 75 - # Mock argparse results 76 85 mock_args = MagicMock() 77 86 mock_args.verbose = False 78 87 79 - # Create mock backend modules 80 - mock_openai = MagicMock() 81 - mock_openai.run_agent = mock_run_agent 88 + mock_all_backends(monkeypatch) 82 89 83 - # Mock the modules in sys.modules before import 84 - monkeypatch.setitem(sys.modules, "muse.openai", mock_openai) 85 - monkeypatch.setitem(sys.modules, "muse.anthropic", MagicMock()) 86 - monkeypatch.setitem(sys.modules, "muse.google", MagicMock()) 87 - monkeypatch.setitem(sys.modules, "muse.claude", MagicMock()) 88 - 89 - # Mock agents module to prevent actual imports 90 - monkeypatch.setitem(sys.modules, "agents", MagicMock()) 91 - 92 - # Now import after mocks are in place 93 90 from muse.agents import main_async 94 91 95 92 with patch("muse.agents.setup_cli", return_value=mock_args): 96 93 with patch("agents.set_default_openai_key"): 97 94 asyncio.run(main_async()) 98 95 99 - # Check output events 100 96 captured = capsys.readouterr() 101 97 lines = captured.out.strip().split("\n") 102 98 103 99 events = [json.loads(line) for line in lines if line] 104 100 105 - # Should have start and finish events 106 101 assert events 107 102 108 103 start_event = events[0] 109 104 assert start_event["event"] == "start" 110 105 assert start_event["prompt"] == "What is 2+2?" 111 106 assert start_event["backend"] == "openai" 112 - assert start_event["model"] == GPT_5 # Model comes from config 107 + assert start_event["model"] == GPT_5 113 108 114 109 finish_events = [e for e in events if e["event"] == "finish"] 115 110 assert finish_events ··· 117 112 118 113 def test_ndjson_multiple_requests(mock_journal, monkeypatch, capsys): 119 114 """Test processing multiple NDJSON requests from stdin.""" 120 - # Multiple NDJSON lines 121 115 requests = [ 122 116 { 123 117 "prompt": "First question", ··· 132 126 }, 133 127 { 134 128 "prompt": "Third question", 129 + "backend": "google", 135 130 "persona": "technical", 136 131 "mcp_server_url": "http://localhost:5175/mcp", 137 132 }, ··· 144 139 mock_args = MagicMock() 145 140 mock_args.verbose = False 146 141 147 - # Create mock backend modules 148 - mock_openai = MagicMock() 149 - mock_openai.run_agent = mock_run_agent 150 - mock_anthropic = MagicMock() 151 - mock_anthropic.run_agent = mock_run_agent 152 - 153 - # Mock the modules in sys.modules before import 154 - monkeypatch.setitem(sys.modules, "muse.openai", mock_openai) 155 - monkeypatch.setitem(sys.modules, "muse.anthropic", mock_anthropic) 156 - monkeypatch.setitem(sys.modules, "muse.google", MagicMock()) 157 - monkeypatch.setitem(sys.modules, "muse.claude", MagicMock()) 158 - monkeypatch.setitem(sys.modules, "agents", MagicMock()) 142 + mock_all_backends(monkeypatch) 159 143 160 144 from muse.agents import main_async 161 145 ··· 166 150 captured = capsys.readouterr() 167 151 lines = [line for line in captured.out.strip().split("\n") if line] 168 152 169 - # Should have 2 events per request (start + finish) 170 153 assert len(lines) >= 6 171 154 172 - # Verify each request was processed 173 155 events = [json.loads(line) for line in lines] 174 156 start_events = [e for e in events if e["event"] == "start"] 175 157 ··· 183 165 184 166 def test_ndjson_invalid_json(mock_journal, monkeypatch, capsys): 185 167 """Test handling of invalid JSON in NDJSON input.""" 186 - # Mix of valid and invalid JSON 187 168 ndjson_input = """{"prompt": "Valid request", "backend": "openai", "mcp_server_url": "http://localhost:5175/mcp"} 188 169 not valid json 189 170 {"prompt": "Another valid request", "backend": "openai", "mcp_server_url": "http://localhost:5175/mcp"}""" ··· 193 174 mock_args = MagicMock() 194 175 mock_args.verbose = False 195 176 196 - # Create mock backend modules 197 - mock_openai = MagicMock() 198 - mock_openai.run_agent = mock_run_agent 199 - 200 - # Mock the modules in sys.modules before import 201 - monkeypatch.setitem(sys.modules, "muse.openai", mock_openai) 202 - monkeypatch.setitem(sys.modules, "muse.anthropic", MagicMock()) 203 - monkeypatch.setitem(sys.modules, "muse.google", MagicMock()) 204 - monkeypatch.setitem(sys.modules, "muse.claude", MagicMock()) 205 - monkeypatch.setitem(sys.modules, "agents", MagicMock()) 177 + mock_all_backends(monkeypatch) 206 178 207 179 from muse.agents import main_async 208 180 ··· 215 187 216 188 events = [json.loads(line) for line in lines] 217 189 218 - # Should have processed valid requests and reported error for invalid 219 190 error_events = [e for e in events if e["event"] == "error"] 220 191 assert len(error_events) == 1 221 192 assert "Invalid JSON" in error_events[0]["error"] 222 193 223 - # Valid requests should still be processed 224 194 start_events = [e for e in events if e["event"] == "start"] 225 195 assert len(start_events) == 2 226 196 ··· 231 201 { 232 202 "backend": "openai", 233 203 "model": GPT_5, 234 - # Missing 'prompt' field 235 204 } 236 205 ) 237 206 ··· 240 209 mock_args = MagicMock() 241 210 mock_args.verbose = False 242 211 243 - # Mock the modules in sys.modules before import 244 - monkeypatch.setitem(sys.modules, "muse.openai", MagicMock()) 245 - monkeypatch.setitem(sys.modules, "muse.anthropic", MagicMock()) 246 - monkeypatch.setitem(sys.modules, "muse.google", MagicMock()) 247 - monkeypatch.setitem(sys.modules, "muse.claude", MagicMock()) 248 - monkeypatch.setitem(sys.modules, "agents", MagicMock()) 212 + mock_all_backends(monkeypatch) 249 213 250 214 from muse.agents import main_async 251 215 ··· 256 220 captured = capsys.readouterr() 257 221 lines = [line for line in captured.out.strip().split("\n") if line] 258 222 259 - # Should have an error event for missing prompt 260 223 assert len(lines) >= 1 261 224 error_event = json.loads(lines[0]) 262 225 assert error_event["event"] == "error" ··· 265 228 266 229 def test_ndjson_empty_lines(mock_journal, monkeypatch, capsys): 267 230 """Test that empty lines in NDJSON input are ignored.""" 268 - ndjson_input = """{"prompt": "First"} 231 + ndjson_input = """{"prompt": "First", "backend": "openai"} 269 232 270 - {"prompt": "Second"} 233 + {"prompt": "Second", "backend": "openai"} 271 234 272 235 """ 273 236 ··· 276 239 mock_args = MagicMock() 277 240 mock_args.verbose = False 278 241 279 - # Create mock backend modules 280 - mock_openai = MagicMock() 281 - mock_openai.run_agent = mock_run_agent 282 - 283 - # Mock the modules in sys.modules before import 284 - monkeypatch.setitem(sys.modules, "muse.openai", mock_openai) 285 - monkeypatch.setitem(sys.modules, "muse.anthropic", MagicMock()) 286 - monkeypatch.setitem(sys.modules, "muse.google", MagicMock()) 287 - monkeypatch.setitem(sys.modules, "muse.claude", MagicMock()) 288 - monkeypatch.setitem(sys.modules, "agents", MagicMock()) 242 + mock_all_backends(monkeypatch) 289 243 290 244 from muse.agents import main_async 291 245 ··· 299 253 events = [json.loads(line) for line in lines] 300 254 start_events = [e for e in events if e["event"] == "start"] 301 255 302 - # Should process both requests, ignoring empty lines 303 256 assert len(start_events) == 2 304 - 305 - 306 - def test_default_values(mock_journal, monkeypatch, capsys): 307 - """Test that default values are applied when not specified.""" 308 - # Minimal request with only prompt 309 - ndjson_input = json.dumps({"prompt": "Test prompt"}) 310 - 311 - monkeypatch.setattr("sys.stdin", StringIO(ndjson_input)) 312 - 313 - mock_args = MagicMock() 314 - mock_args.verbose = False 315 - 316 - # Create mock backend modules 317 - mock_openai = MagicMock() 318 - mock_openai.run_agent = mock_run_agent 319 - 320 - # Mock the modules in sys.modules before import 321 - monkeypatch.setitem(sys.modules, "muse.openai", mock_openai) 322 - monkeypatch.setitem(sys.modules, "muse.anthropic", MagicMock()) 323 - monkeypatch.setitem(sys.modules, "muse.google", MagicMock()) 324 - monkeypatch.setitem(sys.modules, "muse.claude", MagicMock()) 325 - monkeypatch.setitem(sys.modules, "agents", MagicMock()) 326 - 327 - from muse.agents import main_async 328 - 329 - with patch("muse.agents.setup_cli", return_value=mock_args): 330 - with patch("agents.set_default_openai_key"): 331 - asyncio.run(main_async()) 332 - 333 - captured = capsys.readouterr() 334 - lines = captured.out.strip().split("\n") 335 - 336 - start_event = json.loads(lines[0]) 337 - assert start_event["event"] == "start" 338 - assert start_event["prompt"] == "Test prompt" 339 - assert start_event["backend"] == "openai" # Default backend 340 - assert start_event["persona"] == "default" # Default persona 341 - 342 - 343 - def test_openai_key_setting(mock_journal, monkeypatch, capsys): 344 - """Test that OpenAI API key is set when backend is openai.""" 345 - monkeypatch.setenv("OPENAI_API_KEY", "test-key-123") 346 - 347 - ndjson_input = json.dumps( 348 - { 349 - "prompt": "Test", 350 - "backend": "openai", 351 - "mcp_server_url": "http://localhost:5175/mcp", 352 - } 353 - ) 354 - 355 - monkeypatch.setattr("sys.stdin", StringIO(ndjson_input)) 356 - 357 - mock_args = MagicMock() 358 - mock_args.verbose = False 359 - 360 - # Create mock backend modules 361 - mock_openai = MagicMock() 362 - mock_openai.run_agent = mock_run_agent 363 - 364 - # Mock the modules in sys.modules before import 365 - monkeypatch.setitem(sys.modules, "muse.openai", mock_openai) 366 - monkeypatch.setitem(sys.modules, "muse.anthropic", MagicMock()) 367 - monkeypatch.setitem(sys.modules, "muse.google", MagicMock()) 368 - monkeypatch.setitem(sys.modules, "muse.claude", MagicMock()) 369 - monkeypatch.setitem(sys.modules, "agents", MagicMock()) 370 - 371 - from muse.agents import main_async 372 - 373 - mock_set_key = MagicMock() 374 - 375 - with patch("muse.agents.setup_cli", return_value=mock_args): 376 - with patch("agents.set_default_openai_key", mock_set_key): 377 - asyncio.run(main_async()) 378 - 379 - # Verify set_default_openai_key was called with the API key 380 - mock_set_key.assert_called_with("test-key-123")
+1 -1
tests/test_apps.py tests/integration/test_apps.py
··· 27 27 28 28 Returns app names for all apps with workspace.html. 29 29 """ 30 - project_root = Path(__file__).parent.parent 30 + project_root = Path(__file__).parent.parent.parent 31 31 apps_dir = project_root / "apps" 32 32 33 33 app_names = []