"""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}"