"""Unit: phase-66 text pins — the History tab describes the auto-save model, not the retired Save button. House pattern (``test_stale_ui_copy.py``): read the frontend files as text and assert substrings — no browser. The browser-visible layer is gated by the dedicated story suite (``tests/e2e/test_history_copy.py``); these pins catch a silent regression in the template without it. Phase 76 (task 03): the History view is a view of the shell — ``frontend/history.html`` is deleted, the view copy lives in the shell's ``#view-history`` section, and the per-view meta description is the router's (``frontend/assets/router.js`` — the single writer of the client-side ````, value carried over from the old page's ````). The pins follow the copy to its new home. Locked decision (owner-locked A3, 2026-09-01): every conversation saves itself automatically — there is NO Save button (retired phase 55, owner- locked A2, pinned by ``test_save_share_ux.py::test_anonymous_auto_save``), so no user-visible History-tab string may name one. """ from __future__ import annotations import re from pathlib import Path FRONTEND = Path(__file__).resolve().parents[2] / "frontend" SHELL_HTML = FRONTEND / "index.html" ROUTER_JS = FRONTEND / "assets" / "router.js" # --- the locked auto-save copy (owner-locked A3) ----------------------- META = "Saved chats — every conversation is saved automatically, one click back." PAGE_SUB = ( "Every conversation is saved automatically — newest activity first. " "Click a title to return to that chat." ) EMPTY_ROW = "No saved chats yet — start a conversation and it will be saved automatically." # --- the retired manual-save copy (must be nowhere) -------------------- OLD_META = "every conversation you saved" OLD_PAGE_SUB = "you pressed Save" OLD_EMPTY_ROW = "press Save in the chat" # --- state-language survivors (must STAY — "saved" is a state, not a # button; their absence would prove over-deletion) ------------------ STATE_STRINGS = ( "

Saved chats

", "Sign in to view your saved chats", "Saved conversations are admin-only", ) def _shell() -> str: assert SHELL_HTML.is_file(), f"missing {SHELL_HTML}" return SHELL_HTML.read_text(encoding="utf-8") def _router() -> str: assert ROUTER_JS.is_file(), f"missing {ROUTER_JS}" return ROUTER_JS.read_text(encoding="utf-8") def _view(html: str) -> str: """The shell's History view section (the view is the shell's LAST view section — the slice runs to the container main's close).""" start = html.find('
", start) assert end != -1 return html[start:end] def _norm(text: str) -> str: """Collapse whitespace runs — the templates wrap long lines, so the locked copy is pinned against the normalized text.""" return re.sub(r"\s+", " ", text).strip() def test_manual_save_copy_is_gone() -> None: """The shell (which carries the History view) and the router (which carries the per-view meta): the three retired manual-save strings are GONE — the copy no longer tells the visitor to press a Save button.""" for text in (_view(_shell()), _router()): for frag in (OLD_META, OLD_PAGE_SUB, OLD_EMPTY_ROW): assert frag not in text, f"retired manual-save copy still present: {frag!r}" def test_locked_auto_save_copy_present_exactly_once() -> None: """The three locked (A3) auto-save strings, each exactly once (a second copy could drift out of sync). Phase 76 (task 03): the meta description lives in the router's DESCRIPTIONS table (the router is the single writer of the client-side meta — the value is carried over from the old page's ), and the page-sub + empty-row strings live in the shell's History view.""" assert _router().count(META) == 1, "the locked meta description (router-owned)" view = _norm(_view(_shell())) assert view.count(PAGE_SUB) == 1, "the locked page-sub" assert view.count(EMPTY_ROW) == 1, "the locked empty-row string" def test_state_language_survivors_are_untouched() -> None: """Proof of NO over-deletion: the strings where "saved" is a state — the

, the anonymous gate title, and the gate sub — stay exactly as phase 50 wrote them (in the shell's History view).""" view = _view(_shell()) for frag in STATE_STRINGS: assert frag in view, f"state-language survivor deleted: {frag!r}"