personal memory agent
0
fork

Configure Feed

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

tests: add ledger acceptance coverage

+461
+461
tests/test_surfaces_ledger.py
··· 1 1 # SPDX-License-Identifier: AGPL-3.0-only 2 2 # Copyright (c) 2026 sol pbc 3 3 4 + import json 5 + from datetime import UTC, datetime 6 + 4 7 import pytest 5 8 9 + _DAY_MS = 86_400_000 10 + 11 + 12 + def _utc_ms(value: str) -> int: 13 + return int(datetime.fromisoformat(value.replace("Z", "+00:00")).timestamp() * 1000) 14 + 15 + 16 + def _minimal_facet_tree(tmp_path, facets=("work",)) -> None: 17 + for facet in facets: 18 + facet_dir = tmp_path / "facets" / facet 19 + facet_dir.mkdir(parents=True, exist_ok=True) 20 + (facet_dir / "activities").mkdir(exist_ok=True) 21 + (facet_dir / "facet.json").write_text( 22 + json.dumps( 23 + { 24 + "title": facet.title(), 25 + "description": "", 26 + "color": "", 27 + "emoji": "", 28 + "muted": False, 29 + } 30 + ), 31 + encoding="utf-8", 32 + ) 33 + 34 + 35 + def _activity_record( 36 + record_id: str, 37 + created_at: int, 38 + *, 39 + activity: str = "meeting", 40 + hidden: bool = False, 41 + ) -> dict: 42 + return { 43 + "id": record_id, 44 + "activity": activity, 45 + "description": f"{record_id} description", 46 + "segments": [f"{record_id.split('_', 1)[-1]}"], 47 + "created_at": created_at, 48 + "hidden": hidden, 49 + } 50 + 51 + 52 + def _commitment( 53 + *, 54 + owner: str = "Mina", 55 + owner_entity_id: str | None = "mina", 56 + action: str = "send proposal", 57 + counterparty: str = "Ravi", 58 + counterparty_entity_id: str | None = "ravi", 59 + when: str = "tomorrow", 60 + context: str = "Commitment context.", 61 + ) -> dict: 62 + return { 63 + "owner": owner, 64 + "owner_entity_id": owner_entity_id, 65 + "action": action, 66 + "counterparty": counterparty, 67 + "counterparty_entity_id": counterparty_entity_id, 68 + "when": when, 69 + "context": context, 70 + } 71 + 72 + 73 + def _closure( 74 + *, 75 + owner: str = "Mina", 76 + owner_entity_id: str | None = "mina", 77 + action: str = "send proposal", 78 + counterparty: str = "Ravi", 79 + counterparty_entity_id: str | None = "ravi", 80 + resolution: str = "sent", 81 + context: str = "Closure context.", 82 + ) -> dict: 83 + return { 84 + "owner": owner, 85 + "owner_entity_id": owner_entity_id, 86 + "action": action, 87 + "counterparty": counterparty, 88 + "counterparty_entity_id": counterparty_entity_id, 89 + "resolution": resolution, 90 + "context": context, 91 + } 92 + 93 + 94 + def _decision( 95 + *, 96 + owner: str = "Mina", 97 + owner_entity_id: str | None = "mina", 98 + action: str = "move launch review", 99 + context: str = "Decision context.", 100 + ) -> dict: 101 + return { 102 + "owner": owner, 103 + "owner_entity_id": owner_entity_id, 104 + "action": action, 105 + "context": context, 106 + } 107 + 108 + 109 + def _write_story_activity( 110 + facet: str, 111 + day: str, 112 + record_id: str, 113 + created_at: int, 114 + *, 115 + commitments: list[dict] | None = None, 116 + closures: list[dict] | None = None, 117 + decisions: list[dict] | None = None, 118 + hidden: bool = False, 119 + ) -> None: 120 + from think.activities import append_activity_record, merge_story_fields 121 + 122 + append_activity_record( 123 + facet, 124 + day, 125 + _activity_record(record_id, created_at, hidden=hidden), 126 + ) 127 + merge_story_fields( 128 + facet, 129 + day, 130 + record_id, 131 + story={ 132 + "talent": "story", 133 + "body": f"{record_id} summary", 134 + "topics": ["ledger"], 135 + "confidence": 0.9, 136 + }, 137 + commitments=commitments or [], 138 + closures=closures or [], 139 + decisions=decisions or [], 140 + actor="story", 141 + ) 142 + 6 143 7 144 def test_append_edit_payload_validation(): 8 145 from think.activities import append_edit ··· 35 172 note="updated", 36 173 payload="not a dict", 37 174 ) 175 + 176 + 177 + def test_pairing_happy_path(tmp_path, monkeypatch): 178 + from think.surfaces import ledger as ledger_surface 179 + 180 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 181 + _minimal_facet_tree(tmp_path) 182 + _write_story_activity( 183 + "work", 184 + "20260410", 185 + "meeting_090000_300", 186 + _utc_ms("2026-04-10T09:00:00Z"), 187 + commitments=[_commitment()], 188 + ) 189 + _write_story_activity( 190 + "work", 191 + "20260412", 192 + "meeting_110000_300", 193 + _utc_ms("2026-04-12T11:00:00Z"), 194 + closures=[_closure()], 195 + ) 196 + 197 + items = ledger_surface.list(state="closed") 198 + 199 + assert len(items) == 1 200 + assert items[0].state == "closed" 201 + assert items[0].summary == "send proposal" 202 + assert len(items[0].sources) == 2 203 + 204 + 205 + def test_action_fuzzy_just_above_threshold(tmp_path, monkeypatch): 206 + from think.surfaces import ledger as ledger_surface 207 + 208 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 209 + _minimal_facet_tree(tmp_path) 210 + _write_story_activity( 211 + "work", 212 + "20260410", 213 + "meeting_090000_300", 214 + _utc_ms("2026-04-10T09:00:00Z"), 215 + commitments=[_commitment(action="send proposal")], 216 + ) 217 + _write_story_activity( 218 + "work", 219 + "20260411", 220 + "meeting_100000_300", 221 + _utc_ms("2026-04-11T10:00:00Z"), 222 + closures=[_closure(action="sent the proposal")], 223 + ) 224 + 225 + assert ledger_surface._actions_match( 226 + ledger_surface._normalize_action("send proposal"), 227 + ledger_surface._normalize_action("sent the proposal"), 228 + ) 229 + assert len(ledger_surface.list(state="closed")) == 1 230 + 231 + 232 + def test_action_fuzzy_just_below_threshold(tmp_path, monkeypatch): 233 + from think.surfaces import ledger as ledger_surface 234 + 235 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 236 + _minimal_facet_tree(tmp_path) 237 + _write_story_activity( 238 + "work", 239 + "20260410", 240 + "meeting_090000_300", 241 + _utc_ms("2026-04-10T09:00:00Z"), 242 + commitments=[_commitment(action="send proposal")], 243 + ) 244 + _write_story_activity( 245 + "work", 246 + "20260411", 247 + "meeting_100000_300", 248 + _utc_ms("2026-04-11T10:00:00Z"), 249 + commitments=[_commitment(action="share the proposal")], 250 + ) 251 + 252 + assert not ledger_surface._actions_match( 253 + ledger_surface._normalize_action("send proposal"), 254 + ledger_surface._normalize_action("share the proposal"), 255 + ) 256 + actions = [item.action for item in ledger_surface.list(state="open")] 257 + assert actions == ["send proposal", "share the proposal"] 258 + 259 + 260 + def test_cross_facet_dedup(tmp_path, monkeypatch): 261 + from think.surfaces import ledger as ledger_surface 262 + 263 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 264 + _minimal_facet_tree(tmp_path, facets=("work", "personal")) 265 + _write_story_activity( 266 + "work", 267 + "20260410", 268 + "meeting_090000_300", 269 + _utc_ms("2026-04-10T09:00:00Z"), 270 + commitments=[_commitment()], 271 + ) 272 + _write_story_activity( 273 + "personal", 274 + "20260412", 275 + "meeting_100000_300", 276 + _utc_ms("2026-04-12T10:00:00Z"), 277 + closures=[_closure()], 278 + ) 279 + 280 + items = ledger_surface.list(state="closed") 281 + 282 + assert len(items) == 1 283 + assert {source.facet for source in items[0].sources} == {"work", "personal"} 284 + 285 + 286 + def test_manual_close_round_trip(tmp_path, monkeypatch): 287 + from think.surfaces import ledger as ledger_surface 288 + 289 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 290 + _minimal_facet_tree(tmp_path) 291 + _write_story_activity( 292 + "work", 293 + "20260410", 294 + "meeting_090000_300", 295 + _utc_ms("2026-04-10T09:00:00Z"), 296 + commitments=[_commitment()], 297 + ) 298 + 299 + item = ledger_surface.list(state="open")[0] 300 + closed = ledger_surface.close(item.id, note="done") 301 + 302 + assert closed.state == "closed" 303 + refreshed = ledger_surface.get(item.id) 304 + assert refreshed is not None 305 + assert refreshed.state == "closed" 306 + assert any(source.field == "edits" for source in refreshed.sources) 307 + 308 + 309 + def test_idempotent_reclose(tmp_path, monkeypatch): 310 + from think.activities import load_activity_records 311 + from think.surfaces import ledger as ledger_surface 312 + 313 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 314 + _minimal_facet_tree(tmp_path) 315 + _write_story_activity( 316 + "work", 317 + "20260410", 318 + "meeting_090000_300", 319 + _utc_ms("2026-04-10T09:00:00Z"), 320 + commitments=[_commitment()], 321 + ) 322 + 323 + item = ledger_surface.list(state="open")[0] 324 + ledger_surface.close(item.id, note="first") 325 + ledger_surface.close(item.id, note="second") 326 + 327 + record = load_activity_records("work", "20260410", include_hidden=True)[0] 328 + closes = [ 329 + edit for edit in record["edits"] if edit.get("fields") == ["ledger_close"] 330 + ] 331 + assert len(closes) == 1 332 + 333 + 334 + def test_close_as_dropped(tmp_path, monkeypatch): 335 + from think.surfaces import ledger as ledger_surface 336 + 337 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 338 + _minimal_facet_tree(tmp_path) 339 + _write_story_activity( 340 + "work", 341 + "20260410", 342 + "meeting_090000_300", 343 + _utc_ms("2026-04-10T09:00:00Z"), 344 + commitments=[_commitment()], 345 + ) 346 + 347 + item = ledger_surface.list(state="open")[0] 348 + dropped = ledger_surface.close(item.id, note="not needed", as_state="dropped") 349 + assert dropped.state == "dropped" 350 + 351 + 352 + def test_decisions_dedup(tmp_path, monkeypatch): 353 + from think.surfaces import ledger as ledger_surface 354 + 355 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 356 + _minimal_facet_tree(tmp_path) 357 + _write_story_activity( 358 + "work", 359 + "20260410", 360 + "meeting_090000_300", 361 + _utc_ms("2026-04-10T09:00:00Z"), 362 + decisions=[_decision()], 363 + ) 364 + _write_story_activity( 365 + "work", 366 + "20260410", 367 + "meeting_100000_300", 368 + _utc_ms("2026-04-10T10:00:00Z"), 369 + decisions=[_decision(context="Later duplicate.")], 370 + ) 371 + 372 + results = ledger_surface.decisions() 373 + 374 + assert len(results) == 1 375 + assert results[0].created_at == _utc_ms("2026-04-10T09:00:00Z") 376 + assert results[0].source.activity_id == "meeting_090000_300" 377 + 378 + 379 + def test_missing_entity_id_pairing(tmp_path, monkeypatch): 380 + from think.surfaces import ledger as ledger_surface 381 + 382 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 383 + _minimal_facet_tree(tmp_path, facets=("work", "personal")) 384 + 385 + _write_story_activity( 386 + "work", 387 + "20260410", 388 + "meeting_090000_300", 389 + _utc_ms("2026-04-10T09:00:00Z"), 390 + commitments=[_commitment(owner_entity_id=None)], 391 + ) 392 + _write_story_activity( 393 + "personal", 394 + "20260411", 395 + "meeting_100000_300", 396 + _utc_ms("2026-04-11T10:00:00Z"), 397 + closures=[_closure(owner_entity_id=None, action="sent the proposal")], 398 + ) 399 + matched = ledger_surface.list(state="closed") 400 + assert len(matched) == 1 401 + 402 + _write_story_activity( 403 + "work", 404 + "20260412", 405 + "meeting_110000_300", 406 + _utc_ms("2026-04-12T11:00:00Z"), 407 + commitments=[_commitment(owner_entity_id=None, action="draft status update")], 408 + ) 409 + _write_story_activity( 410 + "personal", 411 + "20260413", 412 + "meeting_120000_300", 413 + _utc_ms("2026-04-13T12:00:00Z"), 414 + closures=[ 415 + _closure( 416 + owner_entity_id="mina", 417 + action="draft status update", 418 + counterparty_entity_id="ravi", 419 + ) 420 + ], 421 + ) 422 + open_items = ledger_surface.list(state="open") 423 + assert any(item.action == "draft status update" for item in open_items) 424 + 425 + 426 + def test_hidden_record_exclusion(tmp_path, monkeypatch): 427 + from think.surfaces import ledger as ledger_surface 428 + 429 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 430 + _minimal_facet_tree(tmp_path) 431 + _write_story_activity( 432 + "work", 433 + "20260410", 434 + "meeting_090000_300", 435 + _utc_ms("2026-04-10T09:00:00Z"), 436 + commitments=[_commitment()], 437 + hidden=True, 438 + ) 439 + 440 + assert ledger_surface.list(state="all") == [] 441 + 442 + 443 + def test_sort_default_varies_by_state(tmp_path, monkeypatch): 444 + from think.surfaces import ledger as ledger_surface 445 + 446 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 447 + _minimal_facet_tree(tmp_path) 448 + now_ms = int(datetime.now(UTC).timestamp() * 1000) 449 + 450 + _write_story_activity( 451 + "work", 452 + "20260410", 453 + "meeting_090000_300", 454 + now_ms - (3 * _DAY_MS), 455 + commitments=[_commitment(action="older open")], 456 + ) 457 + _write_story_activity( 458 + "work", 459 + "20260411", 460 + "meeting_100000_300", 461 + now_ms - _DAY_MS, 462 + commitments=[_commitment(action="newer open")], 463 + ) 464 + 465 + open_items = ledger_surface.list(state="open") 466 + assert [item.action for item in open_items] == ["older open", "newer open"] 467 + 468 + _write_story_activity( 469 + "work", 470 + "20260412", 471 + "meeting_110000_300", 472 + now_ms - (10 * _DAY_MS), 473 + commitments=[_commitment(action="older closed")], 474 + ) 475 + _write_story_activity( 476 + "work", 477 + "20260413", 478 + "meeting_120000_300", 479 + now_ms - (5 * _DAY_MS), 480 + closures=[_closure(action="older closed")], 481 + ) 482 + _write_story_activity( 483 + "work", 484 + "20260414", 485 + "meeting_130000_300", 486 + now_ms - (8 * _DAY_MS), 487 + commitments=[_commitment(action="newer closed")], 488 + ) 489 + _write_story_activity( 490 + "work", 491 + "20260415", 492 + "meeting_140000_300", 493 + now_ms - _DAY_MS, 494 + closures=[_closure(action="newer closed")], 495 + ) 496 + 497 + closed_items = ledger_surface.list(state="closed") 498 + assert [item.action for item in closed_items] == ["newer closed", "older closed"]