personal memory agent
0

Configure Feed

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

solstone / tests / test_sol_call.py
8.5 kB 250 lines
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3 4"""Tests for journal identity — identity directory read/write commands.""" 5 6import json 7import re 8 9import pytest 10from typer.testing import CliRunner 11 12from solstone.think.tools.sol import app 13 14runner = CliRunner() 15_HISTORY_FIELDS = [ 16 "ts", 17 "file", 18 "actor", 19 "op", 20 "section", 21 "reason", 22 "before_hash", 23 "after_hash", 24 "bytes_before", 25 "bytes_after", 26] 27 28 29def _read_history(journal_path): 30 history = journal_path / "identity" / "history.jsonl" 31 return [json.loads(line) for line in history.read_text().splitlines()] 32 33 34def _assert_history_record(record, *, file_name, actor, op, section, reason): 35 assert list(record) == _HISTORY_FIELDS 36 assert re.fullmatch(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z", record["ts"]) 37 assert record["file"] == file_name 38 assert record["actor"] == actor 39 assert record["op"] == op 40 assert record["section"] == section 41 assert record["reason"] == reason 42 assert isinstance(record["before_hash"], str) 43 assert isinstance(record["after_hash"], str) 44 assert isinstance(record["bytes_before"], int) 45 assert isinstance(record["bytes_after"], int) 46 47 48@pytest.fixture 49def journal_with_identity(tmp_path, monkeypatch): 50 """Set up a journal with identity/ containing partner.md.""" 51 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) 52 53 # Provide minimal config for ensure_identity_directory 54 config_dir = tmp_path / "config" 55 config_dir.mkdir() 56 (config_dir / "journal.json").write_text( 57 json.dumps({"identity": {"name": "Test User"}}) 58 ) 59 60 identity_dir = tmp_path / "identity" 61 identity_dir.mkdir() 62 63 partner_md = """\ 64# partner 65 66Behavioral profile of the journal owner — observed patterns that help sol 67adapt its responses, timing, and initiative to how this person actually works. 68 69## work patterns 70[observing] 71 72## communication style 73[observing] 74 75## relationship priorities 76[observing] 77 78## decision style 79[observing] 80 81## expertise domains 82[observing] 83""" 84 (identity_dir / "partner.md").write_text(partner_md) 85 (identity_dir / "health.md").write_text( 86 "## Status\n\n" 87 "not yet generated\n\n" 88 "## Needs your attention\n\n" 89 "## Auto-repairs (last 7d)\n", 90 encoding="utf-8", 91 ) 92 93 return tmp_path 94 95 96class TestSolPartnerRead: 97 def test_read_partner(self, journal_with_identity): 98 result = runner.invoke(app, ["partner"]) 99 assert result.exit_code == 0 100 assert "# partner" in result.output 101 assert "## work patterns" in result.output 102 103 def test_read_partner_missing(self, tmp_path, monkeypatch): 104 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) 105 config_dir = tmp_path / "config" 106 config_dir.mkdir() 107 (config_dir / "journal.json").write_text(json.dumps({})) 108 # ensure_identity_directory creates partner.md 109 result = runner.invoke(app, ["partner"]) 110 assert result.exit_code == 0 111 112 113class TestSolPartnerWrite: 114 def test_write_partner(self, journal_with_identity): 115 new_content = "# partner\n\n## work patterns\nPrefers mornings for deep work.\n" 116 result = runner.invoke(app, ["partner", "--write"], input=new_content) 117 assert result.exit_code == 0 118 assert "partner.md updated" in result.output 119 120 partner_path = journal_with_identity / "identity" / "partner.md" 121 assert partner_path.read_text() == new_content 122 123 def test_write_partner_empty_stdin(self, journal_with_identity): 124 result = runner.invoke(app, ["partner", "--write"], input="") 125 assert result.exit_code == 1 126 assert "no content" in result.output 127 128 129class TestSolPartnerUpdateSection: 130 def test_update_section_work_patterns(self, journal_with_identity): 131 result = runner.invoke( 132 app, 133 ["partner", "--update-section", "work patterns"], 134 input="Prefers async communication and morning deep work.", 135 ) 136 assert result.exit_code == 0 137 assert "Updated ## work patterns" in result.output 138 139 partner_path = journal_with_identity / "identity" / "partner.md" 140 content = partner_path.read_text() 141 assert "Prefers async communication" in content 142 assert "## communication style" in content 143 assert "## decision style" in content 144 145 def test_update_section_not_found(self, journal_with_identity): 146 result = runner.invoke( 147 app, 148 ["partner", "--update-section", "nonexistent"], 149 input="content", 150 ) 151 assert result.exit_code == 1 152 assert "not found" in result.output 153 154 def test_update_section_empty_stdin(self, journal_with_identity): 155 result = runner.invoke( 156 app, 157 ["partner", "--update-section", "work patterns"], 158 input="", 159 ) 160 assert result.exit_code == 1 161 assert "no content" in result.output 162 163 164class TestSolWriteDoesNotEscapeIdentityDir: 165 """Verify that journal identity only writes to identity/ files.""" 166 167 def test_partner_write_stays_in_identity_dir(self, journal_with_identity): 168 """Write to partner.md goes to identity/partner.md, not anywhere else.""" 169 result = runner.invoke(app, ["partner", "--write"], input="test content\n") 170 assert result.exit_code == 0 171 partner_path = journal_with_identity / "identity" / "partner.md" 172 assert partner_path.read_text() == "test content\n" 173 journal_files = set( 174 f.name for f in journal_with_identity.iterdir() if f.is_file() 175 ) 176 assert "partner.md" not in journal_files 177 178 179class TestSolPartnerValueOption: 180 def test_write_partner_with_value(self, journal_with_identity): 181 new_content = "# partner\n\n## work patterns\nMorning person.\n" 182 result = runner.invoke(app, ["partner", "--write", "--value", new_content]) 183 assert result.exit_code == 0 184 assert "partner.md updated" in result.output 185 partner_path = journal_with_identity / "identity" / "partner.md" 186 assert partner_path.read_text() == new_content 187 188 def test_update_section_with_value(self, journal_with_identity): 189 result = runner.invoke( 190 app, 191 [ 192 "partner", 193 "--update-section", 194 "work patterns", 195 "--value", 196 "Prefers mornings", 197 ], 198 ) 199 assert result.exit_code == 0 200 assert "Updated ## work patterns" in result.output 201 content = (journal_with_identity / "identity" / "partner.md").read_text() 202 assert "Prefers mornings" in content 203 204 def test_value_empty_string_errors(self, journal_with_identity): 205 result = runner.invoke(app, ["partner", "--write", "--value", " "]) 206 assert result.exit_code == 1 207 assert "no content" in result.output 208 209 210class TestSolHistoryLogging: 211 def test_partner_write_logs_history(self, journal_with_identity): 212 runner.invoke(app, ["partner", "--write", "--value", "# partner\n\nProfile.\n"]) 213 records = _read_history(journal_with_identity) 214 assert len(records) == 1 215 _assert_history_record( 216 records[0], 217 file_name="partner.md", 218 actor="journal identity partner --write", 219 op="replace", 220 section=None, 221 reason="manual replace", 222 ) 223 224 def test_multiple_writes_append(self, journal_with_identity): 225 runner.invoke(app, ["partner", "--write", "--value", "# partner\n\nFirst.\n"]) 226 runner.invoke(app, ["partner", "--write", "--value", "# partner\n\nSecond.\n"]) 227 records = _read_history(journal_with_identity) 228 assert len(records) == 2 229 230 def test_partner_update_section_logs_history(self, journal_with_identity): 231 runner.invoke( 232 app, 233 [ 234 "partner", 235 "--update-section", 236 "work patterns", 237 "--value", 238 "Morning focus", 239 ], 240 ) 241 records = _read_history(journal_with_identity) 242 assert len(records) == 1 243 _assert_history_record( 244 records[0], 245 file_name="partner.md", 246 actor="journal identity partner --update-section <heading>", 247 op="update_section", 248 section="work patterns", 249 reason="manual section update", 250 )