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:
@@ -87,9 +87,10 @@ def test_multi_format_import_hidden_doc_excluded(
|
||||
"""A9 (revised): all seven formats import; hidden (dot) paths never do."""
|
||||
summary = _reset_db(mock_llm, seed=True)
|
||||
assert summary is not None
|
||||
# Eight A9-format fixture files; .hidden/junk.md must never be walked.
|
||||
assert summary.added == 8
|
||||
assert summary.formats == {"md": 4, "yaml": 1, "json": 1, "py": 1, "txt": 1}
|
||||
# Nine A9-format fixture files (phase 44 added homelab/tables.md);
|
||||
# .hidden/junk.md must never be walked.
|
||||
assert summary.added == 9
|
||||
assert summary.formats == {"md": 5, "yaml": 1, "json": 1, "py": 1, "txt": 1}
|
||||
|
||||
# Phase 16: the catalog is admin-only — perform the real form login,
|
||||
# then call the API with the signed cookie the browser now holds.
|
||||
@@ -102,7 +103,7 @@ def test_multi_format_import_hidden_doc_excluded(
|
||||
r = httpx.get(f"{app_url}/api/docs", timeout=10, cookies=cookies)
|
||||
assert r.status_code == 200
|
||||
docs = r.json()["documents"]
|
||||
assert len(docs) == 8
|
||||
assert len(docs) == 9
|
||||
assert all(".hidden" not in d["path"] for d in docs)
|
||||
assert {d["path"] for d in docs} >= {
|
||||
"homelab/container_gitlab/gitlab.md",
|
||||
@@ -113,7 +114,7 @@ def test_multi_format_import_hidden_doc_excluded(
|
||||
}
|
||||
|
||||
# The Sources page (we're already on it, signed in) reflects the set.
|
||||
expect(page.locator("#stat-docs")).to_have_text("8")
|
||||
expect(page.locator("#stat-docs")).to_have_text("9")
|
||||
expect(page.locator("#docs-tbody tr", has_text=".hidden")).to_have_count(0)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user