Files
brain-of-reese/tests/unit/test_frontend_verbatim_css.py
T
ducoterra b34b8ca672
Build and Push Containers / build-and-push-app (push) Successful in 1m46s
Build and Push Containers / build-and-push-db (push) Successful in 12s
phase: 182_verbatim_show_text
**Phase 182 — final verification pass (all 8 tasks already complete; this was verification + docs + one defect fix).**

- Verified: `app/rag/verbatim.py` (locator grammar, `splitlines()` slice, line-boundary cap, `resolve_answer_blocks`), `SHOW_TEXT_TOOL` + `shown_texts`, `SHOW_TEXT_CLAUSE`, `done.answer_text` (9 fields, key dropped when empty) + `ChatMessageRecord.answer_text` (≤50, 600 000-char cap counted in the 8 M budget), `verbatim.js` + 4 `{verbatim:true}` renderers, `Containerfile` minify step, `.env.example` 3 vars.
- `uv run python -c "…answer_text, show_text_max_chars, show_text_max_blocks"` → `True 12000 5`; each negative refuses startup naming its field; `0` accepted as inert ✅
- `bash .agents/validate.sh` → **validation OK**: 4320 unit+integration tests green, `TOTAL 6849 17 → 99%` (gate app/ >90% OK), `ruff check .` "All checks passed!", `pyright` 0 errors (re-run after my edits, 08:28).
- E2E, each in isolation `--no-cov`: `test_verbatim_show_text.py` 13 passed (panel bytes == `documents.content`, server caption, clipboard == server string, `/document.html?source=…&path=…`, honest unresolved state, reload+share, `show_image` regression, flag-off app) ✅; green: `test_answer_images` `test_harness_aligned_tools` `test_agent_document_tools` `test_markdown_tables` `test_markdown_inline_links_images` `test_svg_document_preview` `test_smoke` `test_document_viewer` `test_markdown_blockquotes` `test_chat_persistence` `test_thinking_timeline` `test_chat_rag` `test_settings_mobile_value_width`.
- **Defect found & fixed (pre-existing, not phase 182):** `tests/e2e/test_admin_settings_panel.py::test_admin_sees_groups_and_source_badges` failed — its whole-page `assert "reese" not in page.content()` tripped on the shell's onboarding chips, which follow the newest saved chats in the one shared E2E DB (a leftover `reeselink.json` question from a tool suite). Scoped the strict sweep to `.settings-shell` and kept a whole-page `reese:reese` credential needle → 12 passed with the leftovers still present.
- Docs: README fixed 2 inconsistencies — Quick Start omitted the **required** `BOR_ADMIN_PASSWORD`/`BOR_SESSION_SECRET` (the app fail-loud refuses boot) and the broken anchor `#chat--` → `#chat-`; full re-read of README (1816 lines) + AGENTS confirmed the phase-182 feature/settings/log/UI-contract text matches the code, and every `.env.example` var + every `Settings` field is documented.
- Notable: kept `resolve_answer_blocks` synchronous on the SSE terminal path (bounded indexed SELECTs, matching the existing `query_log` precedent and the phase's documented fence-lookup bound); no live-infra changes made.
- Next pending phase: none — `todo/` holds only 182 (its 8 task files are in `complete/`; the harness commits + moves `00_phase.md`).
2026-10-15 08:33:12 -04:00

466 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Unit: the phase-182 verbatim-panel stylesheet contract (task 06).
The panel DOM is built by ``verbatim.js`` (task 07) and its streaming
placeholder by ``markdown.js`` (task 06); this module pins the CSS contract
that makes the phase readable, in the ``test_frontend_cluster_css.py`` /
``test_markdown_inline_links_images.py`` house pattern (no browser — the
browser-level evidence is the phase's Playwright suite, task 08):
* the phase-182 block EXISTS as one clearly-labelled region of
``frontend/assets/styles.css`` (BEGIN/END markers) and every selector the
contract names is styled there — ``.verbatim``, ``.verbatim-head``,
``.verbatim-caption``, ``.verbatim-badge``, ``.verbatim-body``,
``.verbatim-lines``, ``.verbatim-pending``, ``.verbatim-state``,
``.verbatim-unresolved``, ``.code-block``, ``.code-block-copy``,
``.verbatim-copy``;
* the panel is a BOUNDED scroll panel (PLAN §7.1): ``max-height`` in the
``min(…, 70vh)`` form + ``overflow: auto``, with the head row STICKY
(``position: sticky; top: 0``) on an opaque background so the caption
never scrolls away and rows never show through it;
* the code surface never wraps (phase 116 locked A1, the phase-165 prose/code
split): ``white-space: pre`` + ``overflow-wrap: normal`` + ``tab-size: 2``,
and the line-number gutter is ``user-select: none`` so a Copy/select never
drags numbers into the clipboard;
* text + color, never color alone (WCAG 1.4.1): the badge is a TEXT badge
with ``text-transform: none``, the unresolved state hides the badge, and
the ``.copied`` state exists beside the text swap task 07 performs;
* the whole block is PALETTE-VARIABLES ONLY (the phase-92 zero-literal /
monochrome invariant): no hex/rgb/hsl literal anywhere, and every
``var()`` is a declared ``:root`` token — so a light theme keeps this code
surface readable and no new hue enters the system;
* the ≤640px block lifts the Copy controls to the 44px touch floor, and
nothing in the block animates (so ``prefers-reduced-motion`` has nothing
to still — the rule is written as a guard so a future animation cannot be
added without its still).
"""
from __future__ import annotations
import re
from pathlib import Path
import pytest
STYLES_CSS = Path(__file__).resolve().parents[2] / "frontend" / "assets" / "styles.css"
#: The phase-182 region markers (the block is one contiguous, labelled
#: region, so "no literals in the phase-182 block" is a parseable claim).
BEGIN = "/* ---------- Verbatim panels (phase 182, task 06) — BEGIN ----------"
END = "/* ---------- Verbatim panels (phase 182, task 06) — END ---------- */"
#: Every selector the task's contract names.
PANEL_SELECTORS = (
".verbatim",
".verbatim-head",
".verbatim-caption",
".verbatim-badge",
".verbatim-body",
".verbatim-lines",
".verbatim-pending",
".verbatim-state",
".verbatim-unresolved",
".code-block",
".code-block-copy",
".verbatim-copy",
)
def _css() -> str:
return STYLES_CSS.read_text(encoding="utf-8")
def _phase182_block(css: str) -> str:
"""The phase-182 region, markers included (comments intact — the block
carries the contract in prose too)."""
start = css.find(BEGIN)
assert start != -1, "styles.css must carry the labelled phase-182 block (BEGIN marker)"
end = css.find(END, start)
assert end != -1, "the phase-182 block must be closed by its END marker"
return css[start : end + len(END)]
def _strip_comments(css: str) -> str:
return re.sub(r"/\*[\s\S]*?\*/", "", css)
def _rule_bodies(block: str, selector: str) -> list[str]:
"""The bodies of every rule whose selector list contains ``selector``
EXACTLY (the phase-180 parse pattern, comment-stripped)."""
bodies: list[str] = []
for match in re.finditer(r"([^{}]+)\{([^{}]*)\}", _strip_comments(block)):
selectors = [s.strip() for s in " ".join(match.group(1).split()).split(",")]
if selector in selectors:
bodies.append(match.group(2))
return bodies
def _body(block: str, selector: str) -> str:
bodies = _rule_bodies(block, selector)
assert bodies, f"styles.css must style {selector}"
return " ".join(bodies)
def _root_palette(css: str) -> set[str]:
"""Every variable name the ``:root`` block declares."""
root = re.search(r":root\s*\{", css)
assert root, "styles.css must define :root"
body = css[root.end() : css.find("}", root.end())]
palette = set(re.findall(r"(--[a-z0-9-]+)\s*:", body))
assert palette, "the :root palette must declare variables"
return palette
def _media_block(css: str, query: str) -> str:
"""The body of the ``@media (<query>)`` block — the house non-greedy
pattern (nested closing braces are indented, so the first column-0 ``}``
is the block's own end)."""
m = re.search("@media \\(" + re.escape(query) + r"\) \{([\s\S]*?)\n\}", css)
assert m, f"{query} media query missing"
return m.group(1)
_CSS = _css()
_BLOCK = _phase182_block(_CSS)
_ROOT = _root_palette(_CSS)
# ---------------------------------------------------------------------------
# the block and its selectors
# ---------------------------------------------------------------------------
def test_phase_182_block_is_labelled_and_contains_every_selector() -> None:
"""The block is ONE clearly-labelled region carrying every selector of
the contract (task 06 item 5) — a rule that drifted outside the region
would escape the palette-only and reduced-motion pins below."""
for selector in PANEL_SELECTORS:
assert _rule_bodies(_BLOCK, selector), f"the phase-182 block must style {selector}"
# …and the block says what it is.
assert "phase 182" in _BLOCK
assert "VERBATIM" in _BLOCK, "the block documents the badge it styles"
def test_panel_is_a_themed_surface_card() -> None:
"""``.verbatim`` is a block on the theme's own canvas: the bubble's
rhythm (a vertical margin), a --line hairline, --radius-sm corners, the
--surface fill, ``min-width: 0`` (the long-line escape hatch every
scroller here carries) and ``overflow: hidden`` so the rounded corners
clip the scrolled body."""
body = _body(_BLOCK, ".verbatim")
for prop in (
"display: block",
"min-width: 0",
"background: var(--surface)",
"border: 1px solid var(--line)",
"border-radius: var(--radius-sm)",
"overflow: hidden",
):
assert prop in body, f".verbatim must carry {prop}"
assert re.search(r"margin:\s0\.[0-9]+rem 0;", body), (
".verbatim sits in the bubble's rhythm (a rem margin, not a px one)"
)
def test_head_row_is_sticky_with_an_opaque_background() -> None:
"""PLAN §7.1's bounded-panel rule: the head row STICKS to the top of the
scroll panel, so the caption (``source/path · lines 12–40 of 210``) is
still on screen in a 40-line slice. OPAQUE ``--surface`` background — a
translucent head would let the bytes read through it (the phase-156
``.table-wrap table thead th`` idiom)."""
body = _body(_BLOCK, ".verbatim-head")
assert "position: sticky" in body, "the head row must be sticky"
assert "top: 0" in body, "the sticky offset is the panel top"
assert "background: var(--surface)" in body, "the sticky head must be opaque"
assert "z-index: 1" in body, "the sticky head paints above the scrolled body"
assert "display: flex" in body, "the head is a row (caption, badge, copy)"
def test_caption_is_mono_soft_and_clipped_the_house_way() -> None:
"""``.verbatim-caption`` uses the phase-156 ``.cell-clip`` ellipsis
convention (``min-width: 0`` + ``overflow: hidden`` +
``text-overflow: ellipsis`` + ``white-space: nowrap``) so a long
``source/path`` clips to ``…`` instead of breaking the head row — the
FULL text stays reachable through the control's ``aria-label``/``title``
(the clip is visual only). ``--ink-soft`` on ``--surface`` is 8.2:1
(AA), and the mono family matches every other path in the app."""
body = _body(_BLOCK, ".verbatim-caption")
for prop in (
"min-width: 0",
"overflow: hidden",
"text-overflow: ellipsis",
"white-space: nowrap",
"font-family: var(--mono)",
"color: var(--ink-soft)",
):
assert prop in body, f".verbatim-caption must carry {prop}"
# the ellipsis convention must not rely on a fixed max-width here: the
# caption is the flexible item of a wrapping flex row.
assert "flex: 1" in body, "the caption takes the row's flexible space"
def test_badge_is_text_with_no_uppercase_surprise() -> None:
"""The badge is the WORD ``VERBATIM`` in the --ok family (10.6:1 on its
own fill) with ``text-transform: none`` — an uppercase-forced word is
spelled out letter-by-letter by some screen readers, and an icon-only
badge would fail WCAG 1.4.1 (the claim must be TEXT)."""
body = _body(_BLOCK, ".verbatim-badge")
assert "text-transform: none" in body
assert "color: var(--ok-ink)" in body, "the badge is the --ok family"
assert "background: var(--ok-bg)" in body
assert "content:" not in body, "the badge text is DOM text, never a CSS ::before"
def test_body_is_a_bounded_scroll_panel_of_code() -> None:
"""``.verbatim-body`` is the bounded scroll panel: ``max-height`` in the
``min(…, 70vh)`` form (PLAN §7.1 — the panel never eats the page),
``overflow: auto`` both axes, the mono family, ``white-space: pre`` and
``tab-size: 2`` (the file's own indentation survives), and
``overflow-wrap: normal`` — CODE scrolls horizontally and prose never
does (phase 116 locked A1, the phase-165 split), which also cancels the
``.bubble``'s inherited ``overflow-wrap: anywhere``. ``min-width: 0``
keeps the long-line escape hatch."""
body = _body(_BLOCK, ".verbatim-body")
assert re.search(r"max-height:\s*min\(\s*[0-9.]+rem,\s*70vh\s*\)", body), (
".verbatim-body must cap with max-height: min(<rem>, 70vh)"
)
for prop in (
"overflow: auto",
"min-width: 0",
"font-family: var(--mono)",
"white-space: pre",
"tab-size: 2",
"overflow-wrap: normal",
):
assert prop in body, f".verbatim-body must carry {prop}"
assert "white-space: pre-wrap" not in body, "code never wraps (locked A1)"
def test_line_numbers_are_not_selectable() -> None:
"""The gutter is ``user-select: none`` (with the ``-webkit-`` prefix) on
BOTH the container and its items, right-aligned in ``--ink-soft``: a
reader who selects the panel — or a Copy path that reads the selection —
never drags line numbers into the clipboard. ``list-style: none`` means
the digits are TEXT, so the rule behaves identically in every engine."""
container = _body(_BLOCK, ".verbatim-lines")
for prop in (
"user-select: none",
"-webkit-user-select: none",
"text-align: right",
"color: var(--ink-soft)",
"list-style: none",
):
assert prop in container, f".verbatim-lines must carry {prop}"
items = _body(_BLOCK, ".verbatim-lines li")
assert "user-select: none" in items, "the <li> line numbers must be unselectable too"
def test_pending_state_is_muted_and_static() -> None:
"""The streaming state (``.verbatim-pending`` + ``.verbatim-state``, the
markdown.js placeholder) is MUTED — ``--ink-soft`` on ``--surface``,
8.2:1 — and it never announces itself with motion: the pending line is a
``role="status"`` live region, so the announcement IS the feedback."""
pending = _body(_BLOCK, ".verbatim-pending")
state = _body(_BLOCK, ".verbatim-state")
assert "color: var(--ink-soft)" in state, "the pending line is muted (not an answer)"
assert "background: var(--surface)" in pending
assert "animation" not in pending and "transition" not in pending
assert "animation" not in state and "transition" not in state
def test_unresolved_state_carries_no_badge() -> None:
"""The honest failure state: the --err boundary + --err-bg canvas with
--ink-soft text (7.9:1, AA), and CSS-side enforcement that a ``VERBATIM``
badge NEVER appears on an unresolved panel whatever DOM reaches it — the
badge belongs only to a server-resolved block (locked A5)."""
body = _body(_BLOCK, ".verbatim-unresolved")
assert "var(--err" in body, "the unresolved state is the --err family"
assert "color: var(--ink-soft)" in body
hide = _body(_BLOCK, ".verbatim-unresolved .verbatim-badge")
assert "display: none" in hide, "an unresolved panel never badges"
def test_copy_controls_are_pills_with_a_ring_and_an_ellipsis_label() -> None:
"""``.code-block-copy`` / ``.verbatim-copy``: the house pill (mono text,
--line hairline, --surface fill, ink-soft label), the label is the clip
target (``text-overflow: ellipsis`` on its span, the control's
``aria-label`` naming the full path), the ``:focus-visible`` ring is the
app's 3px --brand ring, and the ``.copied`` rule exists to SUPPORT the
text swap (``Copy`` → ``Copied``) with colour — never colour alone."""
both = _body(_BLOCK, ".code-block-copy") + " " + _body(_BLOCK, ".verbatim-copy")
for prop in (
"border: 1px solid var(--line)",
"background: var(--surface)",
"color: var(--ink-soft)",
"font-family: var(--mono)",
"cursor: pointer",
"min-height:",
):
assert prop in both, f"the copy controls must carry {prop}"
label = _body(_BLOCK, ".code-block-copy > span")
assert "text-overflow: ellipsis" in label, "a clipped copy label shows the hint"
for selector in (".code-block-copy:focus-visible", ".verbatim-copy:focus-visible"):
ring = _body(_BLOCK, selector)
assert "outline: 3px solid var(--brand)" in ring, f"{selector} needs the house ring"
assert "outline-offset: 2px" in ring
for selector in (".code-block-copy.copied", ".verbatim-copy.copied"):
copied = _body(_BLOCK, selector)
assert "var(--ok" in copied, f"{selector} must use the --ok family as SUPPORT"
def test_plain_code_block_wrapper_is_not_a_verbatim_panel() -> None:
"""``.code-block`` is the plain wrapper task 07 puts around an ordinary
answer ``<pre>``: a Copy button belongs to it, the ``VERBATIM`` badge
does NOT — so the wrapper rule carries no --ok fill and no panel chrome
(locked A5: verbatim-and-badged OR plainly the model's, never a hybrid)."""
body = _body(_BLOCK, ".code-block")
assert "position: relative" in body
assert "min-width: 0" in body
assert "--ok" not in body, "an ordinary code block never wears the verbatim badge"
assert not _rule_bodies(_BLOCK, ".code-block .verbatim-badge"), (
"no badge rule may exist inside the plain wrapper"
)
# ---------------------------------------------------------------------------
# the palette-only invariant (phase 92 / the :root-drift guard)
# ---------------------------------------------------------------------------
def test_phase_182_block_uses_palette_variables_only() -> None:
"""NO colour literal anywhere in the phase-182 block: every colour comes
from a declared ``:root`` token (no hex, no ``rgb()``/``rgba()``, no
``hsl()``, no named colour). A literal would be a second source of truth
for the built-in palette (the ``test_theming.py`` drift guard parses the
built-ins out of this file) and a new hue would break the monochrome
invariant — this is also what keeps a code surface readable when the
owner switches the app to a light or a monochrome theme."""
code = _strip_comments(_BLOCK)
refs = re.findall(r"var\((--[a-z0-9-]+)\)", code)
assert refs, "the phase-182 block must reference palette vars"
for ref in set(refs):
assert ref in _ROOT, f"{ref} is not a :root palette var"
assert not re.search(r"#[0-9a-fA-F]{3,8}\b", code), "no hex color literal in the block"
for fn in ("rgb(", "rgba(", "hsl(", "hsla("):
assert fn not in code, f"no {fn} color literal in the block"
# named colours are the other way a second source of truth sneaks in
for name in (": white", ": black", "white;", "black;", "white }", "black }"):
assert name not in code, f"no named colour in the block ({name.strip()})"
def test_phase_182_block_restates_no_global_focus_rule_wrongly() -> None:
"""The restatement of the global ``:focus-visible`` ring uses the SAME
values as the app-wide rule (3px --brand, 2px offset) — a different
ring here would be a second, drifting source of focus truth."""
global_rule = _css()[: _css().find("\n::selection")]
assert re.search(r":focus-visible \{\s*outline: 3px solid var\(--brand\);\s*"
r"outline-offset: 2px;", global_rule), (
"the app-wide :focus-visible rule is the baseline this block copies"
)
# ---------------------------------------------------------------------------
# mobile + reduced motion
# ---------------------------------------------------------------------------
def test_mobile_block_keeps_a_44px_control() -> None:
"""The ≤640px block lifts BOTH Copy controls to the 44px touch floor
(PLAN §7: the phone tap target) — the desktop pill is smaller, so the
rule must live in the media block, not only in the base rule."""
mobile = _media_block(_CSS, "max-width: 640px")
assert "Phase 182" in mobile, "the mobile block must name the phase it serves"
hit = re.search(r"\.code-block-copy, \.verbatim-copy \{([^}]*)\}", mobile)
assert hit, "the mobile block must raise both Copy controls together"
assert "min-height: 44px" in hit.group(1)
def test_nothing_in_the_block_animates_without_a_reduced_motion_still() -> None:
"""``prefers-reduced-motion`` is honored by anything that animates. The
phase-182 block animates NOTHING (the pending state is a live region, so
the text swap is the feedback), which this pins as a GUARD: if a future
edit adds an ``animation``/``transition``, the same selectors must appear
in a ``prefers-reduced-motion: reduce`` block that stills them."""
code = _strip_comments(_BLOCK)
animated: list[str] = []
for match in re.finditer(r"([^{}]+)\{([^{}]*)\}", code):
body = match.group(2)
if re.search(r"(^|[;\s])(animation|transition)\s*:", body):
animated.append(" ".join(match.group(1).split()))
if not animated:
assert "prefers-reduced-motion" in _BLOCK, (
"a static block must say so, so the next reader does not add "
"a pulse 'to match the app'"
)
return
reduced = "\n".join(
m.group(0)
for m in re.finditer(r"@media \(prefers-reduced-motion: reduce\)[\s\S]*?\n\}", _CSS)
)
for selectors in animated:
for selector in selectors.split(","):
assert selector.strip() in reduced, (
f"{selector} animates but is never stilled under "
"prefers-reduced-motion"
)
def test_the_panel_never_repaints_the_existing_code_and_bubble_rules() -> None:
"""The neighbours are untouched: ``.bubble pre`` (the ordinary answer
code block) and ``.doc-raw`` (the viewer's scroller) keep their
declarations, so the phase adds a SURFACE and does not restyle every
code block in the app."""
css = _CSS
pre = re.search(r"\.bubble pre \{([^}]*)\}", css)
assert pre, ".bubble pre must still exist"
for prop in ("background: var(--surface)", "border: 1px solid var(--line)", "overflow-x: auto"):
assert prop in pre.group(1), f".bubble pre must keep {prop}"
raw = re.search(r"\.doc-raw \{([^}]*)\}", css)
assert raw, ".doc-raw must still exist"
assert "white-space: pre" in raw.group(1)
def test_block_is_one_contiguous_region_of_the_sheet() -> None:
"""The BEGIN/END markers wrap ONE region (a phase that appends a stray
``.verbatim-*`` rule elsewhere escapes every pin in this module)."""
css = _CSS
assert css.count(BEGIN) == 1 and css.count(END) == 1
assert css.index(BEGIN) < css.index(END)
between = css[css.index(BEGIN) : css.index(END)]
# every verbatim selector lives inside it
for selector in PANEL_SELECTORS:
assert re.search(r"(?m)^" + re.escape(selector) + r"[^\n]*\{", between), (
f"{selector} must be declared inside the labelled phase-182 region"
)
outside = _strip_comments(css[: css.index(BEGIN)] + css[css.index(END) :])
for selector in PANEL_SELECTORS:
assert not re.search(
r"(?m)^" + re.escape(selector) + r"[^\n]*\{", outside
), f"{selector} must not be restyled outside the phase-182 region"
def test_selector_names_match_the_dom_the_renderer_and_task_07_build() -> None:
"""The CSS class names are the contract between the renderer's pending
template (``markdown.js``) and the panel builder (``verbatim.js``, task
07): the pending template's classes must be exactly the ones styled here,
so the streaming placeholder is never an unstyled div."""
js = (STYLES_CSS.parent / "markdown.js").read_text(encoding="utf-8")
for cls in ("verbatim", "verbatim-pending", "verbatim-caption", "verbatim-state"):
assert cls in js, f"markdown.js must emit .{cls}"
assert _rule_bodies(_BLOCK, "." + cls), f"styles.css must style .{cls}"
assert "verbatim-unresolved" in _BLOCK, (
"the unresolved class is part of THIS contract (task 07 renders it)"
)
@pytest.mark.parametrize("pair", [("--ink", "--surface"), ("--ink-soft", "--surface")])
def test_block_pairs_the_documented_text_tokens(pair: tuple[str, str]) -> None:
"""The block pairs its text with the canvas the :root comment documents
(``--ink`` and ``--ink-soft`` on ``--surface``) rather than inventing a
third ink — the AA duty is inherited from the palette, not restated."""
code = _strip_comments(_BLOCK)
assert re.search(r"color: var\(" + re.escape(pair[0]) + r"\)", code), (
f"the block must use {pair[0]}"
)
assert re.search(r"background: var\(" + re.escape(pair[1]) + r"\)", code), (
f"the block must sit on {pair[1]}"
)