personal memory agent
1# SPDX-License-Identifier: AGPL-3.0-only
2# Copyright (c) 2026 sol pbc
3
4"""Tests for convey configuration validation and defaults."""
5
6from __future__ import annotations
7
8import json
9import logging
10from pathlib import Path
11
12
13def _set_convey_journal(monkeypatch, tmp_path: Path) -> Path:
14 from solstone.convey import state
15
16 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
17 monkeypatch.setattr(state, "journal_root", str(tmp_path))
18 return tmp_path
19
20
21def _write_convey_config(journal: Path, payload: dict) -> None:
22 config_path = journal / "config" / "convey.json"
23 config_path.parent.mkdir(parents=True, exist_ok=True)
24 config_path.write_text(json.dumps(payload), encoding="utf-8")
25
26
27def _read_convey_config(journal: Path) -> dict:
28 return json.loads((journal / "config" / "convey.json").read_text("utf-8"))
29
30
31def _commit_journal_identity() -> None:
32 from solstone.think.link.ca import load_or_generate_ca
33 from solstone.think.link.paths import ca_dir
34
35 load_or_generate_ca(ca_dir())
36
37
38def test_reporting_enabled_defaults_true_when_absent(monkeypatch, tmp_path):
39 from solstone.convey.config import reporting_enabled
40
41 _set_convey_journal(monkeypatch, tmp_path)
42
43 assert reporting_enabled() is True
44
45
46def test_seed_default_app_navigation_seeds_absent_apps():
47 from solstone.convey.config import (
48 DEFAULT_APP_ORDER,
49 DEFAULT_RAIL_APPS,
50 seed_default_app_navigation,
51 )
52
53 config = {}
54
55 changed = seed_default_app_navigation(config)
56
57 assert changed is True
58 assert config["apps"]["starred"] == DEFAULT_RAIL_APPS
59 assert config["apps"]["order"] == DEFAULT_APP_ORDER
60
61
62def test_curation_pinned_in_default_rail():
63 from solstone.convey.config import DEFAULT_APP_ORDER, DEFAULT_RAIL_APPS
64
65 assert "curation" in DEFAULT_RAIL_APPS
66 assert DEFAULT_RAIL_APPS.index("curation") == 3
67 assert "curation" in DEFAULT_APP_ORDER
68
69
70def test_services_absent_from_default_app_order():
71 from solstone.convey.config import DEFAULT_APP_ORDER, DEFAULT_RAIL_APPS
72
73 assert "services" not in DEFAULT_RAIL_APPS
74 assert "services" not in DEFAULT_APP_ORDER
75
76
77def test_apply_app_order_skips_missing_persisted_app_ids():
78 from solstone.convey.config import apply_app_order
79
80 apps = {
81 "home": {"label": "home"},
82 "news": {"label": "news"},
83 "reflections": {"label": "reflections"},
84 }
85 config = {
86 "apps": {
87 "order": ["home", "services", "news"],
88 "starred": ["home", "services"],
89 }
90 }
91
92 ordered = apply_app_order(apps, config)
93
94 assert list(ordered) == ["home", "news", "reflections"]
95 assert "services" not in ordered
96
97
98def test_seed_default_app_navigation_preserves_present_empty_lists():
99 from solstone.convey.config import seed_default_app_navigation
100
101 config = {"apps": {"starred": [], "order": []}}
102
103 changed = seed_default_app_navigation(config)
104
105 assert changed is False
106 assert config["apps"]["starred"] == []
107 assert config["apps"]["order"] == []
108
109
110def test_seed_default_app_navigation_preserves_curated_starred():
111 from solstone.convey.config import (
112 DEFAULT_APP_ORDER,
113 seed_default_app_navigation,
114 )
115
116 config = {"apps": {"starred": ["chat"]}}
117
118 changed = seed_default_app_navigation(config)
119
120 assert changed is True
121 assert config["apps"]["starred"] == ["chat"]
122 assert config["apps"]["order"] == DEFAULT_APP_ORDER
123
124
125def test_seed_default_app_navigation_preserves_curated_order():
126 from solstone.convey.config import (
127 DEFAULT_RAIL_APPS,
128 seed_default_app_navigation,
129 )
130
131 config = {"apps": {"order": ["chat"]}}
132
133 changed = seed_default_app_navigation(config)
134
135 assert changed is True
136 assert config["apps"]["starred"] == DEFAULT_RAIL_APPS
137 assert config["apps"]["order"] == ["chat"]
138
139
140def test_seed_default_app_navigation_is_idempotent():
141 from solstone.convey.config import (
142 DEFAULT_APP_ORDER,
143 DEFAULT_RAIL_APPS,
144 seed_default_app_navigation,
145 )
146
147 config = {}
148
149 first_changed = seed_default_app_navigation(config)
150 first_config = json.loads(json.dumps(config))
151 second_changed = seed_default_app_navigation(config)
152
153 assert first_changed is True
154 assert second_changed is False
155 assert config == first_config
156 assert config["apps"]["starred"] == DEFAULT_RAIL_APPS
157 assert config["apps"]["order"] == DEFAULT_APP_ORDER
158
159
160def test_init_finalize_seeds_default_app_navigation(journal_copy):
161 from solstone.convey import create_app
162 from solstone.convey.config import DEFAULT_APP_ORDER, DEFAULT_RAIL_APPS
163
164 (journal_copy / "config" / "convey.json").unlink()
165 app = create_app(str(journal_copy))
166 app.config["TESTING"] = True
167 _commit_journal_identity()
168
169 resp = app.test_client().post(
170 "/init/finalize",
171 json={},
172 content_type="application/json",
173 )
174
175 assert resp.status_code == 200
176 data = resp.get_json()
177 assert data["success"] is True
178 assert data["redirect"] == "/app/thinking/"
179
180 config = _read_convey_config(journal_copy)
181 assert config["apps"]["starred"] == DEFAULT_RAIL_APPS
182 assert config["apps"]["order"] == DEFAULT_APP_ORDER
183
184
185def test_init_finalize_logs_convey_seed_persist_failure(
186 journal_copy, monkeypatch, caplog
187):
188 from solstone.convey import create_app
189 from solstone.convey import root as root_module
190
191 (journal_copy / "config" / "convey.json").unlink()
192
193 def _fail_seed(_transform):
194 raise OSError("simulated persist failure")
195
196 monkeypatch.setattr(root_module, "locked_modify_convey_config", _fail_seed)
197 caplog.set_level(logging.ERROR, logger="solstone.convey.root")
198 app = create_app(str(journal_copy))
199 app.config["TESTING"] = True
200 _commit_journal_identity()
201
202 resp = app.test_client().post(
203 "/init/finalize",
204 json={},
205 content_type="application/json",
206 )
207
208 assert resp.status_code == 500
209 data = resp.get_json()
210 assert data["committed"] is True
211 assert data["secondary_effect"] == "default_app_navigation"
212 assert "default app navigation seed convey-config PERSIST failed" in caplog.text
213
214
215def test_reporting_enabled_round_trips_false(monkeypatch, tmp_path):
216 from solstone.convey.config import (
217 load_convey_config,
218 reporting_enabled,
219 validate_config,
220 )
221
222 journal = _set_convey_journal(monkeypatch, tmp_path)
223 payload = {"reporting": {"enabled": False}}
224 valid, error = validate_config(payload)
225 assert valid is True
226 assert error is None
227
228 _write_convey_config(journal, payload)
229
230 assert load_convey_config() == payload
231 assert reporting_enabled() is False
232
233
234def test_validate_config_rejects_non_dict_reporting():
235 from solstone.convey.config import validate_config
236
237 valid, error = validate_config({"reporting": "yes"})
238
239 assert valid is False
240 assert error == "reporting must be an object"
241
242
243def test_validate_config_rejects_non_bool_enabled():
244 from solstone.convey.config import validate_config
245
246 valid, error = validate_config({"reporting": {"enabled": "no"}})
247
248 assert valid is False
249 assert error == "reporting.enabled must be a boolean"
250
251
252def test_validate_config_rejects_unknown_reporting_key():
253 from solstone.convey.config import validate_config
254
255 valid, error = validate_config({"reporting": {"enabled": True, "bogus": 1}})
256
257 assert valid is False
258 assert error == "reporting contains unknown key(s): bogus"