Files
brain-of-reese/tests/unit/test_markdown_tables.py
T
ducoterra 58e9d94cff
Build and Push Containers / build-and-push-app (push) Successful in 4m3s
Build and Push Containers / build-and-push-db (push) Successful in 18s
phase: 100_page_width_consistency
**Phase 100 — final verification pass: all green.**

- Verified the shipped CSS contract directly: `--chat-column: 72rem` in `:root`; 0 literal `max-width: 46rem`; no `@media (min-width: 1500px)` block; exactly 4 token-capped reading columns; tuning/theme/doc-edit shells cap-free, structurally `.sources-shell`; `mock_llm.py` diff is comment-only.
- Defect found & fixed (phase-93 suite): `test_theme_semantic_completion.py::test_reset_removes_tag_byte_identical` raced theme.js's post-PUT refetch — it asserted the `#bor-theme` tag was gone right after the result line, but tag removal lands in the reconcile after the re-fetch (failed ~1 in 5 runs after `test_chat_rag`, reproduced). Fixed with an auto-waiting `expect(...).to_have_count(0)` (settled-state wait, the file's existing house pattern). 6/6 clean on the repro loop after.
- Tests/lint/coverage: `uv run pytest --cov=app --cov-report=term-missing` → 2052 passed, **99%** on `app/`; `uv run ruff check .` + `uv run pyright` → 0 errors.
- E2E in isolation (all passed): `test_wide_desktop_column.py` 3 (the phase suite — chat==tuning==theme==RAG ±4px at 1280 & 1920, ≈1152px; shared ≈1152px; standalone doc ≈1112px; modal unchanged ≈1100px; 360px overflow-free), `test_ui_customization` 4, `test_admin_theme_tab` 5, `test_document_viewer` 7, `test_save_share_ux` 5, `test_sticky_navbar` 3, `test_markdown_tables` 6, `test_responsive_polish` 7, `test_chat_rag` 3, `test_theme_semantic_completion` 8.
- Completion criteria: ① measured 72rem everywhere (≥~1200px, ±4px) + full-width below + 360px clean — **PASS** (E2E); ② zero 46rem rules / no 1500px block / four token selectors — **PASS** (grep + unit pins); ③ B4 byte-identical no-op + mobile squeeze — **PASS** (theme + responsive suites green); ④ full suite / coverage / lint — **PASS**; ⑤ atomic commit — left to the harness per executor protocol (all changes in the working tree, uncommitted).
- Deviations: none from phase decisions; only change in this pass is the race fix above (test-only, behavior unchanged).
- Next pending phase: `98_sync_summary_visibility` (numeric order in `todo/`; also pending: 99, 101–105).
2026-09-12 13:37:19 -04:00

352 lines
16 KiB
Python

