diff --git a/solstone/apps/news/app.json b/solstone/apps/news/app.json index b6c039b4d..2279c097b 100644 --- a/solstone/apps/news/app.json +++ b/solstone/apps/news/app.json @@ -1,7 +1,13 @@ { "icon": "📰", "label": "newsletters", - "date_nav": false, + "date_nav": { + "unit": { + "one": "newsletter", + "other": "newsletters", + "none": "no newsletters" + } + }, "facets": { "disabled": true } diff --git a/solstone/apps/news/copy.py b/solstone/apps/news/copy.py index c57a21c17..34fb869f5 100644 --- a/solstone/apps/news/copy.py +++ b/solstone/apps/news/copy.py @@ -30,9 +30,23 @@ NEWS_POPULATED_FRAMING = ( ) NEWS_POPULATED_SAMPLE_LINK = "see a sample" NEWS_POPULATED_NEXT_FOOTER = "next newsletters: {when}" +NEWS_GRID_TITLE = "newsletter days" +NEWS_GRID_LEDE_ONE = "1 newsletter since {month}." +NEWS_GRID_LEDE_OTHER = "{count} newsletters since {month}." +NEWS_GRID_UNIT_ONE = "newsletter" +NEWS_GRID_UNIT_OTHER = "newsletters" +NEWS_GRID_UNIT_NONE = "no newsletters" NEWS_DETAIL_SUBTITLE = "sol's notes for {facet} on this day." NEWS_DETAIL_DEBUG_LINK = "see how this was generated →" +NEWS_DETAIL_EMPTY_TITLE = "no {facet} newsletter" +NEWS_DETAIL_EMPTY_BODY = "There is no {facet} newsletter for {date_label}." +NEWS_DETAIL_EMPTY_DAY_LINK = "see all newsletters for this day →" + +NEWS_DAY_TITLE = "newsletters for {date_label}" +NEWS_DAY_SUBTITLE = "facet summaries for this day." +NEWS_DAY_EMPTY_TITLE = "no newsletters for {date_label}" +NEWS_DAY_EMPTY_BODY = "No newsletters were written for this day." NEWS_SAMPLE_BANNER = "This is a sample newsletter — not from your journal." NEWS_SAMPLE_H1 = "sample newsletter" diff --git a/solstone/apps/news/routes.py b/solstone/apps/news/routes.py index a298e560b..d1eebf0bb 100644 --- a/solstone/apps/news/routes.py +++ b/solstone/apps/news/routes.py @@ -15,7 +15,9 @@ from markdown import Markdown from solstone.apps.news import copy as news_copy from solstone.apps.news.dates import format_news_list_date, next_newsletter_when -from solstone.convey.reasons import FILE_NOT_FOUND +from solstone.convey.date_nav import build_date_nav_index +from solstone.convey.day_grid import build_day_grid_payload +from solstone.convey.reasons import FILE_NOT_FOUND, INVALID_DAY, INVALID_MONTH from solstone.convey.utils import DATE_RE, error_response from solstone.think.features import require_extra from solstone.think.utils import get_journal, get_owner_timezone @@ -73,12 +75,59 @@ def _list_newsletters() -> list[dict[str, str]]: continue rows.append({"facet": facet_dir.name, "day": day}) - rows.sort(key=lambda r: (r["day"], r["facet"]), reverse=True) - # Adjust facet ordering to ascending within each day. - rows.sort(key=lambda r: r["day"], reverse=True) + rows.sort(key=lambda r: (-int(r["day"]), r["facet"])) return rows +def _format_month_name(day: str) -> str: + return datetime.strptime(day, "%Y%m%d").strftime("%B %Y") + + +def _newsletter_counts_by_day(rows: list[dict[str, str]]) -> dict[str, int]: + counts: dict[str, int] = {} + for row in rows: + day = row["day"] + counts[day] = counts.get(day, 0) + 1 + return counts + + +def _newsletter_list_item(row: dict[str, str]) -> dict[str, str]: + return { + "facet": row["facet"], + "day": row["day"], + "label": format_news_list_date(row["day"]), + "url": url_for("app:news.detail", facet=row["facet"], day=row["day"]), + } + + +def _day_copy(day: str) -> dict[str, str]: + date_label = format_news_list_date(day) + return { + "title": news_copy.NEWS_DAY_TITLE.format(date_label=date_label), + "subtitle": news_copy.NEWS_DAY_SUBTITLE, + "empty_title": news_copy.NEWS_DAY_EMPTY_TITLE.format(date_label=date_label), + "empty_body": news_copy.NEWS_DAY_EMPTY_BODY, + } + + +def _empty_detail_payload(facet: str, day: str) -> dict[str, Any]: + date_label = format_news_list_date(day) + return { + "empty": True, + "facet": facet, + "day": day, + "date_label": date_label, + "day_url": url_for("app:news.day_view", day=day), + "copy": { + "empty_title": news_copy.NEWS_DETAIL_EMPTY_TITLE.format(facet=facet), + "empty_body": news_copy.NEWS_DETAIL_EMPTY_BODY.format( + facet=facet, date_label=date_label + ), + "day_link": news_copy.NEWS_DETAIL_EMPTY_DAY_LINK, + }, + } + + def _load_newsletter(facet: str, day: str) -> tuple[Path, str, frontmatter.Post]: path = _newsletter_path(facet, day) if not path.is_file(): @@ -129,26 +178,31 @@ def index() -> Any: @news_bp.route("/api/state") def api_state() -> Any: rows = _list_newsletters() + total_count = len(rows) when = next_newsletter_when(_today()) - newsletters = [ - { - "facet": row["facet"], - "day": row["day"], - "label": format_news_list_date(row["day"]), - "url": url_for("app:news.detail", facet=row["facet"], day=row["day"]), - } - for row in rows - ] + newsletters = [_newsletter_list_item(row) for row in rows[:60]] empty_next = news_copy.NEWS_EMPTY_TOMORROW_WITH_DATE.format(tomorrow=when) populated_next_footer = news_copy.NEWS_POPULATED_NEXT_FOOTER.format(when=when) if not _journal_has_any_observer_input(): empty_next = news_copy.NEWS_EMPTY_NO_DATE + if rows: + template = ( + news_copy.NEWS_GRID_LEDE_ONE + if total_count == 1 + else news_copy.NEWS_GRID_LEDE_OTHER + ) + grid_lede = template.format( + count=total_count, month=_format_month_name(rows[-1]["day"]) + ) + else: + grid_lede = None return jsonify( { "newsletters": newsletters, + "total_count": total_count, "copy": { "kicker": news_copy.NEWS_KICKER, "index_h1": news_copy.NEWS_INDEX_H1, @@ -161,11 +215,68 @@ def api_state() -> Any: "populated_framing": news_copy.NEWS_POPULATED_FRAMING, "populated_sample_link": news_copy.NEWS_POPULATED_SAMPLE_LINK, "populated_next_footer": populated_next_footer, + "grid_title": news_copy.NEWS_GRID_TITLE, + "grid_lede": grid_lede, + "grid_unit_one": news_copy.NEWS_GRID_UNIT_ONE, + "grid_unit_other": news_copy.NEWS_GRID_UNIT_OTHER, + "grid_unit_none": news_copy.NEWS_GRID_UNIT_NONE, }, } ) +@news_bp.route("/api/index") +def api_index() -> Any: + return jsonify(build_date_nav_index(_newsletter_counts_by_day(_list_newsletters()))) + + +@news_bp.route("/api/grid") +def api_grid() -> Any: + rows = _list_newsletters() + counts = _newsletter_counts_by_day(rows) + coverage = ( + {"start": min(counts), "end": _today().strftime("%Y%m%d")} if counts else None + ) + return jsonify( + build_day_grid_payload( + counts, + max(counts, default=None), + coverage=coverage, + ) + ) + + +@news_bp.route("/api/stats/") +def api_stats(month: str) -> Any: + if len(month) != 6 or not month.isdigit(): + return error_response( + INVALID_MONTH, + detail="Invalid month format, expected YYYYMM", + ) + + counts = _newsletter_counts_by_day(_list_newsletters()) + return jsonify( + {day: count for day, count in counts.items() if day.startswith(month)} + ) + + +@news_bp.route("/api/day/") +def api_day(day: str) -> Any: + if not DATE_RE.fullmatch(day): + return error_response(INVALID_DAY, status=404, detail="Day not found") + + rows = [row for row in _list_newsletters() if row["day"] == day] + payload: dict[str, Any] = { + "day": day, + "date_label": format_news_list_date(day), + "newsletters": [_newsletter_list_item(row) for row in rows], + "copy": _day_copy(day), + } + if not rows: + payload["empty"] = True + return jsonify(payload) + + @news_bp.route("/sample") def sample() -> Any: return current_app.send_static_file("shell.html") @@ -194,6 +305,14 @@ def sample_raw() -> Any: ) +@news_bp.route("/") +def day_view(day: str) -> Any: + if not DATE_RE.fullmatch(day): + return error_response(INVALID_DAY, status=404, detail="Day not found") + + return current_app.send_static_file("shell.html") + + @news_bp.route("//") def detail(facet: str, day: str) -> Any: return current_app.send_static_file("shell.html") @@ -207,7 +326,7 @@ def api_detail(facet: str, day: str) -> Any: try: _path, _raw_markdown, post = _load_newsletter(facet, day) except FileNotFoundError: - return error_response(FILE_NOT_FOUND, detail="Newsletter not found") + return jsonify(_empty_detail_payload(facet, day)) return jsonify( { diff --git a/solstone/apps/news/tests/test_spa_routes.py b/solstone/apps/news/tests/test_spa_routes.py index decd8dcf0..25f6056a8 100644 --- a/solstone/apps/news/tests/test_spa_routes.py +++ b/solstone/apps/news/tests/test_spa_routes.py @@ -5,10 +5,12 @@ from __future__ import annotations import ast import json +from datetime import date, timedelta from pathlib import Path import pytest +from solstone.apps.news import routes as news_routes from solstone.convey import create_app APP_ROOT = Path(__file__).resolve().parents[1] @@ -47,6 +49,15 @@ def _seed_news(journal: Path) -> None: target.write_text("# verona\n\nbody\n", encoding="utf-8") +def _seed_news_for( + journal: Path, facet: str, day: str, body: str | None = None +) -> None: + (journal / "chronicle" / day).mkdir(parents=True, exist_ok=True) + target = journal / "facets" / facet / "news" / f"{day}.md" + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(body or f"# {facet} {day}\n\nbody\n", encoding="utf-8") + + def _render_template_call_functions() -> list[str]: tree = ast.parse(ROUTES_PATH.read_text(encoding="utf-8")) parents: dict[ast.AST, ast.AST] = {} @@ -106,7 +117,8 @@ def test_news_state_payload_shape(news_env): data = response.get_json() assert response.status_code == 200 - assert set(data) == {"copy", "newsletters"} + assert set(data) == {"copy", "newsletters", "total_count"} + assert data["total_count"] == 1 assert data["newsletters"] == [ { "facet": "verona", @@ -119,7 +131,202 @@ def test_news_state_payload_shape(news_env): "Your first newsletters arrive tomorrow morning." ) assert data["copy"]["populated_next_footer"] == "next newsletters: tomorrow morning" + assert data["copy"]["grid_lede"] == "1 newsletter since May 2026." def test_news_routes_render_template_only_in_pdf_helper(): assert _render_template_call_functions() == ["_render_newsletter_pdf"] + + +def test_news_state_orders_day_desc_then_facet_asc(news_env): + _seed_news_for(news_env.journal, "zeta", "20260526") + _seed_news_for(news_env.journal, "alpha", "20260526") + _seed_news_for(news_env.journal, "beta", "20260525") + + response = news_env.client.get("/app/news/api/state") + data = response.get_json() + + assert response.status_code == 200 + assert [(item["facet"], item["day"]) for item in data["newsletters"]] == [ + ("alpha", "20260526"), + ("zeta", "20260526"), + ("beta", "20260525"), + ] + + +def test_news_state_bounds_to_newest_sixty_with_full_total(news_env): + start = date(2026, 1, 1) + days: list[str] = [] + for offset in range(65): + day = (start + timedelta(days=offset)).strftime("%Y%m%d") + days.append(day) + _seed_news_for(news_env.journal, "verona", day) + + response = news_env.client.get("/app/news/api/state") + data = response.get_json() + + assert response.status_code == 200 + assert data["total_count"] == 65 + assert len(data["newsletters"]) == 60 + assert [item["day"] for item in data["newsletters"]] == list(reversed(days[5:])) + assert data["copy"]["grid_lede"] == "65 newsletters since January 2026." + + +def test_news_grid_payload_counts_coverage_and_watermark(news_env, monkeypatch): + _seed_news_for(news_env.journal, "alpha", "20260105") + _seed_news_for(news_env.journal, "zeta", "20260105") + _seed_news_for(news_env.journal, "beta", "20260201") + monkeypatch.setattr(news_routes, "_today", lambda: date(2026, 7, 17)) + calls: list[tuple[dict[str, int], str | None, dict[str, object]]] = [] + real_builder = news_routes.build_day_grid_payload + + def spy(counts, watermark, **kwargs): + calls.append((dict(counts), watermark, dict(kwargs))) + return real_builder(counts, watermark, **kwargs) + + monkeypatch.setattr(news_routes, "build_day_grid_payload", spy) + + response = news_env.client.get("/app/news/api/grid") + data = response.get_json() + + assert response.status_code == 200 + assert calls == [ + ( + {"20260201": 1, "20260105": 2}, + "20260201", + {"coverage": {"start": "20260105", "end": "20260717"}}, + ) + ] + assert data == { + "coverage": {"start": "20260105", "end": "20260717"}, + "days": {"20260105": 2, "20260201": 1}, + "pending": {}, + } + + +def test_news_grid_empty_journal(news_env): + response = news_env.client.get("/app/news/api/grid") + + assert response.status_code == 200 + assert response.get_json() == {"coverage": None, "days": {}, "pending": {}} + + +def test_news_index_uses_date_nav_index(news_env, monkeypatch): + _seed_news_for(news_env.journal, "alpha", "20260526") + _seed_news_for(news_env.journal, "zeta", "20260526") + _seed_news_for(news_env.journal, "beta", "20260601") + calls: list[dict[str, int]] = [] + + def spy(counts): + calls.append(dict(counts)) + return {"coverage": {"start": "20260526", "end": "20260601"}, "months": {}} + + monkeypatch.setattr(news_routes, "build_date_nav_index", spy) + + response = news_env.client.get("/app/news/api/index") + + assert response.status_code == 200 + assert calls == [{"20260601": 1, "20260526": 2}] + assert response.get_json() == { + "coverage": {"start": "20260526", "end": "20260601"}, + "months": {}, + } + + +def test_news_index_empty_journal(news_env): + response = news_env.client.get("/app/news/api/index") + + assert response.status_code == 200 + assert response.get_json() == {"coverage": None, "months": {}} + + +def test_news_stats_month_counts_and_index_cross_check(news_env): + _seed_news_for(news_env.journal, "alpha", "20260526") + _seed_news_for(news_env.journal, "zeta", "20260526") + _seed_news_for(news_env.journal, "beta", "20260601") + + may_response = news_env.client.get("/app/news/api/stats/202605") + empty_response = news_env.client.get("/app/news/api/stats/202604") + invalid_response = news_env.client.get("/app/news/api/stats/2026aa") + index = news_env.client.get("/app/news/api/index").get_json() + + assert may_response.status_code == 200 + assert may_response.get_json() == {"20260526": 2} + assert empty_response.status_code == 200 + assert empty_response.get_json() == {} + assert invalid_response.status_code == 400 + invalid = invalid_response.get_json() + assert invalid["reason_code"] == "invalid_month" + assert invalid["detail"] == "Invalid month format, expected YYYYMM" + for month, total in index["months"].items(): + stats = news_env.client.get(f"/app/news/api/stats/{month}").get_json() + assert sum(stats.values()) == total + + +def test_news_day_page_and_api(news_env): + _seed_news_for(news_env.journal, "zeta", "20260526") + _seed_news_for(news_env.journal, "alpha", "20260526") + + page_response = news_env.client.get("/app/news/20260526") + garbage_response = news_env.client.get("/app/news/garbage") + api_response = news_env.client.get("/app/news/api/day/20260526") + empty_response = news_env.client.get("/app/news/api/day/20260527") + invalid_response = news_env.client.get("/app/news/api/day/garbage") + + assert page_response.status_code == 200 + assert b'data-solstone-shell="spa"' in page_response.data + assert garbage_response.status_code == 404 + assert garbage_response.get_json()["reason_code"] == "invalid_day" + assert api_response.status_code == 200 + data = api_response.get_json() + assert data["day"] == "20260526" + assert data["date_label"] == "Tue May 26, 2026" + assert [ + (item["facet"], item["label"], item["url"]) for item in data["newsletters"] + ] == [ + ("alpha", "Tue May 26, 2026", "/app/news/alpha/20260526"), + ("zeta", "Tue May 26, 2026", "/app/news/zeta/20260526"), + ] + assert empty_response.status_code == 200 + empty = empty_response.get_json() + assert empty["empty"] is True + assert "reason_code" not in empty + assert invalid_response.status_code == 404 + assert invalid_response.get_json()["reason_code"] == "invalid_day" + + +def test_news_detail_empty_and_malformed_paths(news_env): + _seed_news_for(news_env.journal, "alpha", "20260526") + + existing_response = news_env.client.get("/app/news/api/alpha/20260526") + missing_response = news_env.client.get("/app/news/api/zeta/20260526") + malformed_response = news_env.client.get("/app/news/api/alpha/notaday") + + assert existing_response.status_code == 200 + existing = existing_response.get_json() + assert "empty" not in existing + assert existing["raw_url"] == "/app/news/alpha/20260526/raw" + assert existing["pdf_url"] == "/app/news/alpha/20260526/pdf" + assert existing["debug_link_url"] == "/app/sol/20260526/talents/facet_newsletter" + assert missing_response.status_code == 200 + missing = missing_response.get_json() + assert missing["empty"] is True + assert missing["day_url"] == "/app/news/20260526" + assert "reason_code" not in missing + assert malformed_response.status_code == 404 + assert malformed_response.get_json()["reason_code"] == "file_not_found" + + +def test_news_workspace_day_axis_source_hooks(news_env): + source = WORKSPACE_PATH.read_text(encoding="utf-8") + + assert "data-date-nav" in source + assert "data-date-nav-heading" in source + assert "mode: 'day'" in source + assert "/app/news/api/day/" in source + assert "if (payload.empty)" in source + assert "context.mode !== 'detail'" in source + assert "context.mode !== 'sample'" not in source + assert "minSpanDays: 70, minActiveDays: 14" in source + assert "fetch('/app/news/api/grid'" in source + assert "hideNewsGrid(card, host, legend, unit);" in source diff --git a/solstone/apps/news/workspace.html b/solstone/apps/news/workspace.html index d934f40bf..96c9d9926 100644 --- a/solstone/apps/news/workspace.html +++ b/solstone/apps/news/workspace.html @@ -155,6 +155,45 @@ color: #6b7280; } +.news-grid-card { + background: var(--paper); + border: 1px solid var(--hairline); + border-radius: var(--radius); + padding: var(--space-4); + margin-bottom: var(--space-4); +} + +.news-grid-card[hidden] { + display: none; +} + +.news-grid-title { + margin: 0; + color: var(--ink); + font-size: 1rem; +} + +.news-grid-lede { + margin: var(--space-2) 0 var(--space-3); + color: var(--ink-soft-paper); + line-height: 1.5; +} + +.news-grid-host { + margin-top: var(--space-3); +} + +.news-grid-legend { + margin-top: var(--space-2); + color: var(--ink-faint-paper); +} + +.news-empty-title { + margin: 0 0 var(--space-2); + color: var(--ink); + font-size: 1.1rem; +} + .news-debug-link { margin: 1.5rem 0 0; font-size: 0.9rem; @@ -189,6 +228,8 @@
+
+

@@ -219,6 +260,13 @@ if (first === 'sample') { return {mode: 'sample', url: '/app/news/api/sample'}; } + if (/^\d{8}$/.test(first) && !parts[4]) { + return { + mode: 'day', + day: first, + url: `/app/news/api/day/${encodeURIComponent(first)}` + }; + } if (parts[3] && parts[4]) { const facet = decodePart(parts[3]); const day = decodePart(parts[4]); @@ -230,6 +278,12 @@ return {mode: 'index', url: '/app/news/api/state'}; } + function suppressDateNavForDetail(context) { + if (context.mode !== 'detail') return; + document.querySelector('[data-date-nav]')?.remove(); + document.querySelector('[data-date-nav-heading]')?.remove(); + } + function renderLoading() { content.innerHTML = window.SurfaceState ? window.SurfaceState.loading({text: 'loading newsletters...'}) @@ -291,6 +345,71 @@ } } + function newsGridUnit(copy) { + return { + one: copy.grid_unit_one, + other: copy.grid_unit_other, + none: copy.grid_unit_none, + }; + } + + function hideNewsGrid(card, host, legend, unit) { + card.hidden = true; + if (window.DayGrid) { + window.DayGrid.mount(host, { + data: null, + mode: 'navigate', + appPath: '/app/news', + unit, + granularity: 'day', + }); + } else { + host.replaceChildren(); + } + legend.replaceChildren(); + } + + async function fetchNewsGrid() { + try { + const response = await fetch('/app/news/api/grid', {cache: 'no-store'}); + if (!response.ok) return null; + return response.json(); + } catch (_error) { + return null; + } + } + + async function renderNewsGrid(copy) { + const card = document.getElementById('newsGridCard'); + const host = document.getElementById('newsGridHost'); + const legend = document.getElementById('newsGridLegend'); + if (!card || !host || !legend) return; + const unit = newsGridUnit(copy); + const data = await fetchNewsGrid(); + if (!data || !window.DayGrid || !copy.grid_lede) { + hideNewsGrid(card, host, legend, unit); + return; + } + + // Newsletters need both a long enough span and enough active days to make + // the overview useful; keep this calibration explicit for this adopter. + const gate = window.DayGrid.gate(data, {minSpanDays: 70, minActiveDays: 14}); + if (!gate.ok) { + hideNewsGrid(card, host, legend, unit); + return; + } + + card.hidden = false; + window.DayGrid.mount(host, { + data, + mode: 'navigate', + appPath: '/app/news', + unit, + granularity: 'day', + }); + window.DayGrid.legend(legend, {unit, data}); + } + function renderIndex(payload) { const copy = payload.copy || {}; const newsletters = payload.newsletters || []; @@ -328,11 +447,71 @@

${escapeHtml(copy.subtitle)}

+
${listHtml}
`; + renderNewsGrid(copy); + } + + function renderDay(payload) { + const copy = payload.copy || {}; + const newsletters = payload.newsletters || []; + const bodyHtml = payload.empty + ? ` +
+

${escapeHtml(copy.empty_title)}

+

${escapeHtml(copy.empty_body)}

+
+ ` + : ` + + `; + content.innerHTML = ` +
+
+

${escapeHtml(copy.title)}

+

${escapeHtml(copy.subtitle)}

+
+
+
${bodyHtml}
+ `; + } + + function renderDetailEmpty(payload) { + const copy = payload.copy || {}; + content.innerHTML = ` +
+
+

${escapeHtml(copy.empty_title)}

+

${escapeHtml(copy.empty_body)}

+
+
+
+

${escapeHtml(copy.day_link)}

+
+ `; } function renderDetail(payload) { + if (payload.empty) { + renderDetailEmpty(payload); + return; + } content.innerHTML = `
@@ -373,13 +552,18 @@ attachCopyButton(payload.raw_url); } + const initialContext = viewContext(); + suppressDateNavForDetail(initialContext); + async function load() { - const context = viewContext(); + const context = initialContext; renderLoading(); try { const payload = await window.apiJson(context.url); if (context.mode === 'sample') { renderSample(payload); + } else if (context.mode === 'day') { + renderDay(payload); } else if (context.mode === 'detail') { renderDetail(payload); } else { diff --git a/tests/test_app_news.py b/tests/test_app_news.py index 0c5ce1db1..fcc27a016 100644 --- a/tests/test_app_news.py +++ b/tests/test_app_news.py @@ -145,7 +145,7 @@ def test_news_detail_api_returns_file(journal_copy): assert data["markdown"].startswith("# 2026-05-26 personal") -def test_news_detail_missing_page_shell_and_api_404(journal_copy): +def test_news_detail_missing_page_shell_and_api_empty_state(journal_copy): _clear_news(journal_copy) client = _make_client(journal_copy) @@ -154,8 +154,10 @@ def test_news_detail_missing_page_shell_and_api_404(journal_copy): assert page_response.status_code == 200 assert b'data-solstone-shell="spa"' in page_response.data - assert api_response.status_code == 404 - assert api_response.get_json()["reason_code"] == "file_not_found" + assert api_response.status_code == 200 + data = api_response.get_json() + assert data["empty"] is True + assert "reason_code" not in data def test_news_sample_api_returns_inlined_content(journal_copy): diff --git a/tests/test_convey_apps.py b/tests/test_convey_apps.py index 13cd9169f..9a7f7e280 100644 --- a/tests/test_convey_apps.py +++ b/tests/test_convey_apps.py @@ -145,6 +145,7 @@ def test_shell_payload_emits_normalized_date_nav(monkeypatch): "activities", "body", "chat", + "news", "reflections", "sol", "speakers", @@ -152,7 +153,6 @@ def test_shell_payload_emits_normalized_date_nav(monkeypatch): "tokens", "transcripts", ] - assert apps["news"]["date_nav"] is None for name, app in apps.items(): assert "allow_future_dates" not in app if app["date_nav"]: