personal memory agent
0

Configure Feed

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

solstone / tests / test_think_daily_idempotency.py
35 kB 1111 lines
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3 4"""Tests for per-unit daily think idempotency.""" 5 6from __future__ import annotations 7 8import importlib 9import json 10import os 11from pathlib import Path 12 13import pytest 14 15from solstone.think.cogitate_policy import DETERMINISTIC_FAILURE_CAPS 16from solstone.think.pipeline_health import ( 17 DeterministicFailure, 18 read_completed_units, 19 read_daily_deterministic_failures, 20) 21from solstone.think.utils import updated_days 22 23DAY = "20990301" 24 25 26@pytest.fixture 27def daily_journal(tmp_path, monkeypatch): 28 journal = tmp_path / "journal" 29 (journal / "chronicle" / DAY / "health").mkdir(parents=True) 30 monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal)) 31 return journal 32 33 34def _write_health(journal: Path, day: str, filename: str, events: list[dict]) -> Path: 35 path = journal / "chronicle" / day / "health" / filename 36 path.parent.mkdir(parents=True, exist_ok=True) 37 with path.open("w", encoding="utf-8") as handle: 38 for event in events: 39 handle.write(json.dumps(event) + "\n") 40 return path 41 42 43def _read_jsonl(path: Path) -> list[dict]: 44 with path.open(encoding="utf-8") as handle: 45 return [json.loads(line) for line in handle if line.strip()] 46 47 48def _skip_events(journal: Path, day: str, filename: str) -> list[dict]: 49 return [ 50 event 51 for event in _read_jsonl(journal / "chronicle" / day / "health" / filename) 52 if event["event"] == "talent.skip" 53 ] 54 55 56def _complete(name: str, ts: int = 1, facet: str | None = None) -> dict: 57 event = {"event": "talent.complete", "ts": ts, "mode": "daily", "name": name} 58 if facet: 59 event["facet"] = facet 60 return event 61 62 63def _fail( 64 name: str, 65 ts: int = 1, 66 facet: str | None = None, 67 *, 68 state: str | None = None, 69 reason_code: str | None = None, 70) -> dict: 71 event = {"event": "talent.fail", "ts": ts, "mode": "daily", "name": name} 72 if facet: 73 event["facet"] = facet 74 if state: 75 event["state"] = state 76 if reason_code: 77 event["reason_code"] = reason_code 78 return event 79 80 81def _single_configs(*names: str) -> dict[str, dict]: 82 return {name: {"type": "cogitate", "priority": 10} for name in names} 83 84 85def _install_daily_mocks( 86 monkeypatch, 87 mod, 88 configs: dict[str, dict], 89 dispatched: list[tuple[str, dict]], 90 *, 91 enabled_facets: dict[str, dict] | None = None, 92 active_facets: set[str] | None = None, 93) -> None: 94 monkeypatch.setattr(mod, "get_talent_configs", lambda schedule: configs) 95 monkeypatch.setattr(mod, "day_input_summary", lambda day: "No recordings") 96 monkeypatch.setattr(mod, "get_enabled_facets", lambda: enabled_facets or {}) 97 monkeypatch.setattr(mod, "get_active_facets", lambda day: active_facets or set()) 98 99 def mock_cortex_request(**kwargs): 100 dispatched.append((kwargs["name"], dict(kwargs["config"]))) 101 return f"use-{len(dispatched)}" 102 103 def mock_drain(spawned, *_args, **_kwargs): 104 return (len(spawned), 0, []) 105 106 monkeypatch.setattr(mod, "_dispatch_cortex_request", mock_cortex_request) 107 monkeypatch.setattr(mod, "_drain_priority_batch", mock_drain) 108 109 110def _run_daily_with_writer(mod, journal: Path, day: str, filename: str, **kwargs): 111 path = journal / "chronicle" / day / "health" / filename 112 old_writer = mod._jsonl 113 writer = mod.ThinkingJSONLWriter(str(path)) 114 mod._jsonl = writer 115 try: 116 return mod.run_daily_prompts( 117 day=day, 118 verbose=False, 119 max_concurrency=0, 120 **kwargs, 121 ) 122 finally: 123 writer.close() 124 mod._jsonl = old_writer 125 126 127def test_check_daily_skip_predicate(): 128 mod = importlib.import_module("solstone.think.thinking") 129 completed = {("daily", "alpha", None), ("daily", "pulse", None)} 130 deterministic_failures = {} 131 132 assert mod._check_daily_skip( 133 "alpha", 134 None, 135 mode="daily", 136 completed=completed, 137 deterministic_failures=deterministic_failures, 138 ) == (True, "already_complete") 139 assert mod._check_daily_skip( 140 "beta", 141 None, 142 mode="daily", 143 completed=completed, 144 deterministic_failures=deterministic_failures, 145 ) == (False, None) 146 assert mod._check_daily_skip( 147 "alpha", 148 None, 149 mode="segment", 150 completed=completed, 151 deterministic_failures=deterministic_failures, 152 ) == (False, None) 153 assert mod._check_daily_skip( 154 "pulse", 155 None, 156 mode="daily", 157 completed=completed, 158 deterministic_failures=deterministic_failures, 159 ) == (True, "already_complete") 160 assert mod._check_daily_skip( 161 "alpha", 162 None, 163 mode="daily", 164 completed=completed, 165 deterministic_failures=deterministic_failures, 166 from_scratch=True, 167 ) == (False, None) 168 169 170def test_check_daily_skip_has_no_freshness_inputs(): 171 mod = importlib.import_module("solstone.think.thinking") 172 173 names = mod._check_daily_skip.__code__.co_varnames[ 174 : mod._check_daily_skip.__code__.co_argcount 175 + mod._check_daily_skip.__code__.co_kwonlyargcount 176 ] 177 178 assert "stream" not in names 179 assert "mtime" not in names 180 assert "freshness" not in names 181 assert "from_scratch" in names 182 183 184@pytest.mark.parametrize("retry_on_deterministic_failure", [False, True]) 185@pytest.mark.parametrize("at_cap", [False, True]) 186@pytest.mark.parametrize( 187 ("reason_code", "cap"), sorted(DETERMINISTIC_FAILURE_CAPS.items()) 188) 189def test_daily_skip_and_completion_cap_predicates_match( 190 retry_on_deterministic_failure, 191 at_cap, 192 reason_code, 193 cap, 194): 195 mod = importlib.import_module("solstone.think.thinking") 196 count = cap if at_cap else cap - 1 197 deterministic_failures = { 198 ("beta", None): DeterministicFailure(count=count, reason_code=reason_code) 199 } 200 201 skip, _reason = mod._check_daily_skip( 202 "beta", 203 None, 204 mode="daily", 205 completed=set(), 206 deterministic_failures=deterministic_failures, 207 retry_on_deterministic_failure=retry_on_deterministic_failure, 208 ) 209 verdict = mod.evaluate_daily_completion( 210 {("beta", None)}, 211 set(), 212 deterministic_failures, 213 [], 214 ) 215 terminal_degraded = bool(verdict.capped_daily_units) 216 217 if retry_on_deterministic_failure: 218 assert skip is False 219 assert terminal_degraded is at_cap 220 else: 221 assert skip is terminal_degraded 222 assert not (skip and not terminal_degraded) 223 224 225def test_evaluate_daily_completion_terminal_cases(): 226 mod = importlib.import_module("solstone.think.thinking") 227 228 complete_and_capped = mod.evaluate_daily_completion( 229 {("alpha", None), ("beta", None)}, 230 {("daily", "alpha", None)}, 231 { 232 ("beta", None): DeterministicFailure( 233 count=2, reason_code="context_window_exceeded" 234 ) 235 }, 236 [], 237 ) 238 assert complete_and_capped.complete is True 239 assert complete_and_capped.daily_units_terminal is True 240 assert complete_and_capped.capped_daily_units == ( 241 mod.CappedDailyUnit( 242 name="beta", 243 facet=None, 244 reason_code="context_window_exceeded", 245 count=2, 246 ), 247 ) 248 249 below_cap = mod.evaluate_daily_completion( 250 {("beta", None)}, 251 set(), 252 { 253 ("beta", None): DeterministicFailure( 254 count=1, reason_code="context_window_exceeded" 255 ) 256 }, 257 [], 258 ) 259 assert below_cap.complete is False 260 assert below_cap.capped_daily_units == () 261 262 263def test_evaluate_daily_completion_transient_latest_stays_incomplete(journal_copy): 264 mod = importlib.import_module("solstone.think.thinking") 265 day = "20990319" 266 _prepare_main_day(journal_copy, day) 267 _write_health( 268 journal_copy, 269 day, 270 "001_daily.jsonl", 271 [ 272 _complete("alpha"), 273 _fail("beta", ts=1, reason_code="context_window_exceeded"), 274 _fail("beta", ts=2, reason_code="context_window_exceeded"), 275 _fail("beta", ts=3, reason_code="schema_invalid"), 276 _fail("beta", ts=4, reason_code="token_budget_exceeded"), 277 _fail("beta", ts=5, reason_code="provider_transient"), 278 ], 279 ) 280 281 completed = read_completed_units(day) 282 deterministic_failures = read_daily_deterministic_failures(day) 283 284 assert ("beta", None) not in deterministic_failures 285 transient_latest = mod.evaluate_daily_completion( 286 {("alpha", None), ("beta", None)}, 287 completed, 288 deterministic_failures, 289 [], 290 ) 291 assert transient_latest.complete is False 292 assert transient_latest.capped_daily_units == () 293 294 295def test_evaluate_daily_completion_dispatch_without_terminal_stays_incomplete( 296 journal_copy, 297): 298 mod = importlib.import_module("solstone.think.thinking") 299 day = "20990320" 300 _prepare_main_day(journal_copy, day) 301 _write_health( 302 journal_copy, 303 day, 304 "001_daily.jsonl", 305 [ 306 _complete("alpha"), 307 {"event": "talent.dispatch", "ts": 2, "mode": "daily", "name": "beta"}, 308 ], 309 ) 310 311 completed = read_completed_units(day) 312 deterministic_failures = read_daily_deterministic_failures(day) 313 314 assert ("daily", "beta", None) not in completed 315 assert ("beta", None) not in deterministic_failures 316 dispatched_without_terminal = mod.evaluate_daily_completion( 317 {("alpha", None), ("beta", None)}, 318 completed, 319 deterministic_failures, 320 [], 321 ) 322 assert dispatched_without_terminal.complete is False 323 assert dispatched_without_terminal.capped_daily_units == () 324 325 326def test_evaluate_daily_completion_withholds_with_segment_blockers(): 327 mod = importlib.import_module("solstone.think.thinking") 328 329 verdict = mod.evaluate_daily_completion( 330 {("beta", None)}, 331 set(), 332 { 333 ("beta", None): DeterministicFailure( 334 count=2, reason_code="context_window_exceeded" 335 ) 336 }, 337 [{"segment": "090000_300", "dimension": "not_thought", "detail": "floor"}], 338 ) 339 340 assert verdict.complete is False 341 assert verdict.daily_units_terminal is True 342 assert verdict.capped_daily_units 343 assert verdict.segment_blockers == ( 344 {"segment": "090000_300", "dimension": "not_thought", "detail": "floor"}, 345 ) 346 347 348def test_run_daily_prompts_skips_all_completed_units(daily_journal, monkeypatch): 349 mod = importlib.import_module("solstone.think.thinking") 350 _write_health( 351 daily_journal, 352 DAY, 353 "001_daily.jsonl", 354 [_complete("alpha"), _complete("beta")], 355 ) 356 dispatched: list[tuple[str, dict]] = [] 357 _install_daily_mocks(monkeypatch, mod, _single_configs("alpha", "beta"), dispatched) 358 359 result = _run_daily_with_writer(mod, daily_journal, DAY, "002_daily.jsonl") 360 361 assert dispatched == [] 362 assert result[3] == {("alpha", None), ("beta", None)} 363 skips = [ 364 event 365 for event in _read_jsonl( 366 daily_journal / "chronicle" / DAY / "health" / "002_daily.jsonl" 367 ) 368 if event["event"] == "talent.skip" 369 ] 370 assert {event["name"] for event in skips} == {"alpha", "beta"} 371 assert {event["reason"] for event in skips} == {"already_complete"} 372 373 374def test_run_daily_prompts_from_scratch_reruns_completed_units( 375 daily_journal, monkeypatch 376): 377 mod = importlib.import_module("solstone.think.thinking") 378 _write_health(daily_journal, DAY, "001_daily.jsonl", [_complete("alpha")]) 379 dispatched: list[tuple[str, dict]] = [] 380 _install_daily_mocks(monkeypatch, mod, _single_configs("alpha"), dispatched) 381 382 _run_daily_with_writer( 383 mod, 384 daily_journal, 385 DAY, 386 "002_daily.jsonl", 387 from_scratch=True, 388 ) 389 390 assert [name for name, _config in dispatched] == ["alpha"] 391 392 393def test_run_daily_prompts_repeated_skips_ignore_prior_skips( 394 daily_journal, monkeypatch 395): 396 mod = importlib.import_module("solstone.think.thinking") 397 _write_health( 398 daily_journal, 399 DAY, 400 "001_daily.jsonl", 401 [_complete("alpha"), _complete("beta")], 402 ) 403 dispatched: list[tuple[str, dict]] = [] 404 _install_daily_mocks(monkeypatch, mod, _single_configs("alpha", "beta"), dispatched) 405 406 _run_daily_with_writer(mod, daily_journal, DAY, "002_daily.jsonl") 407 _run_daily_with_writer(mod, daily_journal, DAY, "003_daily.jsonl") 408 409 assert dispatched == [] 410 for filename in ("002_daily.jsonl", "003_daily.jsonl"): 411 skips = [ 412 event 413 for event in _read_jsonl( 414 daily_journal / "chronicle" / DAY / "health" / filename 415 ) 416 if event["event"] == "talent.skip" 417 ] 418 assert {event["name"] for event in skips} == {"alpha", "beta"} 419 420 421def test_run_daily_prompts_only_reruns_latest_failures(daily_journal, monkeypatch): 422 mod = importlib.import_module("solstone.think.thinking") 423 _write_health( 424 daily_journal, 425 DAY, 426 "001_daily.jsonl", 427 [_complete("alpha"), _fail("beta")], 428 ) 429 dispatched: list[tuple[str, dict]] = [] 430 _install_daily_mocks(monkeypatch, mod, _single_configs("alpha", "beta"), dispatched) 431 432 _run_daily_with_writer(mod, daily_journal, DAY, "002_daily.jsonl") 433 434 assert [name for name, _config in dispatched] == ["beta"] 435 skips = [ 436 event 437 for event in _read_jsonl( 438 daily_journal / "chronicle" / DAY / "health" / "002_daily.jsonl" 439 ) 440 if event["event"] == "talent.skip" 441 ] 442 assert [event["name"] for event in skips] == ["alpha"] 443 444 445def test_run_daily_prompts_reruns_no_output_failures(daily_journal, monkeypatch): 446 mod = importlib.import_module("solstone.think.thinking") 447 _write_health( 448 daily_journal, 449 DAY, 450 "001_daily.jsonl", 451 [ 452 _complete("alpha"), 453 _fail("beta", state="error", reason_code="no_output"), 454 ], 455 ) 456 dispatched: list[tuple[str, dict]] = [] 457 _install_daily_mocks(monkeypatch, mod, _single_configs("alpha", "beta"), dispatched) 458 459 _run_daily_with_writer(mod, daily_journal, DAY, "002_daily.jsonl") 460 461 assert [name for name, _config in dispatched] == ["beta"] 462 skips = [ 463 event 464 for event in _read_jsonl( 465 daily_journal / "chronicle" / DAY / "health" / "002_daily.jsonl" 466 ) 467 if event["event"] == "talent.skip" 468 ] 469 assert [event["name"] for event in skips] == ["alpha"] 470 471 472def test_run_daily_prompts_skips_two_deterministic_failures(daily_journal, monkeypatch): 473 mod = importlib.import_module("solstone.think.thinking") 474 _write_health( 475 daily_journal, 476 DAY, 477 "001_daily.jsonl", 478 [ 479 _fail("alpha", ts=1, reason_code="context_window_exceeded"), 480 _fail("alpha", ts=2, reason_code="context_window_exceeded"), 481 _complete("beta"), 482 ], 483 ) 484 dispatched: list[tuple[str, dict]] = [] 485 _install_daily_mocks(monkeypatch, mod, _single_configs("alpha", "beta"), dispatched) 486 487 _run_daily_with_writer(mod, daily_journal, DAY, "002_daily.jsonl") 488 489 assert dispatched == [] 490 skips = _skip_events(daily_journal, DAY, "002_daily.jsonl") 491 assert {event["name"] for event in skips} == {"alpha", "beta"} 492 alpha_skip = next(event for event in skips if event["name"] == "alpha") 493 assert alpha_skip["reason"] == "deterministic_failure_no_retry" 494 assert alpha_skip["detail"] == ( 495 "2 same-day deterministic failures " 496 "(context_window_exceeded); not re-dispatching" 497 ) 498 499 500def test_run_daily_prompts_skips_one_model_not_found_failure( 501 daily_journal, monkeypatch 502): 503 mod = importlib.import_module("solstone.think.thinking") 504 _write_health( 505 daily_journal, 506 DAY, 507 "001_daily.jsonl", 508 [_fail("alpha", reason_code="model_not_found")], 509 ) 510 dispatched: list[tuple[str, dict]] = [] 511 _install_daily_mocks(monkeypatch, mod, _single_configs("alpha"), dispatched) 512 513 _run_daily_with_writer(mod, daily_journal, DAY, "002_daily.jsonl") 514 515 assert dispatched == [] 516 skips = _skip_events(daily_journal, DAY, "002_daily.jsonl") 517 assert [(event["name"], event["reason"]) for event in skips] == [ 518 ("alpha", "deterministic_failure_no_retry") 519 ] 520 assert skips[0]["detail"] == ( 521 "1 same-day deterministic failures (model_not_found); not re-dispatching" 522 ) 523 524 525def test_run_daily_prompts_schema_invalid_retries_until_third_failure( 526 daily_journal, monkeypatch 527): 528 mod = importlib.import_module("solstone.think.thinking") 529 _write_health( 530 daily_journal, 531 DAY, 532 "001_daily.jsonl", 533 [ 534 _fail("alpha", ts=1, reason_code="schema_invalid"), 535 _fail("alpha", ts=2, reason_code="schema_invalid"), 536 ], 537 ) 538 dispatched: list[tuple[str, dict]] = [] 539 _install_daily_mocks(monkeypatch, mod, _single_configs("alpha"), dispatched) 540 541 _run_daily_with_writer(mod, daily_journal, DAY, "002_daily.jsonl") 542 543 assert [name for name, _config in dispatched] == ["alpha"] 544 dispatched.clear() 545 _write_health( 546 daily_journal, 547 DAY, 548 "003_daily.jsonl", 549 [_fail("alpha", ts=3, reason_code="schema_invalid")], 550 ) 551 552 _run_daily_with_writer(mod, daily_journal, DAY, "004_daily.jsonl") 553 554 assert dispatched == [] 555 skips = _skip_events(daily_journal, DAY, "004_daily.jsonl") 556 assert skips[0]["reason"] == "deterministic_failure_no_retry" 557 assert skips[0]["detail"] == ( 558 "3 same-day deterministic failures (schema_invalid); not re-dispatching" 559 ) 560 561 562def test_mixed_deterministic_reason_uses_latest_reason_cap(daily_journal, monkeypatch): 563 mod = importlib.import_module("solstone.think.thinking") 564 _write_health( 565 daily_journal, 566 DAY, 567 "001_daily.jsonl", 568 [ 569 _fail("alpha", ts=1, reason_code="context_window_exceeded"), 570 _fail("alpha", ts=2, reason_code="schema_invalid"), 571 _fail("beta", ts=1, reason_code="schema_invalid"), 572 _fail("beta", ts=2, reason_code="context_window_exceeded"), 573 ], 574 ) 575 dispatched: list[tuple[str, dict]] = [] 576 _install_daily_mocks(monkeypatch, mod, _single_configs("alpha", "beta"), dispatched) 577 578 _run_daily_with_writer(mod, daily_journal, DAY, "002_daily.jsonl") 579 580 assert [name for name, _config in dispatched] == ["alpha"] 581 skips = _skip_events(daily_journal, DAY, "002_daily.jsonl") 582 assert [(event["name"], event["reason"]) for event in skips] == [ 583 ("beta", "deterministic_failure_no_retry") 584 ] 585 586 587def test_run_daily_prompts_reruns_one_deterministic_failure(daily_journal, monkeypatch): 588 mod = importlib.import_module("solstone.think.thinking") 589 _write_health( 590 daily_journal, 591 DAY, 592 "001_daily.jsonl", 593 [_fail("alpha", reason_code="context_window_exceeded")], 594 ) 595 dispatched: list[tuple[str, dict]] = [] 596 _install_daily_mocks(monkeypatch, mod, _single_configs("alpha"), dispatched) 597 598 _run_daily_with_writer(mod, daily_journal, DAY, "002_daily.jsonl") 599 600 assert [name for name, _config in dispatched] == ["alpha"] 601 602 603def test_run_daily_prompts_reruns_transient_failures(daily_journal, monkeypatch): 604 mod = importlib.import_module("solstone.think.thinking") 605 _write_health( 606 daily_journal, 607 DAY, 608 "001_daily.jsonl", 609 [ 610 _fail("alpha", ts=1, reason_code="provider_quota_exceeded"), 611 _fail("alpha", ts=2, reason_code="provider_quota_exceeded"), 612 _fail("alpha", ts=3, reason_code="provider_quota_exceeded"), 613 ], 614 ) 615 dispatched: list[tuple[str, dict]] = [] 616 _install_daily_mocks(monkeypatch, mod, _single_configs("alpha"), dispatched) 617 618 _run_daily_with_writer(mod, daily_journal, DAY, "002_daily.jsonl") 619 620 assert [name for name, _config in dispatched] == ["alpha"] 621 622 623def test_run_daily_prompts_deterministic_failure_resets_per_day( 624 daily_journal, monkeypatch 625): 626 mod = importlib.import_module("solstone.think.thinking") 627 other_day = "20990302" 628 _write_health( 629 daily_journal, 630 DAY, 631 "001_daily.jsonl", 632 [ 633 _fail("alpha", ts=1, reason_code="context_window_exceeded"), 634 _fail("alpha", ts=2, reason_code="context_window_exceeded"), 635 ], 636 ) 637 dispatched: list[tuple[str, dict]] = [] 638 _install_daily_mocks(monkeypatch, mod, _single_configs("alpha"), dispatched) 639 640 _run_daily_with_writer(mod, daily_journal, other_day, "001_daily.jsonl") 641 642 assert [name for name, _config in dispatched] == ["alpha"] 643 644 645def test_run_daily_prompts_retry_on_deterministic_failure_override( 646 daily_journal, monkeypatch 647): 648 mod = importlib.import_module("solstone.think.thinking") 649 _write_health( 650 daily_journal, 651 DAY, 652 "001_daily.jsonl", 653 [ 654 _fail("alpha", ts=1, reason_code="context_window_exceeded"), 655 _fail("alpha", ts=2, reason_code="context_window_exceeded"), 656 ], 657 ) 658 configs = { 659 "alpha": { 660 "type": "cogitate", 661 "priority": 10, 662 "retry_on_deterministic_failure": True, 663 } 664 } 665 dispatched: list[tuple[str, dict]] = [] 666 _install_daily_mocks(monkeypatch, mod, configs, dispatched) 667 668 _run_daily_with_writer(mod, daily_journal, DAY, "002_daily.jsonl") 669 670 assert [name for name, _config in dispatched] == ["alpha"] 671 672 673def test_run_daily_prompts_keys_multi_facet_units_by_facet(daily_journal, monkeypatch): 674 mod = importlib.import_module("solstone.think.thinking") 675 _write_health( 676 daily_journal, 677 DAY, 678 "001_daily.jsonl", 679 [ 680 _complete("facet_newsletter", facet="work"), 681 _fail("facet_newsletter", facet="personal"), 682 ], 683 ) 684 configs = { 685 "facet_newsletter": { 686 "type": "cogitate", 687 "priority": 10, 688 "multi_facet": True, 689 } 690 } 691 dispatched: list[tuple[str, dict]] = [] 692 _install_daily_mocks( 693 monkeypatch, 694 mod, 695 configs, 696 dispatched, 697 enabled_facets={"work": {}, "personal": {}}, 698 active_facets={"work", "personal"}, 699 ) 700 701 result = _run_daily_with_writer(mod, daily_journal, DAY, "002_daily.jsonl") 702 703 assert [(name, config["facet"]) for name, config in dispatched] == [ 704 ("facet_newsletter", "personal") 705 ] 706 assert result[3] == { 707 ("facet_newsletter", "work"), 708 ("facet_newsletter", "personal"), 709 } 710 skips = [ 711 event 712 for event in _read_jsonl( 713 daily_journal / "chronicle" / DAY / "health" / "002_daily.jsonl" 714 ) 715 if event["event"] == "talent.skip" 716 ] 717 assert [(event["name"], event["facet"]) for event in skips] == [ 718 ("facet_newsletter", "work") 719 ] 720 721 722def test_run_daily_prompts_respects_same_run_complete_and_fail_order( 723 daily_journal, monkeypatch 724): 725 mod = importlib.import_module("solstone.think.thinking") 726 _write_health( 727 daily_journal, 728 DAY, 729 "001_daily.jsonl", 730 [_complete("alpha"), _fail("beta")], 731 ) 732 dispatched: list[tuple[str, dict]] = [] 733 _install_daily_mocks(monkeypatch, mod, _single_configs("alpha", "beta"), dispatched) 734 735 _run_daily_with_writer(mod, daily_journal, DAY, "002_daily.jsonl") 736 737 assert [name for name, _config in dispatched] == ["beta"] 738 739 740def test_run_daily_prompts_reruns_dispatch_without_terminal(daily_journal, monkeypatch): 741 mod = importlib.import_module("solstone.think.thinking") 742 _write_health( 743 daily_journal, 744 DAY, 745 "001_daily.jsonl", 746 [{"event": "talent.dispatch", "ts": 1, "mode": "daily", "name": "alpha"}], 747 ) 748 dispatched: list[tuple[str, dict]] = [] 749 _install_daily_mocks(monkeypatch, mod, _single_configs("alpha"), dispatched) 750 751 _run_daily_with_writer(mod, daily_journal, DAY, "002_daily.jsonl") 752 753 assert [name for name, _config in dispatched] == ["alpha"] 754 755 756def test_run_daily_prompts_ignores_stream_freshness_for_completed_units( 757 daily_journal, monkeypatch 758): 759 mod = importlib.import_module("solstone.think.thinking") 760 _write_health(daily_journal, DAY, "001_daily.jsonl", [_complete("alpha")]) 761 (daily_journal / "chronicle" / DAY / "health" / "stream.updated").touch() 762 dispatched: list[tuple[str, dict]] = [] 763 _install_daily_mocks(monkeypatch, mod, _single_configs("alpha"), dispatched) 764 765 _run_daily_with_writer(mod, daily_journal, DAY, "002_daily.jsonl") 766 767 assert dispatched == [] 768 769 770def test_run_daily_prompts_refreshes_dispatched_generators(daily_journal, monkeypatch): 771 mod = importlib.import_module("solstone.think.thinking") 772 configs = {"alpha": {"type": "generate", "priority": 10, "output": "md"}} 773 dispatched: list[tuple[str, dict]] = [] 774 _install_daily_mocks(monkeypatch, mod, configs, dispatched) 775 776 _run_daily_with_writer(mod, daily_journal, DAY, "001_daily.jsonl") 777 778 assert dispatched == [ 779 ( 780 "alpha", 781 { 782 "day": DAY, 783 "output": "md", 784 "refresh": True, 785 "env": {"SOL_DAY": DAY}, 786 "schedule": "daily", 787 }, 788 ) 789 ] 790 791 792def test_run_daily_prompts_refreshes_dispatched_cogitate_with_output( 793 daily_journal, monkeypatch 794): 795 mod = importlib.import_module("solstone.think.thinking") 796 configs = {"alpha": {"type": "cogitate", "priority": 10, "output": "md"}} 797 _write_health( 798 daily_journal, 799 DAY, 800 "001_daily.jsonl", 801 [{"event": "talent.dispatch", "ts": 1, "mode": "daily", "name": "alpha"}], 802 ) 803 output_path = daily_journal / "chronicle" / DAY / "talents" / "alpha.md" 804 output_path.parent.mkdir(parents=True) 805 output_path.touch() 806 dispatched: list[tuple[str, dict]] = [] 807 _install_daily_mocks(monkeypatch, mod, configs, dispatched) 808 809 _run_daily_with_writer(mod, daily_journal, DAY, "002_daily.jsonl") 810 811 assert dispatched == [ 812 ( 813 "alpha", 814 { 815 "day": DAY, 816 "output": "md", 817 "refresh": True, 818 "env": {"SOL_DAY": DAY}, 819 "schedule": "daily", 820 }, 821 ) 822 ] 823 824 825def test_run_daily_prompts_refreshes_multifacet_cogitate_with_output( 826 daily_journal, monkeypatch 827): 828 mod = importlib.import_module("solstone.think.thinking") 829 configs = { 830 "alpha": { 831 "type": "cogitate", 832 "priority": 10, 833 "output": "md", 834 "multi_facet": True, 835 } 836 } 837 dispatched: list[tuple[str, dict]] = [] 838 _install_daily_mocks( 839 monkeypatch, 840 mod, 841 configs, 842 dispatched, 843 enabled_facets={"work": {}}, 844 active_facets={"work"}, 845 ) 846 847 _run_daily_with_writer(mod, daily_journal, DAY, "001_daily.jsonl") 848 849 assert len(dispatched) == 1 850 name, config = dispatched[0] 851 assert (name, config["facet"]) == ("alpha", "work") 852 assert config["output"] == "md" 853 assert config["refresh"] is True 854 assert config["env"] == {"SOL_DAY": DAY, "SOL_FACET": "work"} 855 856 857def _prepare_main_day(journal: Path, day: str) -> Path: 858 health = journal / "chronicle" / day / "health" 859 health.mkdir(parents=True) 860 return health 861 862 863def _patch_main(monkeypatch, mod, applicable_units): 864 calls = [] 865 866 def mock_run_command(cmd, day): 867 return True 868 869 def mock_run_queued_command(cmd, day, timeout=600): 870 return True 871 872 def mock_run_daily_prompts(**kwargs): 873 calls.append(kwargs) 874 return (len(applicable_units), 0, [], applicable_units) 875 876 monkeypatch.setattr(mod, "run_command", mock_run_command) 877 monkeypatch.setattr(mod, "run_queued_command", mock_run_queued_command) 878 monkeypatch.setattr(mod, "run_daily_prompts", mock_run_daily_prompts) 879 return calls 880 881 882def test_main_writes_daily_marker_when_all_applicable_complete( 883 journal_copy, monkeypatch 884): 885 mod = importlib.import_module("solstone.think.thinking") 886 day = "20990310" 887 health = _prepare_main_day(journal_copy, day) 888 _write_health(journal_copy, day, "001_daily.jsonl", [_complete("alpha")]) 889 _patch_main(monkeypatch, mod, {("alpha", None)}) 890 monkeypatch.setattr("sys.argv", ["sol think", "--day", day]) 891 892 mod.main() 893 894 assert (health / "daily.updated").exists() 895 896 897def test_main_withholds_daily_marker_when_applicable_unit_incomplete( 898 journal_copy, monkeypatch 899): 900 mod = importlib.import_module("solstone.think.thinking") 901 day = "20990311" 902 health = _prepare_main_day(journal_copy, day) 903 _write_health(journal_copy, day, "001_daily.jsonl", [_complete("alpha")]) 904 _patch_main(monkeypatch, mod, {("alpha", None), ("beta", None)}) 905 monkeypatch.setattr("sys.argv", ["sol think", "--day", day]) 906 907 mod.main() 908 909 assert not (health / "daily.updated").exists() 910 911 912def test_main_withholds_daily_marker_when_no_output_failure_is_incomplete( 913 journal_copy, monkeypatch 914): 915 mod = importlib.import_module("solstone.think.thinking") 916 day = "20990314" 917 health = _prepare_main_day(journal_copy, day) 918 _write_health( 919 journal_copy, 920 day, 921 "001_daily.jsonl", 922 [_fail("alpha", state="error", reason_code="no_output")], 923 ) 924 _patch_main(monkeypatch, mod, {("alpha", None)}) 925 monkeypatch.setattr("sys.argv", ["sol think", "--day", day]) 926 927 mod.main() 928 929 assert not (health / "daily.updated").exists() 930 931 932def test_main_writes_daily_marker_when_capped_unit_terminal_and_payload_includes_capped_units( 933 journal_copy, monkeypatch 934): 935 mod = importlib.import_module("solstone.think.thinking") 936 day = "20990316" 937 health = _prepare_main_day(journal_copy, day) 938 _write_health( 939 journal_copy, 940 day, 941 "001_daily.jsonl", 942 [ 943 _complete("alpha"), 944 _fail("beta", ts=1, reason_code="context_window_exceeded"), 945 _fail("beta", ts=2, reason_code="context_window_exceeded"), 946 ], 947 ) 948 _patch_main(monkeypatch, mod, {("alpha", None), ("beta", None)}) 949 emitted: list[tuple[str, dict]] = [] 950 monkeypatch.setattr( 951 mod, 952 "emit", 953 lambda event, **fields: emitted.append((event, fields)), 954 ) 955 monkeypatch.setattr("sys.argv", ["sol think", "--day", day]) 956 957 mod.main() 958 959 assert (health / "daily.updated").exists() 960 daily_complete = next( 961 fields for event, fields in emitted if event == "daily_complete" 962 ) 963 assert daily_complete["capped_daily_units"] == [ 964 { 965 "name": "beta", 966 "facet": None, 967 "reason_code": "context_window_exceeded", 968 "count": 2, 969 } 970 ] 971 972 973def test_capped_completion_clears_after_later_complete(journal_copy): 974 mod = importlib.import_module("solstone.think.thinking") 975 day = "20990317" 976 _prepare_main_day(journal_copy, day) 977 _write_health( 978 journal_copy, 979 day, 980 "001_daily.jsonl", 981 [ 982 _complete("alpha"), 983 _fail("beta", ts=1, reason_code="context_window_exceeded"), 984 _fail("beta", ts=2, reason_code="context_window_exceeded"), 985 ], 986 ) 987 988 capped = mod.evaluate_daily_completion( 989 {("alpha", None), ("beta", None)}, 990 read_completed_units(day), 991 read_daily_deterministic_failures(day), 992 [], 993 ) 994 995 assert capped.complete is True 996 assert capped.capped_daily_units 997 998 _write_health(journal_copy, day, "002_daily.jsonl", [_complete("beta", ts=3)]) 999 cleared = mod.evaluate_daily_completion( 1000 {("alpha", None), ("beta", None)}, 1001 read_completed_units(day), 1002 read_daily_deterministic_failures(day), 1003 [], 1004 ) 1005 1006 assert cleared.complete is True 1007 assert cleared.capped_daily_units == () 1008 1009 1010def test_main_ignores_not_applicable_incomplete_units(journal_copy, monkeypatch): 1011 mod = importlib.import_module("solstone.think.thinking") 1012 day = "20990312" 1013 health = _prepare_main_day(journal_copy, day) 1014 _write_health( 1015 journal_copy, 1016 day, 1017 "001_daily.jsonl", 1018 [_complete("alpha"), _fail("beta")], 1019 ) 1020 _patch_main(monkeypatch, mod, {("alpha", None)}) 1021 monkeypatch.setattr("sys.argv", ["sol think", "--day", day]) 1022 1023 mod.main() 1024 1025 assert (health / "daily.updated").exists() 1026 1027 1028def test_main_does_not_force_refresh_from_stream_marker(journal_copy, monkeypatch): 1029 mod = importlib.import_module("solstone.think.thinking") 1030 day = "20990313" 1031 health = _prepare_main_day(journal_copy, day) 1032 _write_health(journal_copy, day, "001_daily.jsonl", [_complete("alpha")]) 1033 (health / "daily.updated").touch() 1034 (health / "stream.updated").touch() 1035 calls = _patch_main(monkeypatch, mod, {("alpha", None)}) 1036 monkeypatch.setattr("sys.argv", ["sol think", "--day", day]) 1037 1038 mod.main() 1039 1040 assert calls == [ 1041 { 1042 "day": day, 1043 "verbose": False, 1044 "max_concurrency": 2, 1045 "stream": None, 1046 "from_scratch": False, 1047 } 1048 ] 1049 1050 1051def test_capped_terminal_completion_clears_updated_days_until_stream_newer( 1052 journal_copy, monkeypatch 1053): 1054 mod = importlib.import_module("solstone.think.thinking") 1055 day = "20990318" 1056 health = _prepare_main_day(journal_copy, day) 1057 _write_health( 1058 journal_copy, 1059 day, 1060 "001_daily.jsonl", 1061 [ 1062 _complete("alpha"), 1063 _fail("beta", ts=1, reason_code="context_window_exceeded"), 1064 _fail("beta", ts=2, reason_code="context_window_exceeded"), 1065 ], 1066 ) 1067 (health / "stream.updated").touch() 1068 dispatched: list[tuple[str, dict]] = [] 1069 _install_daily_mocks(monkeypatch, mod, _single_configs("alpha", "beta"), dispatched) 1070 monkeypatch.setattr( 1071 mod, "run_bounded_phase", lambda *_args, **_kwargs: (True, False) 1072 ) 1073 monkeypatch.setattr(mod, "run_queued_command", lambda *_args, **_kwargs: True) 1074 monkeypatch.setattr("sys.argv", ["sol think", "--day", day]) 1075 1076 assert day in updated_days() 1077 mod.main() 1078 1079 assert dispatched == [] 1080 assert (health / "daily.updated").exists() 1081 assert day not in updated_days() 1082 1083 os.utime(health / "daily.updated", (1000, 1000)) 1084 os.utime(health / "stream.updated", (1010, 1010)) 1085 assert day in updated_days() 1086 monkeypatch.setattr("sys.argv", ["sol think", "--day", day]) 1087 1088 mod.main() 1089 1090 assert dispatched == [] 1091 assert day not in updated_days() 1092 1093 1094def test_main_passes_from_scratch_to_daily_prompts(journal_copy, monkeypatch): 1095 mod = importlib.import_module("solstone.think.thinking") 1096 day = "20990315" 1097 _prepare_main_day(journal_copy, day) 1098 calls = _patch_main(monkeypatch, mod, set()) 1099 monkeypatch.setattr("sys.argv", ["sol think", "--day", day, "--from-scratch"]) 1100 1101 mod.main() 1102 1103 assert calls == [ 1104 { 1105 "day": day, 1106 "verbose": False, 1107 "max_concurrency": 2, 1108 "stream": None, 1109 "from_scratch": True, 1110 } 1111 ]