feat(chat): render markdown tables in answers, viewer, and thinking
GFM pipe tables in the shared renderer (TODO.md L6): a table-protection
pass in frontend/assets/markdown.js (fences -> tables -> escape order)
pulls each header+separator+body block out as a placeholder, renders
cells escape-first with the same inline transforms, and reinserts a
semantic <table class="md-table"> inside a horizontal-overflow
.md-table-wrap — so a pipe table in a chat answer, the document
viewer/modal, and the thinking block all render the same semantic
table. Fences win over tables; lone pipes stay text.
- styles.css: .md-table palette rules (PLAN §7.2 tokens, no motion);
min-width: max-content so a WIDE table keeps its natural width and
the wrapper is the real scroller (width:100% alone wrapped the wide
table's cells — proven by the new E2E).
- mock_llm.py: TABLE_TRIGGER ("show me a table") -> byte-stable
TABLE_ANSWER (3-column table, <img onerror> XSS probe line, wide
5-column table), checked before DEFLECT_MODE like SUMMARY_MODE.
- tests/fixtures/docs/homelab/tables.md: 3x3 pipe table + pipe-heavy
fenced block (viewer/fence subject); the shared fixture set grows
8 -> 9 docs, so every suite pinning the count (added/formats/
stat-docs/EXPECTED_ROWS) is updated accordingly.
- tests/e2e/test_markdown_tables.py (new, story suite): chat table
shape + non-deflection, wide-table wrapper scroll (no page
overflow), XSS probe inert, viewer modal table, fence-not-a-table,
lone pipe stays text.
- tests/e2e/test_agent_document_tools.py: fix a pre-existing flake —
the "Calling tool…" label window is ~0.4 s at the mock's 0.1 s
tool-frame pacing, and a polling expect could stride over it
(failed 3 of 5 runs on the committed baseline). The pre-submit
MutationObserver record is the deterministic source of truth; the
racy to_have_text gate is gone.
uv run pytest: 738 passed, app/ coverage 99% (TOTAL unchanged);
ruff + pyright clean; story E2E 6/6 in isolation; regression E2E
suites (chat_rag, document_viewer, document_summaries, smoke) green.
This commit is contained in:
@@ -40,6 +40,7 @@ EXPECTED_ROWS = (
|
||||
"homelab/networking/static-dns.json",
|
||||
"homelab/scripts/uptime_probe.py",
|
||||
"homelab/ssh/ssh_aliases.txt",
|
||||
"homelab/tables.md", # phase 44: the markdown-tables fixture
|
||||
)
|
||||
|
||||
|
||||
@@ -85,13 +86,14 @@ def test_sources_page_lists_indexed_docs(
|
||||
page: Page, app_url: str, mock_llm: int, db_ready: None
|
||||
) -> None:
|
||||
summary = _reset_db(mock_llm, seed=True)
|
||||
# Eight A9-format files are imported; .hidden/junk.md is out of scope
|
||||
# (A9 revised — hidden path components are never walked).
|
||||
assert summary is not None and summary.added == 8
|
||||
assert summary.formats == {"md": 4, "yaml": 1, "json": 1, "py": 1, "txt": 1}
|
||||
# Nine A9-format files are imported (phase 44 added homelab/tables.md);
|
||||
# .hidden/junk.md is out of scope (A9 revised — hidden path components
|
||||
# are never walked).
|
||||
assert summary is not None and summary.added == 9
|
||||
assert summary.formats == {"md": 5, "yaml": 1, "json": 1, "py": 1, "txt": 1}
|
||||
|
||||
login(page, app_url) # phase 16: the catalog is admin-only
|
||||
expect(page.locator("#stat-docs")).to_have_text("8")
|
||||
expect(page.locator("#stat-docs")).to_have_text("9")
|
||||
# Phase 30: non-markdown fixtures each gained one ``is_summary`` chunk,
|
||||
# so the Sources total is content chunks + summary chunks.
|
||||
expect(page.locator("#stat-chunks")).to_have_text(
|
||||
|
||||
Reference in New Issue
Block a user