personal memory agent
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 ]