# SPDX-License-Identifier: AGPL-3.0-only # Copyright (c) 2026 sol pbc from __future__ import annotations import json import re import shutil from pathlib import Path from unittest.mock import patch from solstone.apps.reflections import copy as reflections_copy from solstone.convey import create_app REFLECTION_FIXTURE = Path("tests/fixtures/journal/reflections/weekly/20260308.md") def _seed_reflection(journal: Path, content: str | None = None) -> None: target = journal / "reflections" / "weekly" / "20260308.md" target.parent.mkdir(parents=True, exist_ok=True) target.write_text( content if content is not None else REFLECTION_FIXTURE.read_text(encoding="utf-8"), encoding="utf-8", ) def _make_client(journal: Path): app = create_app(str(journal)) app.config["TESTING"] = True client = app.test_client() return client def _clear_weekly_reflections(journal: Path) -> None: shutil.rmtree(journal / "reflections" / "weekly", ignore_errors=True) def test_reflections_index_lists_available_weeks(journal_copy): _seed_reflection(journal_copy) client = _make_client(journal_copy) response = client.get("/app/reflections/api/state") data = response.get_json() assert response.status_code == 200 assert data["copy"]["populated_framing"] == reflections_copy.POPULATED_FRAMING assert data["weeks"] == [ { "day": "20260308", "label": "Sunday March 8th", "url": "/app/reflections/20260308", } ] def test_reflections_index_empty_state_shows_new_copy_next_date_and_sample_link( monkeypatch, journal_copy ): _clear_weekly_reflections(journal_copy) monkeypatch.setattr( "solstone.apps.reflections.routes.next_reflection_sunday", lambda journal, today, tz: "Sunday, March 15", ) client = _make_client(journal_copy) response = client.get("/app/reflections/api/state") data = response.get_json() copy = data["copy"] assert response.status_code == 200 assert data["weeks"] == [] assert copy["subtitle"] == reflections_copy.SUBTITLE assert copy["empty_body"] == reflections_copy.EMPTY_BODY assert copy["empty_next"] == "Your first reflection arrives on Sunday, March 15." assert copy["empty_until_then"] == reflections_copy.EMPTY_UNTIL_THEN assert copy["sample_url"] == "/app/reflections/sample" assert copy["sample_link_label"] == reflections_copy.SAMPLE_LINK_LABEL def test_reflections_index_empty_state_uses_fallback_when_next_date_unavailable( monkeypatch, journal_copy ): _clear_weekly_reflections(journal_copy) monkeypatch.setattr( "solstone.apps.reflections.routes.next_reflection_sunday", lambda journal, today, tz: None, ) client = _make_client(journal_copy) response = client.get("/app/reflections/api/state") copy = response.get_json()["copy"] assert response.status_code == 200 assert copy["empty_next"] == reflections_copy.EMPTY_NEXT_NO_DATE assert copy["populated_next_footer"] is None def test_reflections_index_populated_state_shows_framing_sample_link_and_next_footer( monkeypatch, journal_copy ): _seed_reflection(journal_copy) monkeypatch.setattr( "solstone.apps.reflections.routes.next_reflection_sunday", lambda journal, today, tz: "Sunday, March 15", ) client = _make_client(journal_copy) response = client.get("/app/reflections/api/state") copy = response.get_json()["copy"] assert response.status_code == 200 assert copy["populated_framing"] == reflections_copy.POPULATED_FRAMING assert copy["sample_url"] == "/app/reflections/sample" assert copy["populated_sample_link"] == reflections_copy.POPULATED_SAMPLE_LINK assert copy["populated_next_footer"] == "next reflection: Sunday, March 15" def test_reflections_detail_api_returns_week(journal_copy): _seed_reflection(journal_copy) client = _make_client(journal_copy) response = client.get("/app/reflections/api/20260308") data = response.get_json() assert response.status_code == 200 assert data["day"] == "20260308" assert data["week_label"] == "Sunday March 8th" assert data["raw_url"] == "/app/reflections/20260308/raw" assert data["pdf_url"] == "/app/reflections/20260308/pdf" assert "boardroom balcony inflection" in data["markdown"] def test_reflections_sample_api_returns_fixture_markdown(journal_copy): client = _make_client(journal_copy) response = client.get("/app/reflections/api/sample") data = response.get_json() assert response.status_code == 200 assert data["sample_banner"] == reflections_copy.SAMPLE_BANNER assert "boardroom balcony inflection" in data["markdown"] assert data["raw_url"] == "/app/reflections/sample/raw" assert "pdf_url" not in data def test_reflections_sample_raw_returns_markdown(journal_copy): client = _make_client(journal_copy) response = client.get("/app/reflections/sample/raw") text = response.get_data(as_text=True) assert response.status_code == 200 assert response.headers["Content-Type"] == "text/markdown; charset=utf-8" assert text.startswith("---\ntype: weekly_reflection") assert "boardroom balcony inflection" in text def test_reflections_sample_content_matches_fixture_on_disk(): fixture_text = REFLECTION_FIXTURE.read_text(encoding="utf-8") assert reflections_copy.SAMPLE_CONTENT == fixture_text def test_reflections_no_uppercase_transform_on_title(journal_copy): client = _make_client(journal_copy) response = client.get("/app/reflections/workspace") html = response.get_data(as_text=True) assert response.status_code == 200 for selector in ( ".reflection-shell", ".reflection-header", ".reflection-title", ): match = re.search(rf"{re.escape(selector)}\s*\{{(?P.*?)\}}", html, re.S) if match is None: continue rule_body = match.group("body") assert "text-transform: uppercase" not in rule_body assert "text-transform: capitalize" not in rule_body def test_reflections_no_mirror_string_in_surface(journal_copy): _seed_reflection(journal_copy) client = _make_client(journal_copy) texts = [ client.get("/app/reflections/workspace").get_data(as_text=True), json.dumps(client.get("/app/reflections/api/state").get_json()), json.dumps(client.get("/app/reflections/api/20260308").get_json()), json.dumps(client.get("/app/reflections/api/sample").get_json()), ] for text in texts: assert "mirror" not in text.lower() assert "🪞" not in text def test_reflections_app_json_icon_is_moon(): data = json.loads(Path("solstone/apps/reflections/app.json").read_text()) assert data["icon"] == "🌙" def test_reflections_detail_canonicalizes_to_sunday_in_api(journal_copy): _seed_reflection(journal_copy) client = _make_client(journal_copy) page_response = client.get("/app/reflections/20260310") api_response = client.get("/app/reflections/api/20260310") assert page_response.status_code == 200 assert b'data-solstone-shell="spa"' in page_response.data assert api_response.status_code == 200 assert api_response.get_json()["day"] == "20260308" def test_reflections_missing_week_returns_api_404(journal_copy): client = _make_client(journal_copy) page_response = client.get("/app/reflections/20260315") api_response = client.get("/app/reflections/api/20260315") body = api_response.get_json() assert page_response.status_code == 200 assert b'data-solstone-shell="spa"' in page_response.data assert api_response.status_code == 404 assert body["reason_code"] == "file_not_found" assert body["copy"] == { "heading": reflections_copy.DETAIL_EMPTY_HEADING, "desc": reflections_copy.DETAIL_EMPTY_DESC, } def test_reflections_unparseable_week_error_has_no_empty_copy(journal_copy): client = _make_client(journal_copy) response = client.get("/app/reflections/api/notaday") body = response.get_json() assert response.status_code == 404 assert body["reason_code"] == "file_not_found" assert "copy" not in body def test_reflections_raw_returns_markdown(journal_copy): _seed_reflection(journal_copy) client = _make_client(journal_copy) response = client.get("/app/reflections/20260308/raw") text = response.get_data(as_text=True) assert response.status_code == 200 assert response.headers["Content-Type"] == "text/markdown; charset=utf-8" assert text.startswith("---\ntype: weekly_reflection") def test_reflections_pdf_returns_attachment(journal_copy): _seed_reflection(journal_copy) client = _make_client(journal_copy) response = client.get("/app/reflections/20260308/pdf") assert response.status_code == 200 assert response.mimetype == "application/pdf" assert ( response.headers["Content-Disposition"] == 'attachment; filename="reflection-20260308.pdf"' ) assert response.data.startswith(b"%PDF") def test_reflections_pdf_rejects_remote_assets(journal_copy): _seed_reflection( journal_copy, """--- type: weekly_reflection week: 20260308 generated: 2026-03-10T19:00:00Z model: openai/gpt-5 sources: newsletters: 0 activities: 0 decisions: 0 followups: 0 relationship_signals: 0 gaps: [] --- ![remote](https://example.com/reflection.png) """, ) client = _make_client(journal_copy) with ( patch( "urllib.request.urlopen", side_effect=AssertionError("network disabled during reflection pdf render"), ), patch("weasyprint.default_url_fetcher") as mock_fetcher, ): response = client.get("/app/reflections/20260308/pdf") assert response.status_code == 200 assert response.mimetype == "application/pdf" assert response.data.startswith(b"%PDF") mock_fetcher.assert_not_called() def test_reflections_stats_returns_month_counts(journal_copy): _seed_reflection(journal_copy) client = _make_client(journal_copy) response = client.get("/app/reflections/api/stats/202603") assert response.status_code == 200 assert response.get_json() == {"20260308": 1}