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.news import copy as news_copy
13from solstone.convey import create_app
14
15VERONA_FIXTURE = Path("tests/fixtures/journal/facets/verona/news/20260310.md")
16
17
18def _make_client(journal: Path):
19 app = create_app(str(journal))
20 app.config["TESTING"] = True
21 client = app.test_client()
22 return client
23
24
25def _seed_news(journal: Path, facet: str, day: str, body: str) -> None:
26 target = journal / "facets" / facet / "news" / f"{day}.md"
27 target.parent.mkdir(parents=True, exist_ok=True)
28 target.write_text(body, encoding="utf-8")
29
30
31def _clear_news(journal: Path) -> None:
32 facets_dir = journal / "facets"
33 if not facets_dir.is_dir():
34 return
35 for facet_dir in facets_dir.iterdir():
36 news_dir = facet_dir / "news"
37 if news_dir.is_dir():
38 shutil.rmtree(news_dir, ignore_errors=True)
39
40
41def _clear_chronicle(journal: Path) -> None:
42 shutil.rmtree(journal / "chronicle", ignore_errors=True)
43
44
45def test_news_app_json_icon_and_label():
46 data = json.loads(Path("solstone/apps/news/app.json").read_text())
47 assert data["icon"] == "📰"
48 assert data["label"] == "newsletters"
49
50
51def test_news_sidebar_adjacent_to_reflections(journal_copy):
52 client = _make_client(journal_copy)
53 response = client.get("/api/shell")
54
55 assert response.status_code == 200
56 names = [app["name"] for app in response.get_json()["apps"]]
57 news_idx = names.index("news")
58 refl_idx = names.index("reflections")
59 between = names[min(news_idx, refl_idx) + 1 : max(news_idx, refl_idx)]
60 assert between == []
61
62
63def test_news_index_empty_state_self_explains(journal_copy):
64 _clear_news(journal_copy)
65 client = _make_client(journal_copy)
66
67 response = client.get("/app/news/api/state")
68 data = response.get_json()
69 copy = data["copy"]
70
71 assert response.status_code == 200
72 assert data["newsletters"] == []
73 assert copy["kicker"] == news_copy.NEWS_KICKER
74 assert copy["index_h1"] == news_copy.NEWS_INDEX_H1
75 assert copy["subtitle"] == news_copy.NEWS_SUBTITLE
76 assert copy["empty_body"] == news_copy.NEWS_EMPTY_BODY
77 assert copy["empty_next"] == "Your first newsletters arrive tomorrow morning."
78 assert copy["empty_until_then"] == news_copy.NEWS_EMPTY_UNTIL_THEN
79 assert copy["sample_link_label"] == news_copy.NEWS_SAMPLE_LINK_LABEL
80 assert copy["sample_url"] == "/app/news/sample"
81
82
83def test_news_index_empty_state_no_date_when_journal_brand_new(journal_copy):
84 _clear_news(journal_copy)
85 _clear_chronicle(journal_copy)
86 client = _make_client(journal_copy)
87
88 response = client.get("/app/news/api/state")
89 copy = response.get_json()["copy"]
90
91 assert response.status_code == 200
92 assert copy["empty_next"] == news_copy.NEWS_EMPTY_NO_DATE
93 assert "Your first newsletters arrive" not in copy["empty_next"]
94
95
96def test_news_index_populated_lists_files_reverse_chrono(journal_copy):
97 _clear_news(journal_copy)
98 _seed_news(journal_copy, "personal", "20260526", "# personal 5/26")
99 _seed_news(journal_copy, "solstone", "20260526", "# solstone 5/26")
100 _seed_news(journal_copy, "kognova", "20260525", "# kognova 5/25")
101
102 client = _make_client(journal_copy)
103 response = client.get("/app/news/api/state")
104 data = response.get_json()
105 copy = data["copy"]
106
107 assert response.status_code == 200
108 assert copy["populated_framing"] == news_copy.NEWS_POPULATED_FRAMING
109 assert copy["populated_sample_link"] == news_copy.NEWS_POPULATED_SAMPLE_LINK
110 assert copy["populated_next_footer"] == "next newsletters: tomorrow morning"
111 urls = [item["url"] for item in data["newsletters"]]
112 assert "/app/news/personal/20260526" in urls
113 assert "/app/news/solstone/20260526" in urls
114 assert "/app/news/kognova/20260525" in urls
115 assert [item["day"] for item in data["newsletters"]][:2] == [
116 "20260526",
117 "20260526",
118 ]
119 assert data["newsletters"][2]["day"] == "20260525"
120 assert data["newsletters"][0]["label"] == "Tue May 26, 2026"
121
122
123def test_news_detail_api_returns_file(journal_copy):
124 _clear_news(journal_copy)
125 _seed_news(
126 journal_copy,
127 "personal",
128 "20260526",
129 "# 2026-05-26 personal\n\nA newsletter body.\n",
130 )
131 client = _make_client(journal_copy)
132
133 response = client.get("/app/news/api/personal/20260526")
134 data = response.get_json()
135
136 assert response.status_code == 200
137 assert data["kicker"] == news_copy.NEWS_KICKER
138 assert data["facet"] == "personal"
139 assert data["date_label"] == "Tue May 26, 2026"
140 assert data["subtitle"] == "sol's notes for personal on this day."
141 assert data["raw_url"] == "/app/news/personal/20260526/raw"
142 assert data["pdf_url"] == "/app/news/personal/20260526/pdf"
143 assert data["debug_link_url"] == "/app/sol/20260526/talents/facet_newsletter"
144 assert data["debug_link_label"] == news_copy.NEWS_DETAIL_DEBUG_LINK
145 assert data["markdown"].startswith("# 2026-05-26 personal")
146
147
148def test_news_detail_missing_page_shell_and_api_empty_state(journal_copy):
149 _clear_news(journal_copy)
150 client = _make_client(journal_copy)
151
152 page_response = client.get("/app/news/nonexistent/20260526")
153 api_response = client.get("/app/news/api/nonexistent/20260526")
154
155 assert page_response.status_code == 200
156 assert b'data-solstone-shell="spa"' in page_response.data
157 assert api_response.status_code == 200
158 data = api_response.get_json()
159 assert data["empty"] is True
160 assert "reason_code" not in data
161
162
163def test_news_sample_api_returns_inlined_content(journal_copy):
164 client = _make_client(journal_copy)
165 response = client.get("/app/news/api/sample")
166 data = response.get_json()
167
168 assert response.status_code == 200
169 assert data["sample_banner"] == news_copy.NEWS_SAMPLE_BANNER
170 assert data["sample_h1"] == news_copy.NEWS_SAMPLE_H1
171 assert "Verona Platform Joint Venture" in data["markdown"]
172 assert data["raw_url"] == "/app/news/sample/raw"
173 assert "pdf_url" not in data
174
175
176def test_news_sample_raw_returns_markdown(journal_copy):
177 client = _make_client(journal_copy)
178 response = client.get("/app/news/sample/raw")
179 text = response.get_data(as_text=True)
180
181 assert response.status_code == 200
182 assert response.headers["Content-Type"] == "text/markdown; charset=utf-8"
183 assert "Verona Platform Joint Venture" in text
184
185
186def test_news_sample_content_matches_fixture():
187 """SAMPLE_CONTENT must stay in sync with the on-disk verona fixture.
188
189 The fixture is the source of truth for sample bytes. SAMPLE_CONTENT is the
190 inlined copy that ships in PyPI wheels (tests/fixtures/ is excluded from
191 the wheel — A21 / req_2ntkhdiv lesson). This test fails when either side
192 drifts.
193 """
194 fixture_text = VERONA_FIXTURE.read_text(encoding="utf-8")
195 assert news_copy.SAMPLE_CONTENT == fixture_text
196
197
198def test_news_h1s_are_lowercase(journal_copy):
199 _seed_news(journal_copy, "personal", "20260526", "# 2026-05-26 personal\n")
200 client = _make_client(journal_copy)
201
202 index_data = client.get("/app/news/api/state").get_json()
203 detail_data = client.get("/app/news/api/personal/20260526").get_json()
204 sample_data = client.get("/app/news/api/sample").get_json()
205 workspace_html = client.get("/app/news/workspace").get_data(as_text=True)
206
207 assert index_data["copy"]["index_h1"] == "newsletters"
208 assert f"{detail_data['facet']} · {detail_data['date_label']}" == (
209 "personal · Tue May 26, 2026"
210 )
211 assert sample_data["sample_h1"] == "sample newsletter"
212
213 for selector in (".news-shell", ".news-title", ".news-header"):
214 match = re.search(
215 rf"{re.escape(selector)}\s*\{{(?P<body>.*?)\}}",
216 workspace_html,
217 re.S,
218 )
219 if match is None:
220 continue
221 rule_body = match.group("body")
222 assert "text-transform: uppercase" not in rule_body
223 assert "text-transform: capitalize" not in rule_body
224
225
226def test_news_no_run_log_string_in_surface(journal_copy):
227 _seed_news(journal_copy, "personal", "20260526", "# 2026-05-26 personal\n")
228 client = _make_client(journal_copy)
229
230 texts = [
231 client.get("/app/news/workspace").get_data(as_text=True),
232 json.dumps(client.get("/app/news/api/state").get_json()),
233 json.dumps(client.get("/app/news/api/personal/20260526").get_json()),
234 json.dumps(client.get("/app/news/api/sample").get_json()),
235 ]
236
237 for text in texts:
238 assert "run log" not in text.lower()
239
240
241def test_news_detail_raw_returns_markdown(journal_copy):
242 _seed_news(
243 journal_copy,
244 "personal",
245 "20260526",
246 "# 2026-05-26 personal\n\nbody\n",
247 )
248 client = _make_client(journal_copy)
249
250 response = client.get("/app/news/personal/20260526/raw")
251 text = response.get_data(as_text=True)
252
253 assert response.status_code == 200
254 assert response.headers["Content-Type"] == "text/markdown; charset=utf-8"
255 assert text.startswith("# 2026-05-26 personal")
256
257
258def test_news_detail_pdf_returns_attachment(journal_copy):
259 _seed_news(
260 journal_copy,
261 "personal",
262 "20260526",
263 "# 2026-05-26 personal\n\nbody\n",
264 )
265 client = _make_client(journal_copy)
266
267 response = client.get("/app/news/personal/20260526/pdf")
268
269 assert response.status_code == 200
270 assert response.mimetype == "application/pdf"
271 assert (
272 response.headers["Content-Disposition"]
273 == 'attachment; filename="newsletter-personal-20260526.pdf"'
274 )
275 assert response.data.startswith(b"%PDF")
276
277
278def test_news_detail_pdf_rejects_remote_assets(journal_copy):
279 _seed_news(
280 journal_copy,
281 "personal",
282 "20260526",
283 "# 2026-05-26 personal\n\n\n",
284 )
285 client = _make_client(journal_copy)
286
287 with (
288 patch(
289 "urllib.request.urlopen",
290 side_effect=AssertionError("network disabled during news pdf render"),
291 ),
292 patch("weasyprint.default_url_fetcher") as mock_fetcher,
293 ):
294 response = client.get("/app/news/personal/20260526/pdf")
295
296 assert response.status_code == 200
297 assert response.mimetype == "application/pdf"
298 assert response.data.startswith(b"%PDF")
299 mock_fetcher.assert_not_called()