personal memory agent
0

Configure Feed

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

solstone / tests / test_app_reflections.py
10 kB 319 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.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![remote](https://example.com/reflection.png) 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}