personal memory agent
0

Configure Feed

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

solstone / tests / test_talent_provenance.py
7.0 kB 233 lines
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3 4import asyncio 5import logging 6from pathlib import Path 7from types import SimpleNamespace 8 9import pytest 10 11from solstone.think import talent_provenance, talents 12 13 14def _set_journal(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: 15 journal = tmp_path / "journal" 16 journal.mkdir() 17 monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal.resolve())) 18 return journal 19 20 21def _minimal_config(output_path: Path, *, day: str = "20260101") -> dict: 22 return { 23 "name": "daily", 24 "type": "cogitate", 25 "provider": "google", 26 "model": "gemini-test", 27 "output": "md", 28 "output_path": str(output_path), 29 "day": day, 30 "schedule": "daily", 31 "prompt": "prompt", 32 "user_instruction": "", 33 "system_instruction": "", 34 "sources": {}, 35 "health_stale": False, 36 } 37 38 39def test_weekly_reflection_provenance_path(tmp_path, monkeypatch): 40 journal = _set_journal(tmp_path, monkeypatch) 41 output_path = journal / "reflections" / "weekly" / "20260622.md" 42 43 assert talent_provenance._day_and_logical_output(output_path) == ( 44 "20260622", 45 Path("reflections", "weekly", "20260622.md"), 46 ) 47 assert talent_provenance.provenance_path_for_output(output_path) == ( 48 journal 49 / "chronicle" 50 / "20260622" 51 / "health" 52 / "talent-provenance" 53 / "reflections" 54 / "weekly" 55 / "20260622.md.json" 56 ) 57 58 59def test_facet_news_provenance_path(tmp_path, monkeypatch): 60 journal = _set_journal(tmp_path, monkeypatch) 61 output_path = journal / "facets" / "work" / "news" / "20260622.md" 62 63 assert talent_provenance._day_and_logical_output(output_path) == ( 64 "20260622", 65 Path("facets", "work", "news", "20260622.md"), 66 ) 67 assert talent_provenance.provenance_path_for_output(output_path) == ( 68 journal 69 / "chronicle" 70 / "20260622" 71 / "health" 72 / "talent-provenance" 73 / "facets" 74 / "work" 75 / "news" 76 / "20260622.md.json" 77 ) 78 79 80@pytest.mark.parametrize( 81 "relative", 82 [ 83 Path("reflections", "weekly", "draft.md"), 84 Path("facets", "work", "news", "draft.md"), 85 ], 86) 87def test_stem_day_requires_date_without_day_dir_side_effect( 88 tmp_path, 89 monkeypatch, 90 relative, 91): 92 journal = _set_journal(tmp_path, monkeypatch) 93 94 with pytest.raises(talent_provenance.UnsupportedProvenancePath): 95 talent_provenance.provenance_path_for_output(journal / relative) 96 97 assert not (journal / "chronicle" / "draft").exists() 98 99 100def test_existing_chronicle_mapping_unchanged(tmp_path, monkeypatch): 101 journal = _set_journal(tmp_path, monkeypatch) 102 output_path = journal / "chronicle" / "20260101" / "talents" / "flow.md" 103 104 assert talent_provenance._day_and_logical_output(output_path) == ( 105 "20260101", 106 Path("talents", "flow.md"), 107 ) 108 109 110def test_existing_activity_mapping_unchanged(tmp_path, monkeypatch): 111 journal = _set_journal(tmp_path, monkeypatch) 112 output_path = ( 113 journal / "facets" / "work" / "activities" / "20260101" / "abc" / "summary.md" 114 ) 115 116 assert talent_provenance._day_and_logical_output(output_path) == ( 117 "20260101", 118 Path("facets", "work", "activities", "abc", "summary.md"), 119 ) 120 121 122def test_write_clean_provenance_skips_unmapped_path_without_error( 123 tmp_path, 124 monkeypatch, 125 caplog, 126): 127 journal = _set_journal(tmp_path, monkeypatch) 128 output_path = journal / "apps" / "chat" / "talents" / "support.md" 129 output_path.parent.mkdir(parents=True) 130 output_path.write_text("support", encoding="utf-8") 131 caplog.set_level(logging.WARNING, logger="solstone.think.talents") 132 133 talents._write_clean_provenance( 134 _minimal_config(output_path), 135 output_path, 136 "support", 137 None, 138 123, 139 ) 140 141 assert any(record.levelno == logging.WARNING for record in caplog.records) 142 assert not any(record.levelno >= logging.ERROR for record in caplog.records) 143 144 145def test_write_clean_provenance_logs_unexpected_write_failure( 146 tmp_path, 147 monkeypatch, 148 caplog, 149): 150 journal = _set_journal(tmp_path, monkeypatch) 151 output_path = journal / "chronicle" / "20260101" / "talents" / "daily.md" 152 output_path.parent.mkdir(parents=True) 153 output_path.write_text("daily", encoding="utf-8") 154 155 def raise_os_error(*args, **kwargs): 156 raise OSError("disk full") 157 158 monkeypatch.setattr(talents, "write_provenance", raise_os_error) 159 caplog.set_level(logging.WARNING, logger="solstone.think.talents") 160 161 talents._write_clean_provenance( 162 _minimal_config(output_path), 163 output_path, 164 "daily", 165 None, 166 123, 167 ) 168 169 assert any(record.levelno == logging.ERROR for record in caplog.records) 170 assert not any(record.levelno == logging.WARNING for record in caplog.records) 171 172 173def test_execute_with_tools_weekly_reflection_finish_writes_sidecar( 174 tmp_path, 175 monkeypatch, 176): 177 journal = _set_journal(tmp_path, monkeypatch) 178 output_path = journal / "reflections" / "weekly" / "20260622.md" 179 result = "Full reflection text." 180 events: list[dict] = [] 181 182 async def run_cogitate(config, on_event): 183 on_event({"event": "finish", "result": result}) 184 return "" 185 186 monkeypatch.setattr( 187 "solstone.think.providers.get_provider_module", 188 lambda _provider: SimpleNamespace(run_cogitate=run_cogitate), 189 ) 190 191 config = _minimal_config(output_path, day="20260622") 192 asyncio.run(talents._execute_with_tools(config, events.append)) 193 194 assert any(event.get("event") == "finish" for event in events) 195 assert not any(event.get("event") == "error" for event in events) 196 assert output_path.read_text(encoding="utf-8") == result 197 assert ( 198 journal 199 / "chronicle" 200 / "20260622" 201 / "health" 202 / "talent-provenance" 203 / "reflections" 204 / "weekly" 205 / "20260622.md.json" 206 ).exists() 207 208 209def test_execute_with_tools_unmapped_output_still_finishes( 210 tmp_path, 211 monkeypatch, 212): 213 journal = _set_journal(tmp_path, monkeypatch) 214 output_path = journal / "apps" / "chat" / "talents" / "support.md" 215 result = "Support response." 216 events: list[dict] = [] 217 218 async def run_cogitate(config, on_event): 219 on_event({"event": "finish", "result": result}) 220 return "" 221 222 monkeypatch.setattr( 223 "solstone.think.providers.get_provider_module", 224 lambda _provider: SimpleNamespace(run_cogitate=run_cogitate), 225 ) 226 227 config = _minimal_config(output_path, day="20260622") 228 asyncio.run(talents._execute_with_tools(config, events.append)) 229 230 assert any(event.get("event") == "finish" for event in events) 231 assert not any(event.get("event") == "error" for event in events) 232 assert output_path.read_text(encoding="utf-8") == result 233 assert not any((journal / "chronicle").glob("*/health/talent-provenance/**/*.json"))