"""Unit: the shared renderer's GFM pipe-table pass (phase 44, task 01).
Browser behavior is E2E-covered by ``tests/e2e/test_markdown_tables.py``
(task 03); here we pin the source-level contract — the pass ordering
(fences → tables → escape), the escape-first cell pipeline, the exact
output markers, the placeholder restore, and the ``.md-table`` /
``.md-table-wrap`` CSS rules — so a silent regression is caught without
a browser (the established frontend pattern). A small node-driven
behavioral check (skipped when node is unavailable, like the phase-10
renderer check in ``test_document_viewer.py``) proves the four story
acceptance shapes: table, XSS cell, fence-wins, non-tables stay text.
"""
from __future__ import annotations
import json
import re
import shutil
import subprocess
from pathlib import Path
import pytest
FRONTEND = Path(__file__).resolve().parents[2] / "frontend"
ASSETS = FRONTEND / "assets"
MARKDOWN_JS = ASSETS / "markdown.js"
STYLES_CSS = ASSETS / "styles.css"
HAVE_NODE = shutil.which("node") is not None
def _text(path: Path) -> str:
assert path.is_file(), f"missing frontend file: {path}"
return path.read_text(encoding="utf-8")
def _run_node(script: str) -> str:
proc = subprocess.run(["node", "-e", script], capture_output=True, text=True, timeout=60)
assert proc.returncode == 0, f"node failed: {proc.stderr}"
return proc.stdout
# ---------- markdown.js: the table-protection pass ----------
def test_table_pass_runs_after_fences_and_before_escape() -> None:
"""The renderer's pass order is fences → tables → escape (story AC3:
a |-heavy fenced block is protected before the table pass ever sees
it, so fences win). Pinned by the placeholder-emission order in the
source: the \\u0000CODEn\\u0000 push (step 1) precedes the
\\u0000TABLEn\\u0000 push (step 1b), which precedes step 2's
escape of the whole text."""
js = _text(MARKDOWN_JS)
fence_emit = r"\u0000CODE${codeBlocks.length - 1}\u0000"
table_emit = r"\u0000TABLE${tables.length - 1}\u0000"
assert fence_emit in js, "the fence pass keeps its CODE placeholder"
assert table_emit in js, "the table pass emits a TABLE placeholder"
assert js.find(fence_emit) < js.find(table_emit), (
"the fence pass must run BEFORE the table pass (fences win)"
)
assert "escapeHtml(text)" in js, "step 2 still escapes the whole text"
assert js.find(table_emit) < js.find("escapeHtml(text)"), (
"the table pass must run BEFORE the escape pass (cells are escaped per-cell)"
)
def test_table_detection_requires_pipe_header_then_separator() -> None:
"""Detection (per the phase spec): a table block starts at a line
containing '|' whose NEXT line is a separator — >=1 pipe-separated
cells, each ^\\s*:?-+:?\\s*$ (leading/trailing pipes allowed) — and
extends over every following line that still contains '|'; the
maximal run is one table. Everything else is left untouched."""
js = _text(MARKDOWN_JS)
# the separator cell rule (alignment colons accepted, then ignored)
assert r"^\s*:?-+:?\s*$" in js, "the separator cell rule ^\\s*:?-+:?\\s*$ is missing"
# a table starts at a '|' line whose NEXT line is the separator
assert 'line.includes("|")' in js, "the header line must contain '|'"
assert "isSeparator(tableLines[pos + 1])" in js, (
"the NEXT line after the '|' line must be the separator"
)
# the body extends over every following line that still contains '|'
assert 'tableLines[end].includes("|")' in js, (
"the table body must extend over every following line containing '|'"
)
# zero body rows is valid — body = the lines after header + separator
assert "slice(2)" in js, "body rows are the lines after header + separator"
def test_cells_are_escaped_then_inline_transformed() -> None:
"""Escape-first is the renderer invariant: each cell (header AND
body) goes through escapeHtml() BEFORE the inline transforms, and
the inline chain is the SAME factored helper the whole-text path
uses — so cells keep `code` / **bold** / *em* exactly like prose."""
js = _text(MARKDOWN_JS)
assert js.count("inline(escapeHtml(c))") >= 2, (
"header and body cells must both run inline(escapeHtml(cell))"
)
# the whole-text path runs the same helper over the escaped text
assert "text = inline(escapeHtml(text));" in js, (
"step 2 must reuse the SAME factored inline chain"
)
# the helper carries the exact original .replace chain
helper_start = js.find("const inline = (s) =>")
assert helper_start != -1, "the inline chain must be factored into a local helper"
helper = js[helper_start : js.find(";", js.find("gm", helper_start)) + 1]
for marker in (
r"/`([^`\n]+)`/g",
r"\*\*([^*]+)\*\*",
r"(^|[\s(])\*([^*\n]+)\*",
r"/^### (.*)$/gm",
r"/^## (.*)$/gm",
r"/^# (.*)$/gm",
r"/^\s*[-*] (.*)$/gm",
r"(<li>[\s\S]*?<\/li>)(?!\s*<li>)",
r"/^\d+\. (.*)$/gm",
):
assert marker in helper, f"the factored helper lost the {marker!r} step"
assert js.count("replace(/`([^`\\n]+)`/g") == 1, (
"the code-span replace must live ONLY in the factored helper"
)
def test_table_output_markers_and_wrap() -> None:
"""The exact output shape the story pins: a semantic table inside a
horizontal-overflow wrapper — <div class=\"md-table-wrap\"> /
<table class=\"md-table\"> / <thead> with th scope=\"col\" headers /
<tbody> body rows (a header-only table omits the empty <tbody>)."""
js = _text(MARKDOWN_JS)
assert '<div class="md-table-wrap">' in js
assert '<table class="md-table">' in js
assert "<thead><tr>" in js
assert '<th scope="col">' in js, "headers must carry scope=\"col\""
assert "</thead>" in js and "</table>" in js and "</div>" in js
assert "<tbody>" in js and "</tbody>" in js
assert "<td>" in js and "</tr>" in js
# header-only tables: the tbody is conditional on having body rows
assert re.search(r"body\s*\?", js), "a header-only table must skip the <tbody>"
# the assembly order inside the push template: wrap, table, thead,
# (tbody), close — pinned on the quoted source literals (the header
# comment mentions the shapes out of order on purpose)
wrap_lit = "'<div class=\"md-table-wrap\">\\n'"
table_lit = "'<table class=\"md-table\">\\n'"
assert js.find(wrap_lit) != -1, "the template must open the .md-table-wrap div"
assert js.find(table_lit) != -1, "the template must open the table inside the wrap"
assert js.find(wrap_lit) < js.find(table_lit), "the wrap opens before the table"
assert '</table>\\n</div>' in js, "the template closes the table, then the wrap"
def test_header_and_body_rows_pad_and_truncate_to_header_width() -> None:
"""Defensive ragged rows: short rows are padded with empty <td>s,
long rows are truncated to the header width (mock and real answers
are well-formed; this only guards the parser)."""
js = _text(MARKDOWN_JS)
assert re.search(r"cells\.push\(\"\"\)", js), "short rows must be padded"
assert re.search(r"slice\(0,\s*width\)", js), "long rows must be truncated"
assert "const width = header.length;" in js, "the header row sets the column count"
def test_placeholder_restored_alongside_code_blocks() -> None:
"""Step 4 is the ONLY place placeholders re-expand — the TABLE
placeholders are restored there, next to the CODE ones, so the
final table HTML never re-enters the escape/paragraph passes."""
js = _text(MARKDOWN_JS)
fn = js.find("function renderMarkdown")
assert fn != -1
body = js[fn:]
# step 4 is the ONLY restore point and it restores BOTH placeholder
# kinds, indexing their arrays
assert r"\u0000TABLE(\d+)\u0000/g" in body, (
"step 4 must restore the \\u0000TABLEn\\u0000 placeholders"
)
assert "tables[Number(i)]" in body, "restoration must index the tables array"
assert r"\u0000CODE(\d+)\u0000/g" in body, "the CODE restore stays in step 4"
# the TABLE restore is AFTER the paragraph pass — the restored final
# HTML never re-enters it
assert body.find(r"\u0000TABLE(\d+)\u0000/g") > body.find("split(/\\n{2,}/)"), (
"placeholders are restored AFTER the paragraph pass"
)
def test_alignment_colons_are_parsed_but_ignored() -> None:
"""Owner decision (2026-08-27): the separator's alignment colons
match the cell rule (so ':---:' is a valid separator) but are never
turned into CSS alignment — the CSS pins every cell to left."""
js = _text(MARKDOWN_JS)
assert r"^\s*:?-+:?\s*$" in js, "colons must be accepted in separators"
css = _text(STYLES_CSS)
th_td = re.search(r"\.md-table th, .md-table td \{([^}]*)\}", css)
assert th_td, "styles.css must style .md-table th/td"
assert "text-align: left" in th_td.group(1), "all cells render left-aligned"
def test_header_comment_notes_the_table_pass() -> None:
"""The file's header comment records the extension (2026-08-27,
TODO.md L6) — the ~60-line no-CDN renderer now also does tables."""
header = _text(MARKDOWN_JS).split("*/", 1)[0]
assert "Phase 44" in header
assert "2026-08-27" in header
assert "TODO.md L6" in header
assert re.search(r"table", header, re.I), "the header comment must mention tables"
assert "no CDN" in header, "the no-CDN contract stays stated (A11)"
# ---------- styles.css: the .md-table / .md-table-wrap rules ----------
def test_table_wrap_is_the_horizontal_scroller() -> None:
""".md-table-wrap { overflow-x: auto } — the wrapper (not the
table) is the scroller, so a wide table scrolls inside the bubble
instead of breaking the 72rem column (story AC5 — the width
contract is now the 72rem container, phase 100)."""
css = _text(STYLES_CSS)
block = re.search(r"\.md-table-wrap\s*\{([^}]*)\}", css)
assert block, "styles.css must define .md-table-wrap"
assert re.search(r"overflow-x:\s*auto", block.group(1)), (
"the wrapper must scroll horizontally"
)
def test_table_rule_and_cell_borders_use_the_palette() -> None:
""".md-table is border-collapse, full-width (min-width: max-content
keeps a wide table at its natural width so the wrap scrolls — phase
44 task 03), 0.9rem; th/td carry the --line 1px borders, 0.4rem
0.6rem padding, left alignment, top vertical alignment; the thead is
tinted from the PLAIN surface family (PLAN §7.2: --ink on --surface
is 14.5:1) — never the brand tokens."""
css = _text(STYLES_CSS)
table = re.search(r"(?<![-\w])\.md-table\s*\{([^}]*)\}", css)
assert table, "styles.css must define .md-table"
body = table.group(1)
assert re.search(r"border-collapse:\s*collapse", body)
assert re.search(r"width:\s*100%", body), "narrow tables stretch to the column"
assert re.search(r"min-width:\s*max-content", body), (
"wide tables must keep their natural width so .md-table-wrap scrolls"
)
assert re.search(r"font-size:\s*0\.9rem", body)
cells = re.search(r"\.md-table th, .md-table td\s*\{([^}]*)\}", css)
assert cells, "styles.css must style .md-table th and td"
cell_body = cells.group(1)
assert re.search(r"border:\s*1px solid var\(--line\)", cell_body), (
"cells must carry the quiet --line hairline borders"
)
assert re.search(r"padding:\s*0\.4rem 0\.6rem", cell_body)
assert re.search(r"text-align:\s*left", cell_body)
assert re.search(r"vertical-align:\s*top", cell_body)
thead = re.search(r"\.md-table thead th\s*\{([^}]*)\}", css)
assert thead, "styles.css must tint the thead cells"
thead_body = thead.group(1)
assert "var(--surface)" in thead_body, "thead tint is the plain surface family"
assert "var(--ink)" in thead_body, "--ink on --surface is 14.5:1 (>= 4.5:1)"
assert "--brand" not in thead_body, "never the brand tokens on the thead"
def test_table_rules_carry_no_motion() -> None:
"""Reduced-motion consistency: the table rules are static content —
no animation/transition for prefers-reduced-motion to still (the
phase-44 comment states this, like the phase-08/22/36 static
components)."""
css = _text(STYLES_CSS)
rules = (
r"\.md-table-wrap",
r"(?<![-\w])\.md-table",
r"\.md-table th, .md-table td",
r"\.md-table thead th",
)
for name in rules:
block = re.search(name + r"\s*\{([^}]*)\}", css)
assert block, f"styles.css must define {name}"
assert "animation" not in block.group(1), f"{name} must not animate"
assert "transition" not in block.group(1), f"{name} must not transition"
assert re.search(r"nothing for\s+prefers-reduced-motion to still", css), (
"the table block comment must note there is nothing to still"
)
def test_table_css_lives_with_the_bubble_markdown_rules() -> None:
"""Placement (task 01 step 2): the table rules sit with the
markdown/content styling — right after the .bubble code rules,
before the .msg.user block — and are unscoped so the shared
renderer's tables in the thinking scratchpad and the document
viewer (.doc-md) get the same treatment."""
css = _text(STYLES_CSS)
assert css.find('.bubble pre code {') < css.find(".md-table-wrap"), (
"the table rules follow the bubble markdown content rules"
)
assert css.find(".md-table thead th") < css.find(".msg.user {"), (
"the table rules stay in the Messages section"
)
# no .bubble-scoped table variant: one rule set serves all consumers
assert re.search(r"\.bubble\s+\.md-table", css) is None, (
"the table rules must be unscoped (shared renderer consumers)"
)
# ---------- behavior (node): the story's acceptance shapes ----------
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_table_renders_semantic_xss_safe_and_fences_win() -> None:
"""The four story shapes executed under node: (1) a pipe table with
leading/trailing pipes + inline markdown in cells renders the
<div class=\"md-table-wrap\"><table class=\"md-table\"> shape with
<th scope=\"col\"> headers and padded ragged rows, and NO raw
separator line survives; (2) a cell with an HTML tag is escaped;
(3) a |-heavy fenced block stays <pre><code>; (4) a lone pipe in
prose, a separator without a header, and a 1-line 'table' stay
text."""
js = _text(MARKDOWN_JS)
cases = {
"table": (
"Here:\n\n| Service | Port |\n|:---|---:|\n| **api** | 8000 |\n| web |",
[
'<div class="md-table-wrap">',
'<table class="md-table">',
"<thead><tr><th scope=\"col\">Service</th><th scope=\"col\">Port</th></tr></thead>",
"<tbody><tr><td><strong>api</strong></td><td>8000</td></tr>"
"<tr><td>web</td><td></td></tr></tbody>",
"</table>\n</div>",
],
["|:---|---:|", "| **api** |"],
),
"xss": (
"| c |\n|---|\n| <img src=x onerror=alert(1)> & co |",
["&lt;img src=x onerror=alert(1)&gt; &amp; co"],
["<img"],
),
"fence": (
"```\n| a | b |\n|---|---|\n| 1 | 2 |\n```",
["<pre><code>| a | b |"],
["<table"],
),
"not_tables": (
"a | b prose\n\n---\n\n| only a line\n\nend |---|",
["<p>a | b prose</p>", "<p>---</p>", "<p>| only a line</p>", "<p>end |---|</p>"],
["<table"],
),
}
driver = (
js
+ "\nconst out = {}; "
+ "".join(
f"out.{name} = renderMarkdown({value!r}); " for name, (value, _, _) in cases.items()
)
+ "console.log(JSON.stringify(out));"
)
results = json.loads(_run_node(driver))
for name, (_, must_contain, must_not) in cases.items():
html = results[name]
for marker in must_contain:
assert marker in html, f"{name}: missing {marker!r} in {html!r}"
for marker in must_not:
assert marker not in html, f"{name}: unexpected {marker!r} in {html!r}"