personal memory agent
1# SPDX-License-Identifier: AGPL-3.0-only
2# Copyright (c) 2026 sol pbc
3
4from __future__ import annotations
5
6import json
7import re
8import shutil
9from pathlib import Path
10from unittest.mock import patch
11
12from solstone.apps.reflections import copy as reflections_copy
13from solstone.convey import create_app
14
15REFLECTION_FIXTURE = Path("tests/fixtures/journal/reflections/weekly/20260308.md")
16
17
18def _seed_reflection(journal: Path, content: str | None = None) -> None:
19 target = journal / "reflections" / "weekly" / "20260308.md"
20 target.parent.mkdir(parents=True, exist_ok=True)
21 target.write_text(
22 content
23 if content is not None
24 else REFLECTION_FIXTURE.read_text(encoding="utf-8"),
25 encoding="utf-8",
26 )
27
28
29def _make_client(journal: Path):
30 app = create_app(str(journal))
31 app.config["TESTING"] = True
32 client = app.test_client()
33 return client
34
35
36def _clear_weekly_reflections(journal: Path) -> None:
37 shutil.rmtree(journal / "reflections" / "weekly", ignore_errors=True)
38
39
40def test_reflections_index_lists_available_weeks(journal_copy):
41 _seed_reflection(journal_copy)
42 client = _make_client(journal_copy)
43
44 response = client.get("/app/reflections/api/state")
45 data = response.get_json()
46
47 assert response.status_code == 200
48 assert data["copy"]["populated_framing"] == reflections_copy.POPULATED_FRAMING
49 assert data["weeks"] == [
50 {
51 "day": "20260308",
52 "label": "Sunday March 8th",
53 "url": "/app/reflections/20260308",
54 }
55 ]
56
57
58def test_reflections_index_empty_state_shows_new_copy_next_date_and_sample_link(
59 monkeypatch, journal_copy
60):
61 _clear_weekly_reflections(journal_copy)
62 monkeypatch.setattr(
63 "solstone.apps.reflections.routes.next_reflection_sunday",
64 lambda journal, today, tz: "Sunday, March 15",
65 )
66 client = _make_client(journal_copy)
67
68 response = client.get("/app/reflections/api/state")
69 data = response.get_json()
70 copy = data["copy"]
71
72 assert response.status_code == 200
73 assert data["weeks"] == []
74 assert copy["subtitle"] == reflections_copy.SUBTITLE
75 assert copy["empty_body"] == reflections_copy.EMPTY_BODY
76 assert copy["empty_next"] == "Your first reflection arrives on Sunday, March 15."
77 assert copy["empty_until_then"] == reflections_copy.EMPTY_UNTIL_THEN
78 assert copy["sample_url"] == "/app/reflections/sample"
79 assert copy["sample_link_label"] == reflections_copy.SAMPLE_LINK_LABEL
80
81
82def test_reflections_index_empty_state_uses_fallback_when_next_date_unavailable(
83 monkeypatch, journal_copy
84):
85 _clear_weekly_reflections(journal_copy)
86 monkeypatch.setattr(
87 "solstone.apps.reflections.routes.next_reflection_sunday",
88 lambda journal, today, tz: None,
89 )
90 client = _make_client(journal_copy)
91
92 response = client.get("/app/reflections/api/state")
93 copy = response.get_json()["copy"]
94
95 assert response.status_code == 200
96 assert copy["empty_next"] == reflections_copy.EMPTY_NEXT_NO_DATE
97 assert copy["populated_next_footer"] is None
98
99
100def test_reflections_index_populated_state_shows_framing_sample_link_and_next_footer(
101 monkeypatch, journal_copy
102):
103 _seed_reflection(journal_copy)
104 monkeypatch.setattr(
105 "solstone.apps.reflections.routes.next_reflection_sunday",
106 lambda journal, today, tz: "Sunday, March 15",
107 )
108 client = _make_client(journal_copy)
109
110 response = client.get("/app/reflections/api/state")
111 copy = response.get_json()["copy"]
112
113 assert response.status_code == 200
114 assert copy["populated_framing"] == reflections_copy.POPULATED_FRAMING
115 assert copy["sample_url"] == "/app/reflections/sample"
116 assert copy["populated_sample_link"] == reflections_copy.POPULATED_SAMPLE_LINK
117 assert copy["populated_next_footer"] == "next reflection: Sunday, March 15"
118
119
120def test_reflections_detail_api_returns_week(journal_copy):
121 _seed_reflection(journal_copy)
122 client = _make_client(journal_copy)
123
124 response = client.get("/app/reflections/api/20260308")
125 data = response.get_json()
126
127 assert response.status_code == 200
128 assert data["day"] == "20260308"
129 assert data["week_label"] == "Sunday March 8th"
130 assert data["raw_url"] == "/app/reflections/20260308/raw"
131 assert data["pdf_url"] == "/app/reflections/20260308/pdf"
132 assert "boardroom balcony inflection" in data["markdown"]
133
134
135def test_reflections_sample_api_returns_fixture_markdown(journal_copy):
136 client = _make_client(journal_copy)
137
138 response = client.get("/app/reflections/api/sample")
139 data = response.get_json()
140
141 assert response.status_code == 200
142 assert data["sample_banner"] == reflections_copy.SAMPLE_BANNER
143 assert "boardroom balcony inflection" in data["markdown"]
144 assert data["raw_url"] == "/app/reflections/sample/raw"
145 assert "pdf_url" not in data
146
147
148def test_reflections_sample_raw_returns_markdown(journal_copy):
149 client = _make_client(journal_copy)
150
151 response = client.get("/app/reflections/sample/raw")
152 text = response.get_data(as_text=True)
153
154 assert response.status_code == 200
155 assert response.headers["Content-Type"] == "text/markdown; charset=utf-8"
156 assert text.startswith("---\ntype: weekly_reflection")
157 assert "boardroom balcony inflection" in text
158
159
160def test_reflections_sample_content_matches_fixture_on_disk():
161 fixture_text = REFLECTION_FIXTURE.read_text(encoding="utf-8")
162 assert reflections_copy.SAMPLE_CONTENT == fixture_text
163
164
165def test_reflections_no_uppercase_transform_on_title(journal_copy):
166 client = _make_client(journal_copy)
167
168 response = client.get("/app/reflections/workspace")
169 html = response.get_data(as_text=True)
170
171 assert response.status_code == 200
172 for selector in (
173 ".reflection-shell",
174 ".reflection-header",
175 ".reflection-title",
176 ):
177 match = re.search(rf"{re.escape(selector)}\s*\{{(?P<body>.*?)\}}", html, re.S)
178 if match is None:
179 continue
180 rule_body = match.group("body")
181 assert "text-transform: uppercase" not in rule_body
182 assert "text-transform: capitalize" not in rule_body
183
184
185def test_reflections_no_mirror_string_in_surface(journal_copy):
186 _seed_reflection(journal_copy)
187 client = _make_client(journal_copy)
188
189 texts = [
190 client.get("/app/reflections/workspace").get_data(as_text=True),
191 json.dumps(client.get("/app/reflections/api/state").get_json()),
192 json.dumps(client.get("/app/reflections/api/20260308").get_json()),
193 json.dumps(client.get("/app/reflections/api/sample").get_json()),
194 ]
195
196 for text in texts:
197 assert "mirror" not in text.lower()
198 assert "🪞" not in text
199
200
201def test_reflections_app_json_icon_is_moon():
202 data = json.loads(Path("solstone/apps/reflections/app.json").read_text())
203
204 assert data["icon"] == "🌙"
205
206
207def test_reflections_detail_canonicalizes_to_sunday_in_api(journal_copy):
208 _seed_reflection(journal_copy)
209 client = _make_client(journal_copy)
210
211 page_response = client.get("/app/reflections/20260310")
212 api_response = client.get("/app/reflections/api/20260310")
213
214 assert page_response.status_code == 200
215 assert b'data-solstone-shell="spa"' in page_response.data
216 assert api_response.status_code == 200
217 assert api_response.get_json()["day"] == "20260308"
218
219
220def test_reflections_missing_week_returns_api_404(journal_copy):
221 client = _make_client(journal_copy)
222
223 page_response = client.get("/app/reflections/20260315")
224 api_response = client.get("/app/reflections/api/20260315")
225 body = api_response.get_json()
226
227 assert page_response.status_code == 200
228 assert b'data-solstone-shell="spa"' in page_response.data
229 assert api_response.status_code == 404
230 assert body["reason_code"] == "file_not_found"
231 assert body["copy"] == {
232 "heading": reflections_copy.DETAIL_EMPTY_HEADING,
233 "desc": reflections_copy.DETAIL_EMPTY_DESC,
234 }
235
236
237def test_reflections_unparseable_week_error_has_no_empty_copy(journal_copy):
238 client = _make_client(journal_copy)
239
240 response = client.get("/app/reflections/api/notaday")
241 body = response.get_json()
242
243 assert response.status_code == 404
244 assert body["reason_code"] == "file_not_found"
245 assert "copy" not in body
246
247
248def test_reflections_raw_returns_markdown(journal_copy):
249 _seed_reflection(journal_copy)
250 client = _make_client(journal_copy)
251
252 response = client.get("/app/reflections/20260308/raw")
253 text = response.get_data(as_text=True)
254
255 assert response.status_code == 200
256 assert response.headers["Content-Type"] == "text/markdown; charset=utf-8"
257 assert text.startswith("---\ntype: weekly_reflection")
258
259
260def test_reflections_pdf_returns_attachment(journal_copy):
261 _seed_reflection(journal_copy)
262 client = _make_client(journal_copy)
263
264 response = client.get("/app/reflections/20260308/pdf")
265
266 assert response.status_code == 200
267 assert response.mimetype == "application/pdf"
268 assert (
269 response.headers["Content-Disposition"]
270 == 'attachment; filename="reflection-20260308.pdf"'
271 )
272 assert response.data.startswith(b"%PDF")
273
274
275def test_reflections_pdf_rejects_remote_assets(journal_copy):
276 _seed_reflection(
277 journal_copy,
278 """---
279type: weekly_reflection
280week: 20260308
281generated: 2026-03-10T19:00:00Z
282model: openai/gpt-5
283sources:
284 newsletters: 0
285 activities: 0
286 decisions: 0
287 followups: 0
288 relationship_signals: 0
289gaps: []
290---
291
292
293""",
294 )
295 client = _make_client(journal_copy)
296
297 with (
298 patch(
299 "urllib.request.urlopen",
300 side_effect=AssertionError("network disabled during reflection pdf render"),
301 ),
302 patch("weasyprint.default_url_fetcher") as mock_fetcher,
303 ):
304 response = client.get("/app/reflections/20260308/pdf")
305
306 assert response.status_code == 200
307 assert response.mimetype == "application/pdf"
308 assert response.data.startswith(b"%PDF")
309 mock_fetcher.assert_not_called()
310
311
312def test_reflections_stats_returns_month_counts(journal_copy):
313 _seed_reflection(journal_copy)
314 client = _make_client(journal_copy)
315
316 response = client.get("/app/reflections/api/stats/202603")
317
318 assert response.status_code == 200
319 assert response.get_json() == {"20260308": 1}