personal memory agent
at main 388 lines 12 kB view raw
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3 4"""Tests for ObserverClient with mocked HTTP calls.""" 5 6from __future__ import annotations 7 8import json 9from unittest.mock import MagicMock, patch 10 11import pytest 12import requests 13 14 15@pytest.fixture 16def mock_session(): 17 with patch("observe.observer_client.requests.Session") as mock: 18 session = MagicMock() 19 mock.return_value = session 20 yield session 21 22 23@pytest.fixture 24def mock_config(): 25 with ( 26 patch("observe.observer_client.get_config") as mock, 27 patch("observe.observer_client.read_service_port") as mock_port, 28 ): 29 mock.return_value = {} 30 mock_port.return_value = 8000 31 yield mock 32 33 34@pytest.fixture 35def mock_journal(tmp_path): 36 with patch("observe.observer_client.get_journal") as mock: 37 mock.return_value = str(tmp_path) 38 yield tmp_path 39 40 41def test_observer_client_init(mock_session, mock_config): 42 from observe.observer_client import ObserverClient 43 44 client = ObserverClient("main-stream") 45 46 assert client._url == "http://localhost:8000" 47 assert client._key is None 48 assert client._name == "main-stream" 49 assert client._stream == "main-stream" 50 assert client._auto_register is True 51 52 53def test_observer_client_init_no_port(mock_session): 54 """When no config URL and no convey.port file, _url is empty.""" 55 from observe.observer_client import ObserverClient 56 57 with ( 58 patch("observe.observer_client.get_config") as cfg, 59 patch("observe.observer_client.read_service_port") as port, 60 ): 61 cfg.return_value = {} 62 port.return_value = None 63 client = ObserverClient("main-stream") 64 65 assert client._url == "" 66 67 68def test_observer_client_init_with_config(mock_session, mock_config): 69 from observe.observer_client import ObserverClient 70 71 mock_config.return_value = { 72 "observe": { 73 "observer": { 74 "url": "https://example.test/", 75 "key": "abc123", 76 "name": "named-observer", 77 "auto_register": False, 78 } 79 } 80 } 81 82 client = ObserverClient("main-stream") 83 84 assert client._url == "https://example.test" 85 assert client._key == "abc123" 86 assert client._name == "named-observer" 87 assert client._auto_register is False 88 89 90def test_auto_registration(mock_session, mock_config, mock_journal, tmp_path): 91 from observe.observer_client import ObserverClient 92 93 file1 = tmp_path / "audio.flac" 94 file1.write_bytes(b"audio") 95 96 create_response = MagicMock() 97 create_response.status_code = 200 98 create_response.json.return_value = {"key": "registered-key"} 99 100 upload_response = MagicMock() 101 upload_response.status_code = 200 102 upload_response.json.return_value = {"files": ["audio.flac"], "bytes": 5} 103 104 mock_session.post.side_effect = [create_response, upload_response] 105 106 client = ObserverClient("main-stream") 107 result = client.upload_segment("20250103", "120000_300", [file1]) 108 109 assert result.success is True 110 assert client._key == "registered-key" 111 assert mock_session.post.call_args_list[0][0][0].endswith( 112 "/app/observer/api/create" 113 ) 114 config = json.loads((mock_journal / "config" / "journal.json").read_text()) 115 assert config["observe"]["observer"]["key"] == "registered-key" 116 117 118def test_existing_key_skips_registration(mock_session, mock_config, tmp_path): 119 from observe.observer_client import ObserverClient 120 121 mock_config.return_value = {"observe": {"observer": {"key": "testkey123"}}} 122 123 file1 = tmp_path / "audio.flac" 124 file1.write_bytes(b"audio") 125 126 upload_response = MagicMock() 127 upload_response.status_code = 200 128 upload_response.json.return_value = {"files": ["audio.flac"], "bytes": 5} 129 mock_session.post.return_value = upload_response 130 131 client = ObserverClient("main-stream") 132 result = client.upload_segment("20250103", "120000_300", [file1]) 133 134 assert result.success is True 135 assert mock_session.post.call_count == 1 136 assert mock_session.post.call_args[0][0].endswith("/app/observer/ingest/testkey123") 137 138 139def test_registration_retry(mock_session, mock_config, mock_journal, tmp_path): 140 from observe.observer_client import ObserverClient 141 142 file1 = tmp_path / "audio.flac" 143 file1.write_bytes(b"audio") 144 145 create_response = MagicMock() 146 create_response.status_code = 200 147 create_response.json.return_value = {"key": "registered-key"} 148 149 upload_response = MagicMock() 150 upload_response.status_code = 200 151 upload_response.json.return_value = {"files": ["audio.flac"], "bytes": 5} 152 153 mock_session.post.side_effect = [ 154 requests.ConnectionError("no route"), 155 create_response, 156 upload_response, 157 ] 158 159 with patch("observe.observer_client.time.sleep"): 160 client = ObserverClient("main-stream") 161 result = client.upload_segment("20250103", "120000_300", [file1]) 162 163 assert result.success is True 164 assert mock_session.post.call_count == 3 165 166 167def test_registration_403(mock_session, mock_config): 168 from observe.observer_client import ObserverClient 169 170 response = MagicMock() 171 response.status_code = 403 172 mock_session.post.return_value = response 173 174 client = ObserverClient("main-stream") 175 client._ensure_registered() 176 177 assert client._revoked is True 178 assert client._key is None 179 180 181def test_upload_segment_success(mock_session, mock_config, tmp_path): 182 from observe.observer_client import ObserverClient 183 184 mock_config.return_value = {"observe": {"observer": {"key": "testkey123"}}} 185 186 file1 = tmp_path / "audio.flac" 187 file1.write_bytes(b"audio data") 188 189 mock_response = MagicMock() 190 mock_response.status_code = 200 191 mock_response.json.return_value = {"files": ["audio.flac"], "bytes": 10} 192 mock_session.post.return_value = mock_response 193 194 client = ObserverClient("main-stream") 195 result = client.upload_segment("20250103", "120000_300", [file1]) 196 197 assert result.success is True 198 assert result.duplicate is False 199 200 201def test_upload_segment_retry(mock_session, mock_config, tmp_path): 202 from observe.observer_client import ObserverClient 203 204 mock_config.return_value = {"observe": {"observer": {"key": "testkey123"}}} 205 206 file1 = tmp_path / "audio.flac" 207 file1.write_bytes(b"audio data") 208 209 failure = MagicMock() 210 failure.status_code = 500 211 failure.text = "Server error" 212 213 success = MagicMock() 214 success.status_code = 200 215 success.json.return_value = {"files": ["audio.flac"], "bytes": 10} 216 217 mock_session.post.side_effect = [failure, success] 218 219 with patch("observe.observer_client.time.sleep"): 220 client = ObserverClient("main-stream") 221 result = client.upload_segment("20250103", "120000_300", [file1]) 222 223 assert result.success is True 224 assert mock_session.post.call_count == 2 225 226 227def test_upload_segment_403(mock_session, mock_config, tmp_path): 228 from observe.observer_client import ObserverClient 229 230 mock_config.return_value = {"observe": {"observer": {"key": "testkey123"}}} 231 232 file1 = tmp_path / "audio.flac" 233 file1.write_bytes(b"audio data") 234 235 response = MagicMock() 236 response.status_code = 403 237 response.text = "Forbidden" 238 mock_session.post.return_value = response 239 240 client = ObserverClient("main-stream") 241 result = client.upload_segment("20250103", "120000_300", [file1]) 242 243 assert result.success is False 244 assert client._revoked is True 245 246 247def test_upload_segment_all_retries_fail(mock_session, mock_config, tmp_path): 248 from observe.observer_client import ObserverClient 249 250 mock_config.return_value = {"observe": {"observer": {"key": "testkey123"}}} 251 252 file1 = tmp_path / "audio.flac" 253 file1.write_bytes(b"audio data") 254 255 failure = MagicMock() 256 failure.status_code = 500 257 failure.text = "Server error" 258 mock_session.post.return_value = failure 259 260 with patch("observe.observer_client.time.sleep"): 261 client = ObserverClient("main-stream") 262 result = client.upload_segment("20250103", "120000_300", [file1]) 263 264 assert result.success is False 265 assert mock_session.post.call_count == 3 266 267 268def test_relay_event_success(mock_session, mock_config): 269 from observe.observer_client import ObserverClient 270 271 mock_config.return_value = {"observe": {"observer": {"key": "testkey123"}}} 272 273 response = MagicMock() 274 response.status_code = 200 275 mock_session.post.return_value = response 276 277 client = ObserverClient("main-stream") 278 result = client.relay_event("observe", "status", mode="idle") 279 280 assert result is True 281 assert mock_session.post.call_args[1]["json"] == { 282 "tract": "observe", 283 "event": "status", 284 "mode": "idle", 285 } 286 287 288def test_relay_event_403(mock_session, mock_config): 289 from observe.observer_client import ObserverClient 290 291 mock_config.return_value = {"observe": {"observer": {"key": "testkey123"}}} 292 293 response = MagicMock() 294 response.status_code = 403 295 response.text = "Forbidden" 296 mock_session.post.return_value = response 297 298 client = ObserverClient("main-stream") 299 result = client.relay_event("observe", "status", mode="idle") 300 301 assert result is False 302 assert client._revoked is True 303 304 305def test_key_persistence(mock_session, mock_config, mock_journal): 306 from observe.observer_client import ObserverClient 307 308 client = ObserverClient("main-stream") 309 client._persist_key("persisted-key") 310 311 config = json.loads((mock_journal / "config" / "journal.json").read_text()) 312 assert config == {"observe": {"observer": {"key": "persisted-key"}}} 313 314 315def test_key_persistence_preserves_existing(mock_session, mock_config, mock_journal): 316 from observe.observer_client import ObserverClient 317 318 config_dir = mock_journal / "config" 319 config_dir.mkdir() 320 config_path = config_dir / "journal.json" 321 config_path.write_text( 322 json.dumps( 323 {"identity": {"name": "Jer"}, "observe": {"tmux": {"enabled": True}}} 324 ) 325 ) 326 327 client = ObserverClient("main-stream") 328 client._persist_key("persisted-key") 329 330 config = json.loads(config_path.read_text()) 331 assert config["identity"]["name"] == "Jer" 332 assert config["observe"]["tmux"]["enabled"] is True 333 assert config["observe"]["observer"]["key"] == "persisted-key" 334 335 336def test_cleanup_draft(tmp_path): 337 from observe.observer_client import cleanup_draft 338 339 draft_dir = tmp_path / "draft" 340 draft_dir.mkdir() 341 (draft_dir / "a.txt").write_text("a") 342 (draft_dir / "b.txt").write_text("b") 343 344 cleanup_draft(str(draft_dir)) 345 346 assert not draft_dir.exists() 347 348 349def test_finalize_draft(tmp_path): 350 from observe.observer_client import finalize_draft 351 352 draft_dir = tmp_path / "091551_draft" 353 draft_dir.mkdir() 354 (draft_dir / "screen.webm").write_text("video") 355 (draft_dir / "audio.flac").write_text("audio") 356 357 result = finalize_draft(str(draft_dir), "091551_300") 358 359 assert result == str(tmp_path / "091551_300") 360 assert not draft_dir.exists() 361 final = tmp_path / "091551_300" 362 assert final.exists() 363 assert (final / "screen.webm").read_text() == "video" 364 assert (final / "audio.flac").read_text() == "audio" 365 366 367def test_upload_duplicate_response(mock_session, mock_config, tmp_path): 368 from observe.observer_client import ObserverClient 369 370 mock_config.return_value = {"observe": {"observer": {"key": "testkey123"}}} 371 372 file1 = tmp_path / "audio.flac" 373 file1.write_bytes(b"audio data") 374 375 response = MagicMock() 376 response.status_code = 200 377 response.json.return_value = { 378 "status": "duplicate", 379 "existing_segment": "120000_300", 380 "message": "All files already received", 381 } 382 mock_session.post.return_value = response 383 384 client = ObserverClient("main-stream") 385 result = client.upload_segment("20250103", "120000_300", [file1]) 386 387 assert result.success is True 388 assert result.duplicate is True