personal memory agent
0

Configure Feed

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

solstone / tests / test_observation_ops.py
14 kB 453 lines
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3 4from __future__ import annotations 5 6import json 7from collections.abc import Iterator 8from contextlib import contextmanager 9from pathlib import Path 10from typing import Any 11 12import pytest 13 14import solstone.think.entities.observations as observations 15 16 17@pytest.fixture(autouse=True) 18def _fast_observation_writes(monkeypatch: pytest.MonkeyPatch) -> None: 19 def fast_atomic_replace( 20 path: Path, data: str | bytes, *, mode: int | None = None 21 ) -> None: 22 path.parent.mkdir(parents=True, exist_ok=True) 23 if isinstance(data, bytes): 24 path.write_bytes(data) 25 else: 26 path.write_text(data, encoding="utf-8") 27 if mode is not None: 28 path.chmod(mode) 29 30 monkeypatch.setattr(observations, "atomic_replace", fast_atomic_replace) 31 32 33def _set_journal(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: 34 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) 35 36 37def _row( 38 content: str, 39 observed_at: int, 40 source_day: str = "20260301", 41) -> dict[str, Any]: 42 return { 43 "content": content, 44 "observed_at": observed_at, 45 "source_day": source_day, 46 } 47 48 49def _write_observations(facet: str, name: str, rows: list[dict[str, Any]]) -> Path: 50 observations.save_observations(facet, name, rows) 51 return observations.observations_file_path(facet, name) 52 53 54def _read_raw(path: Path) -> str: 55 return path.read_text(encoding="utf-8") 56 57 58def _write_raw_observation_lines(facet: str, name: str, lines: list[str]) -> Path: 59 path = observations.observations_file_path(facet, name) 60 path.parent.mkdir(parents=True, exist_ok=True) 61 path.write_text("\n".join(lines) + "\n", encoding="utf-8") 62 return path 63 64 65def _install_clock(monkeypatch: pytest.MonkeyPatch, *ticks: int) -> None: 66 values = iter(ticks) 67 monkeypatch.setattr(observations, "now_ms", lambda: next(values)) 68 69 70def test_observation_day_counts_counts_only_valid_source_days( 71 tmp_path: Path, 72 monkeypatch: pytest.MonkeyPatch, 73): 74 _set_journal(tmp_path, monkeypatch) 75 _write_raw_observation_lines( 76 "work", 77 "Alice Example", 78 [ 79 json.dumps({"content": "First", "source_day": "20260401"}), 80 json.dumps({"content": "Second", "source_day": "20260401"}), 81 json.dumps({"content": "Third", "source_day": "20260402"}), 82 json.dumps({"content": "Undated"}), 83 json.dumps({"content": "Integer day", "source_day": 1}), 84 json.dumps({"content": "Dashed day", "source_day": "2026-04-03"}), 85 json.dumps("not a dict"), 86 "{malformed", 87 "", 88 ], 89 ) 90 91 assert observations.observation_day_counts("work", "Alice Example") == { 92 "20260401": 2, 93 "20260402": 1, 94 } 95 96 97def test_observation_day_counts_empty_for_missing_file_and_empty_slug( 98 tmp_path: Path, 99 monkeypatch: pytest.MonkeyPatch, 100): 101 _set_journal(tmp_path, monkeypatch) 102 103 assert observations.observation_day_counts("work", "Missing Entity") == {} 104 assert observations.observation_day_counts("work", "") == {} 105 106 107def test_observation_count_predicates_can_diverge( 108 tmp_path: Path, 109 monkeypatch: pytest.MonkeyPatch, 110): 111 _set_journal(tmp_path, monkeypatch) 112 facet = "work" 113 name = "Predicate Divergence" 114 d1 = "20260401" 115 d2 = "20260402" 116 _write_raw_observation_lines( 117 facet, 118 name, 119 [ 120 json.dumps({"content": "First dated", "source_day": d1}), 121 json.dumps({"content": "Second dated", "source_day": d1}), 122 json.dumps({"content": "Third dated", "source_day": d2}), 123 json.dumps({"content": "Parseable undated"}), 124 "{malformed", 125 ], 126 ) 127 128 assert observations.count_observations(facet, name) == 5 129 assert len(observations.load_observations(facet, name)) == 4 130 assert observations.observation_day_counts(facet, name) == { 131 d1: 2, 132 d2: 1, 133 } 134 135 136def test_record_observation_ops_consolidates_remote_control_project( 137 tmp_path: Path, 138 monkeypatch: pytest.MonkeyPatch, 139): 140 _set_journal(tmp_path, monkeypatch) 141 _install_clock(monkeypatch, 1000, 1001) 142 facet = "work" 143 name = "remote_control_project" 144 initial = [ 145 _row( 146 "Remote Control Project supports browser automation for remote support.", 147 1, 148 ), 149 _row( 150 "Remote Control Project requires operator confirmation before connecting.", 151 2, 152 ), 153 _row( 154 "Remote Control Project supports browser automation for remote support.", 155 3, 156 ), 157 _row("Remote Control Project stores session audit records.", 4), 158 _row( 159 "Remote Control Project supports browser automation for remote support.", 160 5, 161 ), 162 _row("Remote Control Project uses a relay for live control sessions.", 6), 163 ] 164 _write_observations(facet, name, initial) 165 166 counts = observations.record_observation_ops( 167 facet, 168 name, 169 [ 170 { 171 "op": "drop", 172 "target_index": 2, 173 "target_quote": "browser automation", 174 }, 175 { 176 "op": "update", 177 "target_index": 0, 178 "content": ( 179 "Remote Control Project supports browser automation for remote " 180 "support with operator confirmation." 181 ), 182 "target_quote": "SUPPORTS BROWSER AUTOMATION", 183 }, 184 { 185 "op": "drop", 186 "target_index": 4, 187 "target_quote": "browser automation", 188 }, 189 { 190 "op": "add", 191 "content": ( 192 "Remote Control Project treats remote access as an " 193 "operator-owned workflow." 194 ), 195 }, 196 ], 197 "20260401", 198 ) 199 200 assert counts == {"update": 1, "add": 1, "drop": 2, "keep": 0, "skipped": 0} 201 assert observations.load_observations(facet, name) == [ 202 { 203 "content": ( 204 "Remote Control Project supports browser automation for remote " 205 "support with operator confirmation." 206 ), 207 "observed_at": 1000, 208 "source_day": "20260401", 209 }, 210 initial[1], 211 initial[3], 212 initial[5], 213 { 214 "content": ( 215 "Remote Control Project treats remote access as an " 216 "operator-owned workflow." 217 ), 218 "observed_at": 1001, 219 "source_day": "20260401", 220 }, 221 ] 222 223 224def test_record_observation_ops_skips_invalid_inputs_without_raising( 225 tmp_path: Path, 226 monkeypatch: pytest.MonkeyPatch, 227): 228 _set_journal(tmp_path, monkeypatch) 229 facet = "work" 230 name = "remote_control_project" 231 initial = [_row("Remote Control Project requires operator confirmation.", 1)] 232 path = _write_observations(facet, name, initial) 233 before = _read_raw(path) 234 235 counts = observations.record_observation_ops( 236 facet, 237 name, 238 [ 239 { 240 "op": "drop", 241 "target_index": 99, 242 "target_quote": "operator confirmation", 243 }, 244 { 245 "op": "update", 246 "target_index": 0, 247 "content": "Remote Control Project requires two-person approval.", 248 "target_quote": "missing quote", 249 }, 250 {"op": "add", "content": " "}, 251 ], 252 "20260401", 253 ) 254 255 assert counts == {"update": 0, "add": 0, "drop": 0, "keep": 0, "skipped": 3} 256 assert observations.load_observations(facet, name) == initial 257 assert _read_raw(path) == before 258 259 260def test_record_observation_ops_uses_original_snapshot_indices( 261 tmp_path: Path, 262 monkeypatch: pytest.MonkeyPatch, 263): 264 _set_journal(tmp_path, monkeypatch) 265 _install_clock(monkeypatch, 2000) 266 facet = "work" 267 name = "remote_control_project" 268 initial = [ 269 _row("Index zero should be dropped.", 1), 270 _row("Index one should survive unchanged.", 2), 271 _row("Index two should be updated.", 3), 272 ] 273 _write_observations(facet, name, initial) 274 275 counts = observations.record_observation_ops( 276 facet, 277 name, 278 [ 279 {"op": "drop", "target_index": 0, "target_quote": "dropped"}, 280 { 281 "op": "update", 282 "target_index": 2, 283 "content": "Index two was updated using the original index.", 284 "target_quote": "Index two", 285 }, 286 ], 287 "20260402", 288 ) 289 290 assert counts == {"update": 1, "add": 0, "drop": 1, "keep": 0, "skipped": 0} 291 assert observations.load_observations(facet, name) == [ 292 initial[1], 293 { 294 "content": "Index two was updated using the original index.", 295 "observed_at": 2000, 296 "source_day": "20260402", 297 }, 298 ] 299 300 301def test_record_observation_ops_holds_lock_across_read_and_write( 302 tmp_path: Path, 303 monkeypatch: pytest.MonkeyPatch, 304): 305 _set_journal(tmp_path, monkeypatch) 306 _install_clock(monkeypatch, 3000) 307 facet = "work" 308 name = "remote_control_project" 309 initial = [_row("Remote Control Project has a lock-sensitive row.", 1)] 310 _write_observations(facet, name, initial) 311 expected_path = observations.observations_file_path(facet, name) 312 lock_active = False 313 entered_paths: list[Path] = [] 314 read_inside_lock: list[bool] = [] 315 write_inside_lock: list[bool] = [] 316 real_load = observations.load_observations 317 real_save = observations.save_observations 318 319 @contextmanager 320 def spy_lock(path: Path) -> Iterator[None]: 321 nonlocal lock_active 322 entered_paths.append(path) 323 lock_active = True 324 try: 325 yield 326 finally: 327 lock_active = False 328 329 def spy_load(facet_arg: str, name_arg: str) -> list[dict[str, Any]]: 330 read_inside_lock.append(lock_active) 331 return real_load(facet_arg, name_arg) 332 333 def spy_save(facet_arg: str, name_arg: str, rows: list[dict[str, Any]]) -> None: 334 write_inside_lock.append(lock_active) 335 real_save(facet_arg, name_arg, rows) 336 337 monkeypatch.setattr(observations, "hold_lock", spy_lock) 338 monkeypatch.setattr(observations, "load_observations", spy_load) 339 monkeypatch.setattr(observations, "save_observations", spy_save) 340 341 counts = observations.record_observation_ops( 342 facet, 343 name, 344 [ 345 { 346 "op": "update", 347 "target_index": 0, 348 "content": "Remote Control Project updates while locked.", 349 } 350 ], 351 "20260403", 352 ) 353 354 assert counts == {"update": 1, "add": 0, "drop": 0, "keep": 0, "skipped": 0} 355 assert entered_paths == [expected_path] 356 assert read_inside_lock == [True] 357 assert write_inside_lock == [True] 358 359 360def test_record_observation_ops_on_zero_observations_skips_indexed_ops_and_adds( 361 tmp_path: Path, 362 monkeypatch: pytest.MonkeyPatch, 363): 364 _set_journal(tmp_path, monkeypatch) 365 _install_clock(monkeypatch, 4000) 366 facet = "work" 367 name = "remote_control_project" 368 369 counts = observations.record_observation_ops( 370 facet, 371 name, 372 [ 373 {"op": "drop", "target_index": 0}, 374 { 375 "op": "update", 376 "target_index": 0, 377 "content": "This update has no target.", 378 }, 379 { 380 "op": "add", 381 "content": "Remote Control Project starts with an added observation.", 382 }, 383 ], 384 "20260404", 385 ) 386 387 assert counts == {"update": 0, "add": 1, "drop": 0, "keep": 0, "skipped": 2} 388 assert observations.load_observations(facet, name) == [ 389 { 390 "content": "Remote Control Project starts with an added observation.", 391 "observed_at": 4000, 392 "source_day": "20260404", 393 } 394 ] 395 396 397def test_record_observation_ops_empty_ops_do_not_churn_files( 398 tmp_path: Path, 399 monkeypatch: pytest.MonkeyPatch, 400): 401 _set_journal(tmp_path, monkeypatch) 402 facet = "work" 403 absent_name = "remote_control_project" 404 absent_path = observations.observations_file_path(facet, absent_name) 405 406 absent_counts = observations.record_observation_ops(facet, absent_name, []) 407 408 assert absent_counts == { 409 "update": 0, 410 "add": 0, 411 "drop": 0, 412 "keep": 0, 413 "skipped": 0, 414 } 415 assert not absent_path.exists() 416 417 existing_name = "existing_remote_control_project" 418 existing_rows = [_row("Existing observation remains unchanged.", 1)] 419 existing_path = _write_observations(facet, existing_name, existing_rows) 420 before = _read_raw(existing_path) 421 422 existing_counts = observations.record_observation_ops(facet, existing_name, []) 423 424 assert existing_counts == { 425 "update": 0, 426 "add": 0, 427 "drop": 0, 428 "keep": 0, 429 "skipped": 0, 430 } 431 assert _read_raw(existing_path) == before 432 433 434def test_record_observation_ops_keep_only_does_not_churn_file( 435 tmp_path: Path, 436 monkeypatch: pytest.MonkeyPatch, 437): 438 _set_journal(tmp_path, monkeypatch) 439 facet = "work" 440 name = "remote_control_project" 441 initial = [_row("Remote Control Project keeps this observation.", 1)] 442 path = _write_observations(facet, name, initial) 443 before = _read_raw(path) 444 445 counts = observations.record_observation_ops( 446 facet, 447 name, 448 [{"op": "keep", "target_index": 0, "target_quote": "keeps this"}], 449 ) 450 451 assert counts == {"update": 0, "add": 0, "drop": 0, "keep": 1, "skipped": 0} 452 assert observations.load_observations(facet, name) == initial 453 assert _read_raw(path) == before