personal memory agent
0

Configure Feed

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

solstone / tests / test_think_segment_prephase.py
55 kB 1734 lines
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3 4"""Regression witnesses for the daily segment-think pre-phase.""" 5 6import asyncio 7import importlib 8import json 9import logging 10import subprocess 11import threading 12import time 13from pathlib import Path 14from unittest.mock import Mock 15 16import pytest 17 18from solstone.think.providers import fanout_policy 19from tests.test_think_segment import _segment_configs, _write_sense_output 20 21DAY = "20240115" 22STREAM = "default" 23ACTIVE_SEGMENT = "090000_300" 24IDLE_SEGMENT = "090500_300" 25FACET = "work" 26 27 28class NullCallosumConnection: 29 def __init__(self, *args, **kwargs) -> None: 30 pass 31 32 def start(self, callback=None) -> None: 33 return None 34 35 def emit(self, *args, **kwargs) -> None: 36 return None 37 38 def stop(self) -> None: 39 return None 40 41 42def _read_jsonl(path: Path) -> list[dict]: 43 return [ 44 json.loads(line) 45 for line in path.read_text(encoding="utf-8").splitlines() 46 if line.strip() 47 ] 48 49 50def _write_jsonl(path: Path, events: list[dict]) -> None: 51 path.parent.mkdir(parents=True, exist_ok=True) 52 with path.open("w", encoding="utf-8") as handle: 53 for event in events: 54 handle.write(json.dumps(event) + "\n") 55 56 57def _event_name(event: dict) -> str: 58 return str(event.get("event") or event.get("type") or "") 59 60 61def _active_sense() -> dict: 62 return { 63 "density": "active", 64 "content_type": "coding", 65 "activity_summary": "Writing tests", 66 "entities": [], 67 "facets": [{"facet": FACET, "level": "high"}], 68 "recommend": {}, 69 } 70 71 72def _idle_sense() -> dict: 73 return { 74 "density": "idle", 75 "content_type": "idle", 76 "activity_summary": "", 77 "entities": [], 78 "facets": [], 79 "recommend": {}, 80 } 81 82 83def _seed_segment( 84 journal: Path, 85 day: str, 86 segment: str, 87 sense_json: dict | None = None, 88) -> Path: 89 segment_dir = journal / "chronicle" / day / STREAM / segment 90 (segment_dir / "talents").mkdir(parents=True, exist_ok=True) 91 (segment_dir / "screen.jsonl").write_text( 92 json.dumps({"timestamp": f"{day}T09:00:00"}) + "\n", 93 encoding="utf-8", 94 ) 95 _write_sense_output(segment_dir, sense_json or _active_sense()) 96 return segment_dir 97 98 99def _complete_segment_progress(): 100 from solstone.think.pipeline_health import SEGMENT_FLOOR_TALENTS, SegmentProgress 101 102 floor = frozenset(SEGMENT_FLOOR_TALENTS) 103 return SegmentProgress( 104 sensed=True, 105 density="active", 106 change_class=None, 107 dispatched=floor, 108 completed=floor, 109 unconfigured=frozenset(), 110 capped=frozenset(), 111 ) 112 113 114def _append_segment_terminal( 115 journal: Path, 116 *, 117 name: str, 118 event: str = "talent.complete", 119 ts: int = 100, 120 stream: str = STREAM, 121) -> None: 122 health_dir = journal / "chronicle" / DAY / "health" 123 health_dir.mkdir(parents=True, exist_ok=True) 124 path = health_dir / "001_segment.jsonl" 125 with path.open("a", encoding="utf-8") as handle: 126 handle.write( 127 json.dumps( 128 { 129 "event": event, 130 "ts": ts, 131 "mode": "segment", 132 "day": DAY, 133 "segment": ACTIVE_SEGMENT, 134 "stream": stream, 135 "name": name, 136 } 137 ) 138 + "\n" 139 ) 140 141 142def _patch_main_runtime(monkeypatch: pytest.MonkeyPatch) -> None: 143 from solstone.think import thinking as think 144 145 monkeypatch.setattr(think, "CallosumConnection", NullCallosumConnection) 146 monkeypatch.setattr(think, "check_callosum_available", lambda: True) 147 148 149def test_daily_health_log_keeps_segment_events_out(journal_copy, monkeypatch): 150 mod = importlib.import_module("solstone.think.thinking") 151 152 def mock_run_command(cmd, day): 153 return True 154 155 def mock_run_queued_command(cmd, day, timeout=600): 156 return True 157 158 def mock_run_daily_prompts(day, verbose, **kwargs): 159 return (5, 0, [], set()) 160 161 _patch_main_runtime(monkeypatch) 162 monkeypatch.setattr(mod, "run_command", mock_run_command) 163 monkeypatch.setattr( 164 mod, "run_bounded_phase", lambda cmd, day, timeout=None: (True, False) 165 ) 166 monkeypatch.setattr(mod, "run_queued_command", mock_run_queued_command) 167 monkeypatch.setattr(mod, "run_daily_prompts", mock_run_daily_prompts) 168 monkeypatch.setattr("sys.argv", ["sol think", "--day", "20240101"]) 169 170 mod.main() 171 172 health_dir = journal_copy / "chronicle" / "20240101" / "health" 173 daily_files = sorted(health_dir.glob("*_daily.jsonl")) 174 assert len(daily_files) == 1 175 176 events = _read_jsonl(daily_files[0]) 177 assert any( 178 event.get("phase") == "segment_think" 179 and _event_name(event).startswith("phase.") 180 for event in events 181 ) 182 assert not [ 183 event 184 for event in events 185 if _event_name(event).startswith(("talent.", "activity.")) 186 ] 187 188 189def _forbid_slot_discovery(monkeypatch, mod): 190 """Assert the non-local path never probes the local server.""" 191 del mod 192 193 def _unreachable() -> int: 194 raise AssertionError("slot discovery must not run for non-local defaults") 195 196 monkeypatch.setattr(fanout_policy, "read_server_parallel_slots", _unreachable) 197 198 199def _pin_describe_non_local(monkeypatch, mod): 200 """Pin the describe default to its CPU formula. 201 202 The fixture journal resolves observe.* to google, but these tests assert an 203 exact -j value; stub the predicate so they do not silently depend on that. 204 """ 205 monkeypatch.setattr(fanout_policy, "_describe_uses_local", lambda: False) 206 _forbid_slot_discovery(monkeypatch, mod) 207 208 209def test_sense_repair_prephase_uses_default_describe_jobs(journal_copy, monkeypatch): 210 mod = importlib.import_module("solstone.think.thinking") 211 bounded_calls = [] 212 daily_called = [] 213 214 _pin_describe_non_local(monkeypatch, mod) 215 216 monkeypatch.setattr(fanout_policy.os, "cpu_count", lambda: 16) 217 assert fanout_policy.default_describe_jobs() == 4 218 monkeypatch.setattr(fanout_policy.os, "cpu_count", lambda: 7) 219 assert fanout_policy.default_describe_jobs() == 1 220 monkeypatch.setattr(fanout_policy.os, "cpu_count", lambda: 8) 221 assert fanout_policy.default_describe_jobs() == 2 222 223 def fake_bounded(cmd, day, timeout=None): 224 bounded_calls.append((cmd, day, timeout)) 225 return (True, False) 226 227 def fake_daily(day, verbose, **kwargs): 228 daily_called.append(day) 229 return (5, 0, [], set()) 230 231 _patch_main_runtime(monkeypatch) 232 monkeypatch.setattr(mod, "run_bounded_phase", fake_bounded) 233 monkeypatch.setattr(mod, "run_command", lambda cmd, day: True) 234 monkeypatch.setattr(mod, "run_queued_command", lambda cmd, day, timeout=600: True) 235 monkeypatch.setattr(mod, "run_daily_prompts", fake_daily) 236 monkeypatch.setattr("sys.argv", ["sol think", "--day", "20240101"]) 237 238 mod.main() 239 240 assert daily_called == ["20240101"] 241 assert bounded_calls[0] == ( 242 ["journal", "sense", "--day", "20240101", "-j", "2"], 243 "20240101", 244 mod.DEFAULT_TASK_MAX_RUNTIME, 245 ) 246 247 248def test_daily_segment_prephase_timeout_is_nonfatal(journal_copy, monkeypatch): 249 mod = importlib.import_module("solstone.think.thinking") 250 bounded_calls = [] 251 command_calls = [] 252 daily_called = [] 253 254 def fake_bounded(cmd, day, timeout=None): 255 bounded_calls.append((cmd, day, timeout)) 256 return (False, True) 257 258 def fake_command(cmd, day): 259 command_calls.append(cmd) 260 return True 261 262 def fake_daily(day, verbose, **kwargs): 263 daily_called.append(day) 264 return (5, 0, [], set()) 265 266 _patch_main_runtime(monkeypatch) 267 _pin_describe_non_local(monkeypatch, mod) 268 monkeypatch.setattr(fanout_policy.os, "cpu_count", lambda: 16) 269 monkeypatch.setattr(mod, "run_bounded_phase", fake_bounded) 270 monkeypatch.setattr(mod, "run_command", fake_command) 271 monkeypatch.setattr(mod, "run_queued_command", lambda cmd, day, timeout=600: True) 272 monkeypatch.setattr(mod, "run_daily_prompts", fake_daily) 273 monkeypatch.setattr("sys.argv", ["sol think", "--day", "20240101"]) 274 275 mod.main() 276 277 health_dir = journal_copy / "chronicle" / "20240101" / "health" 278 daily_files = sorted(health_dir.glob("*_daily.jsonl")) 279 assert len(daily_files) == 1 280 events = _read_jsonl(daily_files[0]) 281 segment_completes = [ 282 event 283 for event in events 284 if _event_name(event) == "phase.complete" 285 and event.get("phase") == "segment_think" 286 ] 287 assert len(segment_completes) == 1 288 complete = segment_completes[0] 289 assert complete["success"] is False 290 assert complete["reason_code"] == "wall_clock_exceeded" 291 assert complete["timeout_seconds"] == 1800 292 assert complete["bounded"] is True 293 294 assert daily_called 295 assert bounded_calls == [ 296 ( 297 ["journal", "sense", "--day", "20240101", "-j", "4"], 298 "20240101", 299 mod.DEFAULT_TASK_MAX_RUNTIME, 300 ), 301 ( 302 ["journal", "think", "--segments", "--day", "20240101"], 303 "20240101", 304 mod.DEFAULT_TASK_MAX_RUNTIME, 305 ), 306 ( 307 ["journal", "journal-stats"], 308 "20240101", 309 mod.JOURNAL_STATS_MAX_RUNTIME, 310 ), 311 ] 312 assert mod.DEFAULT_TASK_MAX_RUNTIME == 1800 313 assert mod.JOURNAL_STATS_MAX_RUNTIME == 600 314 assert command_calls == [] 315 316 317def test_daily_segment_prephase_records_yield_and_progressing_repair( 318 journal_copy, monkeypatch, caplog 319): 320 from solstone.think import catchup_state 321 from solstone.think import thinking as mod 322 323 day = "20240131" 324 _seed_segment(journal_copy, day, ACTIVE_SEGMENT) 325 326 def fake_bounded(cmd, day_arg, timeout=None): 327 if cmd[:2] == ["journal", "sense"]: 328 return (True, False) 329 if cmd[:3] == ["journal", "think", "--segments"]: 330 _write_jsonl( 331 journal_copy / "chronicle" / day / "health" / "001_segment.jsonl", 332 [ 333 { 334 "event": "sense.complete", 335 "ts": 1, 336 "mode": "segment", 337 "day": day, 338 "segment": ACTIVE_SEGMENT, 339 "stream": STREAM, 340 "density": "active", 341 }, 342 *[ 343 { 344 "event": "talent.complete", 345 "ts": index + 2, 346 "mode": "segment", 347 "day": day, 348 "segment": ACTIVE_SEGMENT, 349 "stream": STREAM, 350 "name": name, 351 } 352 for index, name in enumerate(mod.SEGMENT_FLOOR_TALENTS) 353 ], 354 ], 355 ) 356 return (False, True) 357 return (True, False) 358 359 def fake_daily(day, verbose, **kwargs): 360 return (1, 0, [], set()) 361 362 _patch_main_runtime(monkeypatch) 363 monkeypatch.setattr(mod, "run_bounded_phase", fake_bounded) 364 monkeypatch.setattr(mod, "run_queued_command", lambda cmd, day, timeout=600: True) 365 monkeypatch.setattr(mod, "run_daily_prompts", fake_daily) 366 monkeypatch.setattr("sys.argv", ["sol think", "--day", day]) 367 368 caplog.set_level(logging.WARNING) 369 mod.main() 370 371 health_dir = journal_copy / "chronicle" / day / "health" 372 daily_files = sorted(health_dir.glob("*_daily.jsonl")) 373 events = _read_jsonl(daily_files[0]) 374 segment_complete = next( 375 event 376 for event in events 377 if _event_name(event) == "phase.complete" 378 and event.get("phase") == "segment_think" 379 ) 380 assert segment_complete["success"] is False 381 assert segment_complete["cleared"] == 1 382 assert segment_complete["remaining"] == 0 383 384 record = catchup_state.read_day_record(day, catchup_state.KIND_SEGMENT_REPAIR) 385 assert record["last_outcome"] == catchup_state.PROGRESSING_OUTCOME 386 assert record["cleared"] == 1 387 assert record["remaining"] == 0 388 assert record["exit_reason"] == catchup_state.SEGMENT_REPAIR_TIMEOUT_REASON 389 assert "Segment-think repair exceeded its 1800s budget" in caplog.text 390 assert "yield: 1 cleared, 0 remaining" in caplog.text 391 392 393def test_daily_sense_prephase_timeout_records_disposition(journal_copy, monkeypatch): 394 mod = importlib.import_module("solstone.think.thinking") 395 daily_called = [] 396 397 def fake_bounded(cmd, day, timeout=None): 398 if cmd[:2] == ["journal", "sense"]: 399 return (False, True) 400 return (True, False) 401 402 def fake_daily(day, verbose, **kwargs): 403 daily_called.append(day) 404 return (5, 0, [], set()) 405 406 _patch_main_runtime(monkeypatch) 407 monkeypatch.setattr(mod, "run_bounded_phase", fake_bounded) 408 monkeypatch.setattr(mod, "run_command", lambda cmd, day: True) 409 monkeypatch.setattr(mod, "run_queued_command", lambda cmd, day, timeout=600: True) 410 monkeypatch.setattr(mod, "run_daily_prompts", fake_daily) 411 monkeypatch.setattr("sys.argv", ["sol think", "--day", "20240101"]) 412 413 mod.main() 414 415 health_dir = journal_copy / "chronicle" / "20240101" / "health" 416 daily_files = sorted(health_dir.glob("*_daily.jsonl")) 417 assert len(daily_files) == 1 418 events = _read_jsonl(daily_files[0]) 419 completes = [ 420 e 421 for e in events 422 if _event_name(e) == "phase.complete" and e.get("phase") == "sense_repair" 423 ] 424 assert len(completes) == 1 425 complete = completes[0] 426 assert complete["success"] is False 427 assert complete["reason_code"] == "wall_clock_exceeded" 428 assert complete["timeout_seconds"] == mod.DEFAULT_TASK_MAX_RUNTIME 429 assert complete["bounded"] is True 430 # Pipeline continued past the sense pre-phase into the daily prompts. 431 assert daily_called 432 433 434def test_daily_journal_stats_postphase_timeout_records_disposition( 435 journal_copy, monkeypatch 436): 437 mod = importlib.import_module("solstone.think.thinking") 438 daily_called = [] 439 440 def fake_bounded(cmd, day, timeout=None): 441 if cmd[:2] == ["journal", "journal-stats"]: 442 return (False, True) 443 return (True, False) 444 445 def fake_daily(day, verbose, **kwargs): 446 daily_called.append(day) 447 return (5, 0, [], set()) 448 449 _patch_main_runtime(monkeypatch) 450 monkeypatch.setattr(mod, "run_bounded_phase", fake_bounded) 451 monkeypatch.setattr(mod, "run_command", lambda cmd, day: True) 452 monkeypatch.setattr(mod, "run_queued_command", lambda cmd, day, timeout=600: True) 453 monkeypatch.setattr(mod, "run_daily_prompts", fake_daily) 454 monkeypatch.setattr("sys.argv", ["sol think", "--day", "20240101"]) 455 456 # main() returning normally proves the post-phase did not wedge the pipeline. 457 mod.main() 458 459 health_dir = journal_copy / "chronicle" / "20240101" / "health" 460 daily_files = sorted(health_dir.glob("*_daily.jsonl")) 461 assert len(daily_files) == 1 462 events = _read_jsonl(daily_files[0]) 463 completes = [ 464 e 465 for e in events 466 if _event_name(e) == "phase.complete" and e.get("phase") == "journal_stats" 467 ] 468 assert len(completes) == 1 469 complete = completes[0] 470 assert complete["success"] is False 471 assert complete["reason_code"] == "wall_clock_exceeded" 472 assert complete["timeout_seconds"] == mod.JOURNAL_STATS_MAX_RUNTIME 473 assert complete["bounded"] is True 474 assert daily_called 475 476 477def test_daily_postphase_emits_mixed_storage_warnings(journal_copy, monkeypatch): 478 mod = importlib.import_module("solstone.think.thinking") 479 callosum = importlib.import_module("solstone.think.callosum") 480 retention = importlib.import_module("solstone.think.retention") 481 sent = [] 482 warnings = [ 483 { 484 "level": "warning", 485 "type": "disk_percent", 486 "message": "disk warning", 487 "current": 95.0, 488 "threshold": 80, 489 }, 490 { 491 "level": "warning", 492 "type": "offload_stalled", 493 "message": "offload warning", 494 "current": None, 495 "threshold": None, 496 }, 497 ] 498 499 def fake_daily(day, verbose, **kwargs): 500 return (5, 0, [], set()) 501 502 def fake_send(tract, event, **fields): 503 sent.append((tract, event, fields)) 504 return True 505 506 _patch_main_runtime(monkeypatch) 507 monkeypatch.setattr( 508 mod, "run_bounded_phase", lambda cmd, day, timeout=None: (True, False) 509 ) 510 monkeypatch.setattr(mod, "run_command", lambda cmd, day: True) 511 monkeypatch.setattr(mod, "run_queued_command", lambda cmd, day, timeout=600: True) 512 monkeypatch.setattr(mod, "run_daily_prompts", fake_daily) 513 monkeypatch.setattr(retention, "compute_storage_summary", lambda: object()) 514 monkeypatch.setattr( 515 retention, 516 "check_storage_health", 517 lambda summary, journal_path: warnings, 518 ) 519 monkeypatch.setattr(callosum, "callosum_send", fake_send) 520 monkeypatch.setattr("sys.argv", ["sol think", "--day", "20240101"]) 521 522 mod.main() 523 524 storage = [item for item in sent if item[:2] == ("storage", "warning")] 525 notification = [item for item in sent if item[:2] == ("notification", "show")] 526 assert storage == [ 527 ( 528 "storage", 529 "warning", 530 { 531 "level": "warning", 532 "type": "disk_percent", 533 "message": "disk warning", 534 "current": 95.0, 535 "threshold": 80, 536 }, 537 ), 538 ( 539 "storage", 540 "warning", 541 { 542 "level": "warning", 543 "type": "offload_stalled", 544 "message": "offload warning", 545 "current": None, 546 "threshold": None, 547 }, 548 ), 549 ] 550 assert notification == [ 551 ( 552 "notification", 553 "show", 554 { 555 "title": "Storage Warning", 556 "message": "disk warning", 557 "action": "/app/settings#storage", 558 }, 559 ) 560 ] 561 562 563def test_daily_segment_prephase_failure_has_no_timeout_reason( 564 journal_copy, monkeypatch 565): 566 mod = importlib.import_module("solstone.think.thinking") 567 daily_called = [] 568 569 def fake_daily(day, verbose, **kwargs): 570 daily_called.append(day) 571 return (5, 0, [], set()) 572 573 _patch_main_runtime(monkeypatch) 574 monkeypatch.setattr( 575 mod, "run_bounded_phase", lambda cmd, day, timeout=None: (False, False) 576 ) 577 monkeypatch.setattr(mod, "run_command", lambda cmd, day: True) 578 monkeypatch.setattr(mod, "run_queued_command", lambda cmd, day, timeout=600: True) 579 monkeypatch.setattr(mod, "run_daily_prompts", fake_daily) 580 monkeypatch.setattr("sys.argv", ["sol think", "--day", "20240101"]) 581 582 mod.main() 583 584 health_dir = journal_copy / "chronicle" / "20240101" / "health" 585 daily_files = sorted(health_dir.glob("*_daily.jsonl")) 586 assert len(daily_files) == 1 587 events = _read_jsonl(daily_files[0]) 588 segment_completes = [ 589 event 590 for event in events 591 if _event_name(event) == "phase.complete" 592 and event.get("phase") == "segment_think" 593 ] 594 assert len(segment_completes) == 1 595 complete = segment_completes[0] 596 assert complete["success"] is False 597 assert "reason_code" not in complete 598 assert "timeout_seconds" not in complete 599 assert "bounded" not in complete 600 assert daily_called 601 602 603def test_run_bounded_phase_timeout_with_exit_zero_propagates_failure( 604 journal_copy, monkeypatch 605): 606 mod = importlib.import_module("solstone.think.thinking") 607 log_path = journal_copy / "x.log" 608 fake = Mock() 609 fake.log_writer = Mock() 610 fake.log_writer.path = log_path 611 fake.name = "test" 612 fake.wait.side_effect = subprocess.TimeoutExpired(cmd=["x"], timeout=0.01) 613 fake.terminate.return_value = 0 614 fake.cleanup.return_value = None 615 monkeypatch.setattr( 616 "solstone.think.runner.ManagedProcess.spawn", lambda *a, **k: fake 617 ) 618 619 result = mod.run_bounded_phase( 620 ["journal", "think", "--segments", "--day", DAY], 621 DAY, 622 timeout=0.01, 623 ) 624 625 assert result == (False, True) 626 627 628def test_segment_health_log_receives_segment_talent_events(tmp_path, monkeypatch): 629 from solstone.think import thinking as think 630 631 journal = tmp_path / "journal" 632 monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal)) 633 _seed_segment(journal, DAY, ACTIVE_SEGMENT) 634 635 segments = think.cluster_segments(DAY) 636 assert any(segment["key"] == ACTIVE_SEGMENT for segment in segments) 637 638 _patch_main_runtime(monkeypatch) 639 # A tmp journal carries no local artifacts, so the real predicate resolves 640 # non-local and the default must never probe the server. 641 _forbid_slot_discovery(monkeypatch, think) 642 monkeypatch.setattr( 643 think, 644 "get_talent_configs", 645 lambda schedule=None, **kwargs: _segment_configs("sense"), 646 ) 647 monkeypatch.setattr( 648 think, 649 "cortex_request", 650 lambda prompt, name, config=None, **kwargs: f"agent-{name}", 651 ) 652 monkeypatch.setattr( 653 think, 654 "wait_for_uses", 655 lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), 656 ) 657 monkeypatch.setattr("sys.argv", ["sol think", "--segments", "--day", DAY]) 658 659 with pytest.raises(SystemExit) as excinfo: 660 think.main() 661 662 assert excinfo.value.code == 0 663 health_dir = journal / "chronicle" / DAY / "health" 664 segment_files = sorted(health_dir.glob("*_segment.jsonl")) 665 assert len(segment_files) == 1 666 assert any( 667 _event_name(event).startswith("talent.") 668 for event in _read_jsonl(segment_files[0]) 669 ) 670 assert not list(health_dir.glob("*_daily.jsonl")) 671 672 673def test_select_segment_repair_targets_skips_complete_and_unsensed(monkeypatch): 674 from solstone.think import thinking as think 675 676 complete = { 677 "key": "090000_300", 678 "stream": STREAM, 679 "data_state": {"screen": "analyzed"}, 680 } 681 incomplete = { 682 "key": "090500_300", 683 "stream": STREAM, 684 "data_state": {"screen": "analyzed"}, 685 } 686 raw_blocked = { 687 "key": "091000_300", 688 "stream": STREAM, 689 "data_state": {"screen": "pending"}, 690 } 691 segments = [complete, incomplete, raw_blocked] 692 monkeypatch.setattr( 693 think, 694 "read_segment_progress", 695 lambda day: {(STREAM, complete["key"]): _complete_segment_progress()}, 696 ) 697 698 selected, counts = think._select_segment_repair_targets( 699 DAY, 700 segments, 701 force_all=False, 702 ) 703 704 assert selected == [incomplete] 705 assert counts == { 706 "total": 3, 707 "selected": 1, 708 "complete": 1, 709 "raw_blocked": 1, 710 } 711 712 713def test_select_segment_repair_targets_force_all_preserves_refresh_semantics( 714 monkeypatch, 715): 716 from solstone.think import thinking as think 717 718 segments = [ 719 { 720 "key": "090000_300", 721 "stream": STREAM, 722 "data_state": {"screen": "analyzed"}, 723 }, 724 { 725 "key": "090500_300", 726 "stream": STREAM, 727 "data_state": {"screen": "pending"}, 728 }, 729 ] 730 monkeypatch.setattr(think, "read_segment_progress", lambda day: {}) 731 732 selected, counts = think._select_segment_repair_targets( 733 DAY, 734 segments, 735 force_all=True, 736 ) 737 738 assert selected == segments 739 assert counts == { 740 "total": 2, 741 "selected": 2, 742 "complete": 0, 743 "raw_blocked": 0, 744 } 745 746 747def test_raw_media_pending_skip_becomes_repair_selectable_after_describe_output( 748 monkeypatch, 749): 750 from solstone.think import thinking as think 751 752 pending = { 753 "key": "091000_300", 754 "stream": STREAM, 755 "data_state": {"screen": "pending"}, 756 } 757 sensed = { 758 "key": pending["key"], 759 "stream": STREAM, 760 "data_state": {"screen": "analyzed"}, 761 } 762 monkeypatch.setattr(think, "read_segment_progress", lambda day: {}) 763 764 selected, counts = think._select_segment_repair_targets( 765 DAY, 766 [pending], 767 force_all=False, 768 ) 769 assert selected == [] 770 assert counts == { 771 "total": 1, 772 "selected": 0, 773 "complete": 0, 774 "raw_blocked": 1, 775 } 776 777 selected, counts = think._select_segment_repair_targets( 778 DAY, 779 [sensed], 780 force_all=False, 781 ) 782 assert selected == [sensed] 783 assert counts == { 784 "total": 1, 785 "selected": 1, 786 "complete": 0, 787 "raw_blocked": 0, 788 } 789 790 791def test_media_terminal_no_sense_complete_is_selected_and_dispatched(monkeypatch): 792 from solstone.think import thinking as think 793 from solstone.think.pipeline_health import classify_segment_completion 794 795 segment = { 796 "key": "090000_300", 797 "stream": STREAM, 798 "start": "09:00", 799 "end": "09:05", 800 "data_state": {"screen": "analyzed"}, 801 } 802 monkeypatch.setattr(think, "read_segment_progress", lambda day: {}) 803 804 completion = classify_segment_completion([segment], {}) 805 assert completion.blockers == [ 806 { 807 "segment": segment["key"], 808 "dimension": "not_thought", 809 "detail": "no_sense_complete", 810 } 811 ] 812 813 selected, counts = think._select_segment_repair_targets( 814 DAY, 815 [segment], 816 force_all=False, 817 ) 818 assert selected == [segment] 819 assert counts == { 820 "total": 1, 821 "selected": 1, 822 "complete": 0, 823 "raw_blocked": 0, 824 } 825 826 dispatched = [] 827 828 def fake_run_segment_sense(**kwargs): 829 dispatched.append((kwargs["stream"], kwargs["segment"])) 830 return (1, 0, []) 831 832 monkeypatch.setattr(think, "run_segment_sense", fake_run_segment_sense) 833 monkeypatch.setattr(think, "resolve_predecessor", lambda *args: None) 834 835 success, failed = think._run_segment_repair_batch( 836 day=DAY, 837 segments=selected, 838 refresh=False, 839 verbose=False, 840 max_concurrency=1, 841 segment_workers=1, 842 timeout=None, 843 skip_activity_prompts=False, 844 skip_talents=frozenset(), 845 ) 846 847 assert (success, failed) == (1, 0) 848 assert dispatched == [(STREAM, segment["key"])] 849 850 851def test_run_segment_repair_batch_respects_worker_bound(monkeypatch): 852 from solstone.think import thinking as think 853 854 segments = [ 855 {"key": f"090{i}00_300", "stream": STREAM, "start": "09:00", "end": "09:05"} 856 for i in range(4) 857 ] 858 lock = threading.Lock() 859 barrier = threading.Barrier(2, timeout=1.0) 860 current = 0 861 peak = 0 862 863 def fake_run_segment_sense(**kwargs): 864 nonlocal current, peak 865 with lock: 866 current += 1 867 peak = max(peak, current) 868 try: 869 barrier.wait() 870 except threading.BrokenBarrierError: 871 pass 872 time.sleep(0.02) 873 with lock: 874 current -= 1 875 return (1, 0, []) 876 877 monkeypatch.setattr(think, "run_segment_sense", fake_run_segment_sense) 878 monkeypatch.setattr(think, "resolve_predecessor", lambda *args: None) 879 880 success, failed = think._run_segment_repair_batch( 881 day=DAY, 882 segments=segments, 883 refresh=False, 884 verbose=False, 885 max_concurrency=2, 886 segment_workers=2, 887 timeout=None, 888 skip_activity_prompts=False, 889 skip_talents=frozenset(), 890 ) 891 892 assert (success, failed) == (4, 0) 893 assert peak == 2 894 895 896def test_segments_mode_targets_incomplete_tail_and_replays_full_day( 897 tmp_path, 898 monkeypatch, 899): 900 from solstone.think import thinking as think 901 from solstone.think import utils as think_utils 902 903 day = "20240117" 904 journal = tmp_path / "journal" 905 (journal / "chronicle" / day).mkdir(parents=True) 906 monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal)) 907 monkeypatch.setenv("SOL_SKIP_SUPERVISOR_CHECK", "1") 908 think_utils._journal_path_cache = None 909 _patch_main_runtime(monkeypatch) 910 911 complete = { 912 "key": "090000_300", 913 "stream": STREAM, 914 "start": "09:00", 915 "end": "09:05", 916 "data_state": {"screen": "analyzed"}, 917 } 918 incomplete = { 919 "key": "090500_300", 920 "stream": STREAM, 921 "start": "09:05", 922 "end": "09:10", 923 "data_state": {"screen": "analyzed"}, 924 } 925 raw_blocked = { 926 "key": "091000_300", 927 "stream": STREAM, 928 "start": "09:10", 929 "end": "09:15", 930 "data_state": {"screen": "pending"}, 931 } 932 segments = [complete, incomplete, raw_blocked] 933 calls: list[dict] = [] 934 replay_calls: list[dict] = [] 935 936 def fake_run_segment_sense(**kwargs): 937 calls.append(kwargs) 938 return (1, 0, []) 939 940 monkeypatch.setattr(think, "cluster_segments", lambda day: segments) 941 monkeypatch.setattr( 942 think, 943 "read_segment_progress", 944 lambda day: {(STREAM, complete["key"]): _complete_segment_progress()}, 945 ) 946 monkeypatch.setattr(think, "run_segment_sense", fake_run_segment_sense) 947 monkeypatch.setattr(think, "resolve_predecessor", lambda *args: None) 948 monkeypatch.setattr( 949 think, 950 "_replay_activity_state_for_segments", 951 lambda **kwargs: replay_calls.append(kwargs), 952 ) 953 monkeypatch.setattr( 954 "sys.argv", 955 ["sol think", "--segments", "--day", day, "--segment-workers", "1"], 956 ) 957 958 with pytest.raises(SystemExit) as excinfo: 959 think.main() 960 961 assert excinfo.value.code == 0 962 assert [call["segment"] for call in calls] == [incomplete["key"]] 963 assert calls[0]["state_machine"] is None 964 assert calls[0]["skip_activity_prompts"] is True 965 assert replay_calls[0]["segments"] == segments 966 967 968def test_segments_mode_complete_day_noops_without_dispatch(tmp_path, monkeypatch): 969 from solstone.think import thinking as think 970 from solstone.think import utils as think_utils 971 972 day = "20240118" 973 journal = tmp_path / "journal" 974 (journal / "chronicle" / day).mkdir(parents=True) 975 monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal)) 976 monkeypatch.setenv("SOL_SKIP_SUPERVISOR_CHECK", "1") 977 think_utils._journal_path_cache = None 978 _patch_main_runtime(monkeypatch) 979 980 segment = { 981 "key": "090000_300", 982 "stream": STREAM, 983 "start": "09:00", 984 "end": "09:05", 985 "data_state": {"screen": "analyzed"}, 986 } 987 calls: list[dict] = [] 988 replay_calls: list[dict] = [] 989 monkeypatch.setattr(think, "cluster_segments", lambda day: [segment]) 990 monkeypatch.setattr( 991 think, 992 "read_segment_progress", 993 lambda day: {(STREAM, segment["key"]): _complete_segment_progress()}, 994 ) 995 monkeypatch.setattr( 996 think, 997 "run_segment_sense", 998 lambda **kwargs: calls.append(kwargs) or (1, 0, []), 999 ) 1000 monkeypatch.setattr( 1001 think, 1002 "_replay_activity_state_for_segments", 1003 lambda **kwargs: replay_calls.append(kwargs), 1004 ) 1005 monkeypatch.setattr( 1006 "sys.argv", 1007 ["sol think", "--segments", "--day", day, "--segment-workers", "1"], 1008 ) 1009 1010 with pytest.raises(SystemExit) as excinfo: 1011 think.main() 1012 1013 assert excinfo.value.code == 0 1014 assert calls == [] 1015 assert replay_calls == [] 1016 1017 1018def test_existing_segment_talent_output_prevents_second_llm_run( 1019 tmp_path, 1020 monkeypatch, 1021): 1022 from solstone.think import models, talents 1023 1024 journal = tmp_path / "journal" 1025 monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal)) 1026 out = journal / "chronicle" / DAY / STREAM / ACTIVE_SEGMENT / "talents" / "flow.md" 1027 events: list[dict] = [] 1028 called: list[str] = [] 1029 1030 def fake_generate_with_result(**kwargs): 1031 called.append("generate") 1032 return {"text": "FRESH", "usage": {"output_tokens": 500}} 1033 1034 monkeypatch.setattr(models, "generate_with_result", fake_generate_with_result) 1035 monkeypatch.setattr(talents, "_run_pre_hooks", lambda config: {}) 1036 1037 config = { 1038 "type": "generate", 1039 "name": "flow", 1040 "provider": "google", 1041 "model": "x", 1042 "prompt": "think about this segment", 1043 "day": DAY, 1044 "segment": ACTIVE_SEGMENT, 1045 "stream": STREAM, 1046 "output_path": str(out), 1047 "output": "md", 1048 "refresh": False, 1049 "schedule": "segment", 1050 } 1051 1052 # Lode-level analogue of test_talent_fallback's guard: the second run is 1053 # the healthy-day segment re-think and must use the cached output. 1054 asyncio.run(talents._run_talent(config, events.append, dry_run=False)) 1055 assert len(called) == 1 1056 assert out.exists() 1057 1058 _append_segment_terminal(journal, name="flow") 1059 1060 second_events: list[dict] = [] 1061 asyncio.run(talents._run_talent(config, second_events.append, dry_run=False)) 1062 1063 finish_events = [event for event in second_events if event.get("event") == "finish"] 1064 assert len(called) == 1 1065 assert finish_events[-1]["result"] == "FRESH" 1066 assert finish_events[-1]["cache_hit"] is True 1067 assert finish_events[-1]["output_changed"] is False 1068 assert "usage" not in finish_events[-1] 1069 1070 changed_config = dict(config) 1071 changed_config["model"] = "y" 1072 third_events: list[dict] = [] 1073 asyncio.run(talents._run_talent(changed_config, third_events.append, dry_run=False)) 1074 assert len(called) == 2 1075 third_finish = [event for event in third_events if event.get("event") == "finish"][ 1076 -1 1077 ] 1078 assert third_finish["cache_hit"] is False 1079 1080 transcript_changed_config = dict(changed_config) 1081 transcript_changed_config["transcript"] = "new source content from the segment" 1082 fourth_events: list[dict] = [] 1083 asyncio.run( 1084 talents._run_talent( 1085 transcript_changed_config, 1086 fourth_events.append, 1087 dry_run=False, 1088 ) 1089 ) 1090 assert len(called) == 3 1091 fourth_finish = [ 1092 event for event in fourth_events if event.get("event") == "finish" 1093 ][-1] 1094 assert fourth_finish["cache_hit"] is False 1095 1096 1097def test_provenance_reuse_requires_successful_latest_terminal( 1098 tmp_path, 1099 monkeypatch, 1100): 1101 from solstone.think import models, talents 1102 1103 journal = tmp_path / "journal" 1104 monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal)) 1105 out = ( 1106 journal / "chronicle" / DAY / STREAM / ACTIVE_SEGMENT / "talents" / "screen.md" 1107 ) 1108 called: list[str] = [] 1109 1110 def fake_generate_with_result(**kwargs): 1111 called.append("generate") 1112 return {"text": "FRESH", "usage": {"output_tokens": 500}} 1113 1114 monkeypatch.setattr(models, "generate_with_result", fake_generate_with_result) 1115 monkeypatch.setattr(talents, "_run_pre_hooks", lambda config: {}) 1116 1117 config = { 1118 "type": "generate", 1119 "name": "screen", 1120 "provider": "google", 1121 "model": "x", 1122 "prompt": "think about this segment", 1123 "day": DAY, 1124 "segment": ACTIVE_SEGMENT, 1125 "stream": STREAM, 1126 "output_path": str(out), 1127 "output": "md", 1128 "refresh": False, 1129 "schedule": "segment", 1130 } 1131 1132 asyncio.run(talents._run_talent(config, lambda event: None, dry_run=False)) 1133 _append_segment_terminal(journal, name="screen", ts=100) 1134 _append_segment_terminal(journal, name="screen", event="talent.fail", ts=200) 1135 1136 events: list[dict] = [] 1137 asyncio.run(talents._run_talent(config, events.append, dry_run=False)) 1138 1139 assert len(called) == 2 1140 finish = [event for event in events if event.get("event") == "finish"][-1] 1141 assert finish["cache_hit"] is False 1142 1143 1144def test_refresh_identical_output_regenerates_without_output_changed( 1145 tmp_path, 1146 monkeypatch, 1147): 1148 from solstone.think import models, talents 1149 1150 journal = tmp_path / "journal" 1151 monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal)) 1152 out = ( 1153 journal / "chronicle" / DAY / STREAM / ACTIVE_SEGMENT / "talents" / "screen.md" 1154 ) 1155 out.parent.mkdir(parents=True, exist_ok=True) 1156 out.write_text("SAME", encoding="utf-8") 1157 called: list[str] = [] 1158 1159 def fake_generate_with_result(**kwargs): 1160 called.append("generate") 1161 return {"text": "SAME", "usage": {"output_tokens": 500}} 1162 1163 monkeypatch.setattr(models, "generate_with_result", fake_generate_with_result) 1164 monkeypatch.setattr(talents, "_run_pre_hooks", lambda config: {}) 1165 1166 events: list[dict] = [] 1167 asyncio.run( 1168 talents._run_talent( 1169 { 1170 "type": "generate", 1171 "name": "screen", 1172 "provider": "google", 1173 "model": "x", 1174 "prompt": "think about this segment", 1175 "day": DAY, 1176 "segment": ACTIVE_SEGMENT, 1177 "stream": STREAM, 1178 "output_path": str(out), 1179 "output": "md", 1180 "refresh": True, 1181 "schedule": "segment", 1182 }, 1183 events.append, 1184 dry_run=False, 1185 ) 1186 ) 1187 1188 finish = [event for event in events if event.get("event") == "finish"][-1] 1189 assert len(called) == 1 1190 assert finish["cache_hit"] is False 1191 assert finish["output_changed"] is False 1192 1193 1194def test_json_cache_reuse_requires_current_schema_validation( 1195 tmp_path, 1196 monkeypatch, 1197): 1198 from solstone.think import models, talents 1199 from solstone.think.talent_provenance import output_digest, write_provenance 1200 1201 journal = tmp_path / "journal" 1202 monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal)) 1203 out = ( 1204 journal / "chronicle" / DAY / STREAM / ACTIVE_SEGMENT / "talents" / "sense.json" 1205 ) 1206 out.parent.mkdir(parents=True, exist_ok=True) 1207 out.write_text('{"bad": true}', encoding="utf-8") 1208 1209 config = { 1210 "type": "generate", 1211 "name": "sense", 1212 "provider": "google", 1213 "model": "x", 1214 "prompt": "think about this segment", 1215 "day": DAY, 1216 "segment": ACTIVE_SEGMENT, 1217 "stream": STREAM, 1218 "output_path": str(out), 1219 "output": "json", 1220 "json_schema": { 1221 "type": "object", 1222 "required": ["ok"], 1223 "properties": {"ok": {"type": "boolean"}}, 1224 }, 1225 "refresh": False, 1226 "schedule": "segment", 1227 } 1228 runtime_schema = talents.hydrate_runtime_enums(config["json_schema"]) 1229 output_sha256, output_size = output_digest(out) 1230 write_provenance( 1231 out, 1232 identity_hash=talents._identity_hash(config, runtime_schema), 1233 output_sha256=output_sha256, 1234 output_size=output_size, 1235 provider="google", 1236 model="x", 1237 generation_params=talents._generation_params(config), 1238 completed_at_ms=100, 1239 use_id="seed", 1240 identity_fields={"name": "sense"}, 1241 ) 1242 _append_segment_terminal(journal, name="sense", ts=100) 1243 1244 called: list[str] = [] 1245 1246 def fake_generate_with_result(**kwargs): 1247 called.append("generate") 1248 return { 1249 "text": '{"ok": true}', 1250 "usage": {"output_tokens": 500}, 1251 "schema_validation": {"valid": True, "errors": []}, 1252 } 1253 1254 monkeypatch.setattr(models, "generate_with_result", fake_generate_with_result) 1255 monkeypatch.setattr(talents, "_run_pre_hooks", lambda config: {}) 1256 1257 events: list[dict] = [] 1258 asyncio.run(talents._run_talent(config, events.append, dry_run=False)) 1259 1260 assert len(called) == 1 1261 finish = [event for event in events if event.get("event") == "finish"][-1] 1262 assert finish["cache_hit"] is False 1263 assert out.read_text(encoding="utf-8") == '{"ok": true}' 1264 1265 1266def test_identity_hash_ignores_source_count_vocabulary(): 1267 from solstone.think import talents 1268 1269 config = { 1270 "type": "generate", 1271 "name": "sense", 1272 "provider": "google", 1273 "model": "x", 1274 "prompt": "think about this segment", 1275 "day": DAY, 1276 "segment": ACTIVE_SEGMENT, 1277 "stream": STREAM, 1278 "output": "json", 1279 "json_schema": { 1280 "type": "object", 1281 "required": ["ok"], 1282 "properties": {"ok": {"type": "boolean"}}, 1283 }, 1284 "schedule": "segment", 1285 "sources": { 1286 "transcripts": True, 1287 "percepts": False, 1288 "talents": {"sense": True}, 1289 }, 1290 "source_counts": {"transcripts": 1, "percepts": 0, "agents": 1}, 1291 } 1292 runtime_schema = talents.hydrate_runtime_enums(config["json_schema"]) 1293 renamed_counts = { 1294 **config, 1295 "source_counts": {"transcripts": 1, "percepts": 0, "talents": 1}, 1296 } 1297 1298 assert talents._identity_hash(config, runtime_schema) == talents._identity_hash( 1299 renamed_counts, 1300 runtime_schema, 1301 ) 1302 1303 1304def test_activity_replay_dedupes_records_and_preserves_non_refresh( 1305 tmp_path, 1306 monkeypatch, 1307): 1308 from solstone.think import thinking as think 1309 from solstone.think.activities import make_activity_id 1310 from solstone.think.activity_state_machine import ActivityStateMachine 1311 1312 journal = tmp_path / "journal" 1313 monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal)) 1314 _seed_segment(journal, DAY, ACTIVE_SEGMENT, _active_sense()) 1315 _seed_segment(journal, DAY, IDLE_SEGMENT, _idle_sense()) 1316 1317 activity_calls: list[dict] = [] 1318 1319 monkeypatch.setattr( 1320 think, 1321 "get_talent_configs", 1322 lambda schedule=None, **kwargs: _segment_configs("sense"), 1323 ) 1324 monkeypatch.setattr( 1325 think, 1326 "cortex_request", 1327 lambda prompt, name, config=None, **kwargs: f"agent-{name}", 1328 ) 1329 monkeypatch.setattr( 1330 think, 1331 "wait_for_uses", 1332 lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), 1333 ) 1334 monkeypatch.setattr( 1335 think, 1336 "run_activity_prompts", 1337 lambda **kwargs: activity_calls.append(kwargs) or True, 1338 ) 1339 monkeypatch.setattr(think, "_callosum", None) 1340 health_log = journal / "chronicle" / DAY / "health" / "activity_test.jsonl" 1341 writer = think.ThinkingJSONLWriter(str(health_log)) 1342 monkeypatch.setattr(think, "_jsonl", writer) 1343 1344 try: 1345 for _ in range(2): 1346 state_machine = ActivityStateMachine() 1347 for segment in (ACTIVE_SEGMENT, IDLE_SEGMENT): 1348 think.run_segment_sense( 1349 DAY, 1350 segment, 1351 refresh=False, 1352 verbose=False, 1353 stream=STREAM, 1354 state_machine=state_machine, 1355 ) 1356 1357 changed = _active_sense() 1358 changed["activity_summary"] = "Writing tests with new context" 1359 _write_sense_output( 1360 journal / "chronicle" / DAY / STREAM / ACTIVE_SEGMENT, 1361 changed, 1362 ) 1363 state_machine = ActivityStateMachine() 1364 for segment in (ACTIVE_SEGMENT, IDLE_SEGMENT): 1365 think.run_segment_sense( 1366 DAY, 1367 segment, 1368 refresh=False, 1369 verbose=False, 1370 stream=STREAM, 1371 state_machine=state_machine, 1372 ) 1373 finally: 1374 writer.close() 1375 1376 record_path = journal / "facets" / FACET / "activities" / f"{DAY}.jsonl" 1377 records = _read_jsonl(record_path) 1378 activity_id = make_activity_id("coding", ACTIVE_SEGMENT) 1379 matching = [record for record in records if record.get("id") == activity_id] 1380 1381 assert len(matching) == 1 1382 assert len(activity_calls) == 2 1383 assert activity_calls[0]["refresh"] is False 1384 assert activity_calls[1]["refresh"] is False 1385 health_events = _read_jsonl(health_log) 1386 assert any(event.get("event") == "activity.unchanged" for event in health_events) 1387 1388 1389def test_activity_replay_treats_malformed_sense_like_absent( 1390 tmp_path, 1391 monkeypatch, 1392 caplog, 1393): 1394 from solstone.think import thinking as think 1395 from solstone.think.activity_state_machine import ActivityStateMachine 1396 1397 ending_segment = "091000_300" 1398 segments = [ 1399 {"stream": STREAM, "key": ACTIVE_SEGMENT}, 1400 {"stream": STREAM, "key": IDLE_SEGMENT}, 1401 {"stream": STREAM, "key": ending_segment}, 1402 ] 1403 1404 monkeypatch.setattr( 1405 "solstone.think.activity_state_machine.time.time", 1406 lambda: 123.456, 1407 ) 1408 monkeypatch.setattr( 1409 think, 1410 "run_activity_prompts", 1411 lambda **kwargs: True, 1412 ) 1413 monkeypatch.setattr(think, "_callosum", None) 1414 state_machines: list[ActivityStateMachine] = [] 1415 1416 class CapturingActivityStateMachine(ActivityStateMachine): 1417 def __init__(self, *args, **kwargs): 1418 super().__init__(*args, **kwargs) 1419 state_machines.append(self) 1420 1421 monkeypatch.setattr(think, "ActivityStateMachine", CapturingActivityStateMachine) 1422 1423 def run_case(journal: Path, *, malformed: bool) -> tuple[bytes, bytes]: 1424 start_index = len(state_machines) 1425 monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal)) 1426 _seed_segment(journal, DAY, ACTIVE_SEGMENT, _active_sense()) 1427 middle_dir = _seed_segment(journal, DAY, IDLE_SEGMENT, _active_sense()) 1428 _seed_segment(journal, DAY, ending_segment, _idle_sense()) 1429 middle_sense = middle_dir / "talents" / "sense.json" 1430 if malformed: 1431 _write_sense_output( 1432 middle_dir, 1433 { 1434 "density": "active", 1435 "activity_summary": "Malformed cached output", 1436 "entities": [], 1437 "facets": [{"facet": FACET, "level": "high"}], 1438 "recommend": {}, 1439 }, 1440 ) 1441 else: 1442 middle_sense.unlink() 1443 1444 think._replay_activity_state_for_segments( 1445 day=DAY, 1446 segments=segments, 1447 refresh=False, 1448 verbose=False, 1449 max_concurrency=2, 1450 skip_activity_prompts=False, 1451 ) 1452 record_path = journal / "facets" / FACET / "activities" / f"{DAY}.jsonl" 1453 state_machine = state_machines[start_index] 1454 state_snapshot = { 1455 "last_segment_key": state_machine.last_segment_key, 1456 "last_segment_day": state_machine.last_segment_day, 1457 "active": { 1458 facet: {k: v for k, v in entry.items() if k != "_change"} 1459 for facet, entry in state_machine.state.items() 1460 }, 1461 } 1462 return record_path.read_bytes(), json.dumps( 1463 state_snapshot, sort_keys=True 1464 ).encode() 1465 1466 caplog.set_level(logging.WARNING) 1467 malformed_records, malformed_state = run_case( 1468 tmp_path / "malformed", malformed=True 1469 ) 1470 absent_records, absent_state = run_case(tmp_path / "absent", malformed=False) 1471 1472 assert malformed_records == absent_records 1473 assert malformed_state == absent_state 1474 assert "missing required keys: content_type" in caplog.text 1475 1476 1477@pytest.mark.parametrize("missing_key", ["density", "content_type"]) 1478def test_run_segment_sense_reports_invalid_sense_output( 1479 tmp_path, 1480 monkeypatch, 1481 caplog, 1482 missing_key, 1483): 1484 from solstone.think import thinking as think 1485 from solstone.think.activity_state_machine import ActivityStateMachine 1486 1487 journal = tmp_path / "journal" 1488 monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal)) 1489 malformed = _active_sense() 1490 malformed.pop(missing_key) 1491 _seed_segment(journal, DAY, ACTIVE_SEGMENT, malformed) 1492 1493 monkeypatch.setattr( 1494 think, 1495 "get_talent_configs", 1496 lambda schedule=None, **kwargs: _segment_configs("sense"), 1497 ) 1498 monkeypatch.setattr( 1499 think, 1500 "cortex_request", 1501 lambda prompt, name, config=None, **kwargs: f"agent-{name}", 1502 ) 1503 monkeypatch.setattr( 1504 think, 1505 "wait_for_uses", 1506 lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), 1507 ) 1508 emitted: list[tuple[str, str, dict]] = [] 1509 1510 class CaptureCallosum: 1511 def emit(self, tract, event, **fields): 1512 emitted.append((tract, event, fields)) 1513 1514 monkeypatch.setattr(think, "_callosum", CaptureCallosum()) 1515 caplog.set_level(logging.WARNING) 1516 1517 success, failed, failed_names = think.run_segment_sense( 1518 DAY, 1519 ACTIVE_SEGMENT, 1520 refresh=False, 1521 verbose=False, 1522 stream=STREAM, 1523 state_machine=ActivityStateMachine(), 1524 ) 1525 1526 assert success == 1 1527 assert failed == 1 1528 assert failed_names == ["sense (output_invalid)"] 1529 assert f"missing required keys: {missing_key}" in caplog.text 1530 completed = [event for event in emitted if event[1] == "completed"] 1531 assert completed[-1][2]["failed"] == 1 1532 assert completed[-1][2]["failed_names"] == ["sense (output_invalid)"] 1533 1534 1535def test_cache_hit_priority_drain_suppresses_rescan_but_counts_segment_complete( 1536 tmp_path, 1537 monkeypatch, 1538): 1539 from solstone.think import thinking as think 1540 from solstone.think.pipeline_health import ( 1541 read_completed_since, 1542 read_segment_progress, 1543 ) 1544 1545 journal = tmp_path / "journal" 1546 monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal)) 1547 output_path = ( 1548 journal / "chronicle" / DAY / STREAM / ACTIVE_SEGMENT / "talents" / "screen.md" 1549 ) 1550 output_path.parent.mkdir(parents=True, exist_ok=True) 1551 output_path.write_text("cached", encoding="utf-8") 1552 1553 use_id = "1700000000000" 1554 use_dir = journal / "talents" / "screen" 1555 use_dir.mkdir(parents=True, exist_ok=True) 1556 (use_dir / f"{use_id}.jsonl").write_text( 1557 json.dumps( 1558 { 1559 "event": "finish", 1560 "ts": 300, 1561 "use_id": use_id, 1562 "name": "screen", 1563 "result": "cached", 1564 "cache_hit": True, 1565 "output_changed": False, 1566 "completed_at_ms": 100, 1567 } 1568 ) 1569 + "\n", 1570 encoding="utf-8", 1571 ) 1572 1573 health_log = journal / "chronicle" / DAY / "health" / "segment.jsonl" 1574 writer = think.ThinkingJSONLWriter(str(health_log)) 1575 monkeypatch.setattr(think, "_jsonl", writer) 1576 monkeypatch.setattr( 1577 think, 1578 "wait_for_uses", 1579 lambda agent_ids, timeout=610: ({use_id: "finish"}, []), 1580 ) 1581 rescan_calls: list[list[str]] = [] 1582 monkeypatch.setattr( 1583 think, 1584 "run_queued_command", 1585 lambda cmd, day, timeout=600: rescan_calls.append(cmd) or True, 1586 ) 1587 1588 try: 1589 success, failed, failed_names = think._drain_priority_batch( 1590 [(use_id, "screen", {"type": "generate", "output": "md"}, None)], 1591 "segment", 1592 DAY, 1593 ACTIVE_SEGMENT, 1594 STREAM, 1595 ) 1596 finally: 1597 writer.close() 1598 1599 assert (success, failed, failed_names) == (1, 0, []) 1600 assert rescan_calls == [] 1601 health_events = _read_jsonl(health_log) 1602 complete = [ 1603 event for event in health_events if event.get("event") == "talent.complete" 1604 ][-1] 1605 assert complete["cache_hit"] is True 1606 assert complete["completed_at_ms"] == 100 1607 assert read_completed_since(DAY, 0).segments == () 1608 assert "screen" in read_segment_progress(DAY)[(STREAM, ACTIVE_SEGMENT)].completed 1609 1610 1611def test_segments_mode_zero_segment_noop(tmp_path, monkeypatch, caplog): 1612 from solstone.think import thinking as think 1613 1614 day = "20240116" 1615 journal = tmp_path / "journal" 1616 monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal)) 1617 _patch_main_runtime(monkeypatch) 1618 _forbid_slot_discovery(monkeypatch, think) 1619 monkeypatch.setattr("sys.argv", ["sol think", "--segments", "--day", day]) 1620 1621 caplog.set_level(logging.INFO) 1622 with pytest.raises(SystemExit) as excinfo: 1623 think.main() 1624 1625 assert excinfo.value.code == 0 1626 assert f"No segments found for {day}" in caplog.text 1627 1628 1629def _patch_segment_run(monkeypatch, think, journal): 1630 """Seed a runnable segment and stub the talent machinery around it.""" 1631 _seed_segment(journal, DAY, ACTIVE_SEGMENT) 1632 _patch_main_runtime(monkeypatch) 1633 monkeypatch.setattr( 1634 think, 1635 "get_talent_configs", 1636 lambda schedule=None, **kwargs: _segment_configs("sense"), 1637 ) 1638 monkeypatch.setattr( 1639 think, 1640 "cortex_request", 1641 lambda prompt, name, config=None, **kwargs: f"agent-{name}", 1642 ) 1643 monkeypatch.setattr( 1644 think, 1645 "wait_for_uses", 1646 lambda agent_ids, timeout=600: ({aid: "finish" for aid in agent_ids}, []), 1647 ) 1648 1649 1650def test_segments_default_local_slot_fallback_logs_once_across_call_sites( 1651 tmp_path, monkeypatch, caplog 1652): 1653 """Discovery failure yields the floor value and logs the fallback once. 1654 1655 ``--segments`` calls ``default_segment_workers()`` twice in one process: 1656 once in argument validation and once in the run path. The tmp journal has 1657 no ``health/local.port``, so discovery fails without any network I/O. 1658 """ 1659 from solstone.think import thinking as think 1660 1661 journal = tmp_path / "journal" 1662 monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal)) 1663 _patch_segment_run(monkeypatch, think, journal) 1664 monkeypatch.setattr(fanout_policy, "_segment_work_uses_local", lambda: True) 1665 monkeypatch.setattr(fanout_policy.os, "cpu_count", lambda: 12) 1666 monkeypatch.setattr("sys.argv", ["sol think", "--segments", "--day", DAY]) 1667 1668 assert not (journal / "health" / "local.port").exists() 1669 1670 derived = [] 1671 original_default = fanout_policy.default_segment_workers 1672 1673 def spy_default() -> int: 1674 value = original_default() 1675 derived.append(value) 1676 return value 1677 1678 monkeypatch.setattr(fanout_policy, "default_segment_workers", spy_default) 1679 1680 caplog.set_level(logging.INFO) 1681 with pytest.raises(SystemExit) as excinfo: 1682 think.main() 1683 assert excinfo.value.code == 0 1684 1685 # Validation (thinking.py:4161) and the run path (:4270) both call it. 1686 assert derived == [1, 1] 1687 1688 messages = [record.getMessage() for record in caplog.records] 1689 fallbacks = [m for m in messages if "local_server_parallel_slots fallback" in m] 1690 assert fallbacks == [ 1691 "local_server_parallel_slots fallback slots=1 port=None " 1692 "context_tokens=None source=default" 1693 ] 1694 1695 caps = [m for m in messages if "default_segment_workers capped" in m] 1696 assert caps == [ 1697 "default_segment_workers capped provider=local slots=1 formula=6 derived=1" 1698 ] 1699 1700 1701def test_segments_explicit_segment_workers_bypasses_local_default_at_call_site( 1702 tmp_path, monkeypatch 1703): 1704 """An explicit --segment-workers wins over a smaller derived default.""" 1705 from solstone.think import thinking as think 1706 1707 journal = tmp_path / "journal" 1708 monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal)) 1709 _patch_segment_run(monkeypatch, think, journal) 1710 1711 # The derived default would be 1; the CLI asks for 6. 1712 monkeypatch.setattr(fanout_policy, "_segment_work_uses_local", lambda: True) 1713 monkeypatch.setattr(fanout_policy, "read_server_parallel_slots", lambda: 1) 1714 monkeypatch.setattr(fanout_policy.os, "cpu_count", lambda: 12) 1715 assert fanout_policy.default_segment_workers() == 1 1716 1717 observed = [] 1718 original_batch = think._run_segment_repair_batch 1719 1720 def spy_batch(**kwargs): 1721 observed.append(kwargs["segment_workers"]) 1722 return original_batch(**kwargs) 1723 1724 monkeypatch.setattr(think, "_run_segment_repair_batch", spy_batch) 1725 monkeypatch.setattr( 1726 "sys.argv", 1727 ["sol think", "--segments", "--day", DAY, "--segment-workers", "6"], 1728 ) 1729 1730 with pytest.raises(SystemExit) as excinfo: 1731 think.main() 1732 1733 assert excinfo.value.code == 0 1734 assert observed == [6]