personal memory agent
0

Configure Feed

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

solstone / tests / test_app_news.py
10 kB 299 lines
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![remote](https://example.com/n.png)\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()