personal memory agent
0

Configure Feed

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

solstone / tests / test_speaker_identify_operations.py
12 kB 370 lines
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3 4from __future__ import annotations 5 6import json 7from pathlib import Path 8 9import pytest 10 11import solstone.think.speaker_identify_operations as ledger 12 13 14@pytest.fixture 15def op_journal(monkeypatch, tmp_path): 16 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) 17 import solstone.think.utils as think_utils 18 19 think_utils._journal_path_cache = None 20 return Path(tmp_path) 21 22 23def _members() -> list[dict[str, object]]: 24 return [ 25 { 26 "day": "20260101", 27 "stream": "test", 28 "segment_key": "090000_300", 29 "source": "mic_audio", 30 "sentence_id": 1, 31 }, 32 { 33 "day": "20260101", 34 "stream": "test", 35 "segment_key": "090000_300", 36 "source": "mic_audio", 37 "sentence_id": 2, 38 }, 39 ] 40 41 42def _prepared_event(request_id: str = "req-1") -> dict[str, object]: 43 operation_id = ledger.operation_id_for_request(request_id) 44 members = _members() 45 fingerprint = ledger.request_fingerprint( 46 cluster_members=members, 47 target_entity_id="alice_test", 48 will_create=True, 49 entity_type="Person", 50 reviewed_near_match_entity_ids=["bob_test"], 51 ) 52 plan = { 53 "plan_schema_version": 1, 54 "operation_id": operation_id, 55 "request_id": request_id, 56 "planned_at": "2026-07-20T12:00:00Z", 57 "request": { 58 "cluster_id": 7, 59 "name": "Alice Test", 60 "entity_id": "alice_test", 61 "resolve_only": False, 62 "create_new": True, 63 "entity_type": "Person", 64 "reviewed_near_match_entity_ids": ["bob_test"], 65 }, 66 "cluster": {"cluster_id": 7, "member_count": len(members), "members": members}, 67 "target": { 68 "entity_id": "alice_test", 69 "entity_name": "Alice Test", 70 "entity_type": "Person", 71 "will_create": True, 72 }, 73 "entity_identity": { 74 "prior_identity": None, 75 "intended_identity": { 76 "id": "alice_test", 77 "name": "Alice Test", 78 "type": "Person", 79 "created_at": 1, 80 }, 81 "expected_history_operation": { 82 "operation_kind": "speaker_identify", 83 "operation_id": operation_id, 84 }, 85 }, 86 "direct_voiceprints": {"preexisting_keys": [], "entries_to_add": []}, 87 "segments": [], 88 "retro_confirm": { 89 "matched": False, 90 "match_score": None, 91 "candidate_id": None, 92 "candidate_before": None, 93 "candidate_after": None, 94 "preexisting_voiceprint_keys": [], 95 "voiceprints_to_add": [], 96 }, 97 "sentinel": { 98 "cluster_key": "7", 99 "prior_entry": None, 100 "intended_entry": { 101 "entity_id": "alice_test", 102 "label": "Alice Test", 103 "ts": "2026-07-20T12:00:00Z", 104 }, 105 }, 106 "keep_separate_assertions": [], 107 } 108 return { 109 "schema_version": ledger.IDENTIFY_OPERATION_SCHEMA_VERSION, 110 "event_id": f"{operation_id}:prepared", 111 "operation_id": operation_id, 112 "request_id": request_id, 113 "event_kind": "prepared", 114 "ts": "2026-07-20T12:00:00Z", 115 "caller": "test", 116 "actor": None, 117 "request_fingerprint": fingerprint, 118 "prepared_plan": plan, 119 } 120 121 122def _checkpoint_event( 123 operation_id: str, request_id: str, phase: str 124) -> dict[str, object]: 125 return { 126 "schema_version": ledger.IDENTIFY_OPERATION_SCHEMA_VERSION, 127 "event_id": f"{operation_id}:checkpoint:{phase}", 128 "operation_id": operation_id, 129 "request_id": request_id, 130 "event_kind": "checkpoint", 131 "ts": "2026-07-20T12:00:01Z", 132 "caller": "test", 133 "actor": None, 134 "phase": phase, 135 "checkpoint": { 136 "phase_status": "complete", 137 "completed_at": "2026-07-20T12:00:01Z", 138 "counts": {"saved_count": 2}, 139 "skipped_reasons": {}, 140 "saved_count": 2, 141 "skipped_existing_count": 0, 142 "saved_keys": [ 143 { 144 "day": "20260101", 145 "segment_key": "090000_300", 146 "source": "mic_audio", 147 "sentence_id": 1, 148 } 149 ], 150 }, 151 } 152 153 154def test_operation_id_for_request_is_deterministic(): 155 assert ledger.operation_id_for_request("abc") == ledger.operation_id_for_request( 156 "abc" 157 ) 158 assert ledger.operation_id_for_request("abc").startswith("idop_") 159 assert ledger.operation_id_for_request("abc") != ledger.operation_id_for_request( 160 "abcd" 161 ) 162 163 164def test_request_fingerprint_changes_for_each_identity_input(): 165 base = ledger.request_fingerprint( 166 cluster_members=_members(), 167 target_entity_id="alice_test", 168 will_create=True, 169 entity_type="Person", 170 reviewed_near_match_entity_ids=["bob_test"], 171 ) 172 changed_member = ledger.request_fingerprint( 173 cluster_members=[{**_members()[0], "sentence_id": 99}], 174 target_entity_id="alice_test", 175 will_create=True, 176 entity_type="Person", 177 reviewed_near_match_entity_ids=["bob_test"], 178 ) 179 changed_target = ledger.request_fingerprint( 180 cluster_members=_members(), 181 target_entity_id="carol_test", 182 will_create=True, 183 entity_type="Person", 184 reviewed_near_match_entity_ids=["bob_test"], 185 ) 186 changed_create = ledger.request_fingerprint( 187 cluster_members=_members(), 188 target_entity_id="alice_test", 189 will_create=False, 190 entity_type="Person", 191 reviewed_near_match_entity_ids=["bob_test"], 192 ) 193 changed_type = ledger.request_fingerprint( 194 cluster_members=_members(), 195 target_entity_id="alice_test", 196 will_create=True, 197 entity_type="Project", 198 reviewed_near_match_entity_ids=["bob_test"], 199 ) 200 changed_reviewed = ledger.request_fingerprint( 201 cluster_members=_members(), 202 target_entity_id="alice_test", 203 will_create=True, 204 entity_type="Person", 205 reviewed_near_match_entity_ids=["carol_test"], 206 ) 207 208 assert len(base) == 64 209 assert ( 210 len( 211 { 212 base, 213 changed_member, 214 changed_target, 215 changed_create, 216 changed_type, 217 changed_reviewed, 218 } 219 ) 220 == 6 221 ) 222 assert base not in { 223 changed_member, 224 changed_target, 225 changed_create, 226 changed_type, 227 changed_reviewed, 228 } 229 230 231def test_append_and_fold_prepared_resume_state(op_journal): 232 prepared = _prepared_event() 233 checkpoint = _checkpoint_event( 234 str(prepared["operation_id"]), 235 str(prepared["request_id"]), 236 "direct_voiceprints", 237 ) 238 239 ledger.append_event(prepared) 240 ledger.append_event(checkpoint) 241 242 state = ledger.fold_operation(str(prepared["operation_id"])) 243 assert state is not None 244 assert state.operation_id == prepared["operation_id"] 245 assert state.request_fingerprint == prepared["request_fingerprint"] 246 assert state.cluster_member_set == { 247 ("20260101", "test", "090000_300", "mic_audio", 1), 248 ("20260101", "test", "090000_300", "mic_audio", 2), 249 } 250 assert state.target_entity_id == "alice_test" 251 assert state.target_entity_name == "Alice Test" 252 assert state.will_create is True 253 assert state.entity_type == "Person" 254 assert state.reviewed_near_match_entity_ids == ("bob_test",) 255 assert state.completed_phases == ("direct_voiceprints",) 256 assert state.pending_phases == ( 257 "entity", 258 "keep_separate", 259 "corrections", 260 "labels", 261 "retro_tracker", 262 "sentinel", 263 ) 264 assert state.terminal_status == "in_progress" 265 assert state.phase_checkpoints["direct_voiceprints"]["counts"]["saved_count"] == 2 266 267 268def test_read_paths_do_not_create_speakers_dir(op_journal): 269 assert ledger.load_operations() == [] 270 assert ledger.fold_operation("idop_missing") is None 271 assert ledger.fold_all_operations() == [] 272 assert not (op_journal / "speakers").exists() 273 274 275def test_committed_fold_returns_stored_result(op_journal): 276 prepared = _prepared_event() 277 operation_id = str(prepared["operation_id"]) 278 committed = { 279 "schema_version": ledger.IDENTIFY_OPERATION_SCHEMA_VERSION, 280 "event_id": f"{operation_id}:committed", 281 "operation_id": operation_id, 282 "request_id": prepared["request_id"], 283 "event_kind": "committed", 284 "ts": "2026-07-20T12:00:02Z", 285 "caller": "test", 286 "actor": None, 287 "result": {"status": "identified", "operation_id": operation_id}, 288 } 289 290 ledger.append_event(prepared) 291 ledger.append_event(committed) 292 293 state = ledger.fold_operation(operation_id) 294 assert state is not None 295 assert state.terminal_status == "committed" 296 assert state.result == {"status": "identified", "operation_id": operation_id} 297 assert state.pending_phases == () 298 299 300def test_undo_started_folds_as_undoing_not_committed(op_journal): 301 prepared = _prepared_event() 302 operation_id = str(prepared["operation_id"]) 303 committed = { 304 "schema_version": ledger.IDENTIFY_OPERATION_SCHEMA_VERSION, 305 "event_id": f"{operation_id}:committed", 306 "operation_id": operation_id, 307 "request_id": prepared["request_id"], 308 "event_kind": "committed", 309 "ts": "2026-07-20T12:00:02Z", 310 "caller": "test", 311 "actor": None, 312 "result": {"status": "identified", "operation_id": operation_id}, 313 } 314 undo_prepared = { 315 "schema_version": ledger.IDENTIFY_OPERATION_SCHEMA_VERSION, 316 "event_id": f"{operation_id}:undo_prepared", 317 "operation_id": operation_id, 318 "request_id": prepared["request_id"], 319 "event_kind": "undo_prepared", 320 "ts": "2026-07-20T12:00:03Z", 321 "caller": "test", 322 "actor": None, 323 "undo_started_at": "2026-07-20T12:00:03Z", 324 } 325 326 ledger.append_event(prepared) 327 ledger.append_event(committed) 328 ledger.append_event(undo_prepared) 329 330 state = ledger.fold_operation(operation_id) 331 assert state is not None 332 assert state.terminal_status == "undoing" 333 assert state.pending_phases == ledger.UNDO_PHASE_ORDER 334 335 336def test_identical_duplicate_event_id_folds_once(op_journal): 337 prepared = _prepared_event() 338 path = ledger.identify_operations_path(create=True) 339 path.write_text( 340 json.dumps(prepared) + "\n" + json.dumps(prepared) + "\n", 341 encoding="utf-8", 342 ) 343 344 state = ledger.fold_operation(str(prepared["operation_id"])) 345 assert state is not None 346 assert state.terminal_status == "in_progress" 347 348 349def test_non_identical_duplicate_event_id_raises(op_journal): 350 prepared = _prepared_event() 351 changed = dict(prepared) 352 changed["ts"] = "2026-07-20T12:00:09Z" 353 path = ledger.identify_operations_path(create=True) 354 path.write_text( 355 json.dumps(prepared) + "\n" + json.dumps(changed) + "\n", 356 encoding="utf-8", 357 ) 358 359 with pytest.raises(ledger.IdentifyOperationError): 360 ledger.fold_operation(str(prepared["operation_id"])) 361 362 363def test_strict_malformed_row_raises(op_journal): 364 ledger.identify_operations_path(create=True).write_text( 365 "not-json\n", 366 encoding="utf-8", 367 ) 368 369 with pytest.raises(ledger.IdentifyOperationError): 370 ledger.load_operations()