"""Behavioural tests that drive the ACTUAL renderMd() in static/ui.js via node.
The Python mirrors in test_blockquote_rendering.py and
test_renderer_comprehensive.py validate intent, but they can drift from the
JS. Twice now (PR #1073 commit 94d63d0 — phantom
; PR #1073 commit
04e7b53 — leading-space-in-blockquote prefix-strip regex) the Python mirror
was correct while the JS was not, so the static-mirror tests passed even
though the live UI was broken.
This file closes that gap by spawning ``node`` on the real ui.js and
asserting the rendered HTML for the most common LLM-output shapes.
Add a case here whenever the renderer fix targets a class of input the
Python mirror cannot exercise faithfully.
"""
import os
import re
import shutil
import subprocess
import sys
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).parent.parent.resolve()
UI_JS_PATH = REPO_ROOT / "static" / "ui.js"
NODE = shutil.which("node")
pytestmark = pytest.mark.skipif(NODE is None, reason="node not on PATH")
_DRIVER_SRC = r"""
const fs = require('fs');
const src = fs.readFileSync(process.argv[2], 'utf8');
global.window = {};
global.document = { createElement: () => ({ innerHTML: '', textContent: '' }), baseURI: 'http://localhost/app/' };
function _sessionUrlForSid(sid) { return '/app/session/' + encodeURIComponent(String(sid || '')); }
const esc = s => String(s ?? '').replace(/[&<>"']/g, c => (
{'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
const _IMAGE_EXTS=/\.(png|jpg|jpeg|gif|webp|bmp|ico|avif)$/i;
const _SVG_EXTS=/\.svg$/i;
const _AUDIO_EXTS=/\.(mp3|ogg|wav|m4a|aac|flac|wma|opus|webm)$/i;
const _VIDEO_EXTS=/\.(mp4|webm|mkv|mov|avi|ogv|m4v)$/i;
// Minimal stand-in for ui.js' _inlineMediaHtmlForRef used when the driver
// extracts only renderMd(). Mirrors the live UI for the cases the existing
// tests assert against (https image, bare file:// image). The full renderer
// (ui.js) is preferred on the real page — these nodes only cover what is
// reachable from a standalone renderMd() invocation.
function _inlineMediaHtmlForRef(ref){
const r = String(ref || '');
if (/^https?:\/\//.test(r)) return ``;
if (/^file:\/\//.test(r)){
const m = r.replace(/^file:\/\//i, '');
return `
`;
}
return `
`;
}
function extractFunc(name) {
const re = new RegExp('function\\s+' + name + '\\s*\\(');
const start = src.search(re);
if (start < 0) throw new Error(name + ' not found');
let i = src.indexOf('{', start);
let depth = 1; i++;
while (depth > 0 && i < src.length) {
if (src[i] === '{') depth++;
else if (src[i] === '}') depth--;
i++;
}
return src.slice(start, i);
}
eval(extractFunc('_matchBacktickFenceLine'));
eval(extractFunc('_isBacktickFenceClose'));
eval(extractFunc('renderMd'));
let buf = '';
process.stdin.on('data', c => { buf += c; });
process.stdin.on('end', () => { process.stdout.write(renderMd(buf)); });
"""
@pytest.fixture(scope="module")
def driver_path(tmp_path_factory):
"""Write the node driver to a tmp file (works around `node -e` arg quirks)."""
p = tmp_path_factory.mktemp("renderer_driver") / "driver.js"
p.write_text(_DRIVER_SRC, encoding="utf-8")
return str(p)
def _render(driver_path, markdown: str) -> str:
"""Run renderMd against the actual ui.js and return the rendered HTML."""
result = subprocess.run(
[NODE, driver_path, str(UI_JS_PATH)],
input=markdown,
capture_output=True,
text=True,
timeout=30,
)
if result.returncode != 0:
raise RuntimeError(f"node driver failed: {result.stderr}")
return result.stdout
class TestSessionInternalLinks:
"""Drive renderMd() so session:// hardening covers the real sanitizer path."""
def test_session_scheme_renders_same_origin_internal_anchor(self, driver_path):
out = _render(driver_path, "[Open session](session://abc123)")
assert 'class="session-link"' in out
assert 'href="/app/session/abc123"' in out
assert 'target="_blank"' not in out
assert 'rel="noopener"' not in out
def test_hostile_session_scheme_collapses_to_encoded_session_path(self, driver_path):
out = _render(driver_path, "[bad](session://javascript:alert(1))")
assert 'class="session-link"' in out
assert 'href="/app/session/javascript%3Aalert(1"' in out
assert 'href="javascript:' not in out
assert 'target="_blank"' not in out
def test_unrelated_same_origin_session_segment_is_not_allowlisted(self, driver_path):
out = _render(driver_path, 'bad')
assert 'href="/anything/foo/session/abc"' not in out
assert 'bad' in out
# ─────────────────────────────────────────────────────────────────────────────
# Blockquote prefix strip — the bug commit 04e7b53 introduced was a one-char
# regex regression where `^>[\t]?` (only tab) replaced `^>[ \t]?` (space or
# tab), producing leading-space artifacts and breaking lists-in-quotes
# because the list-detection regex `^( )?[-*+]` couldn't match the
# space-prefixed lines. These tests exercise the actual JS so the regex
# can't silently regress to tab-only again.
# ─────────────────────────────────────────────────────────────────────────────
class TestBlockquotePrefixStrip:
"""Drive the actual renderMd to confirm `> ` is fully stripped."""
def test_single_line_blockquote_no_leading_space(self, driver_path):
out = _render(driver_path, "> Hello world").strip()
# New shape: recursive renderMd wraps content in
(CommonMark-correct). assert "
" in out, ( f"`> Hello world` must render asHello world
" f"with no leading space. Got: {out!r}." ) def test_multiline_blockquote_no_leading_space(self, driver_path): out = _render(driver_path, "> Line one\n> Line two").strip() # New shape: single paragraph withHello world
" in out, ( f"Multi-line blockquote must strip the space after each `>` and " f"render as a single paragraph. Got: {out!r}" ) # Belt-and-braces: there must be no space-after-newline-in-content assert "\n " not in out.replace("", ""), ( f"Inner content of blockquote should not contain leading-space " f"lines. Got: {out!r}" ) def test_list_inside_blockquote_renders_as_ul(self, driver_path): """The PR explicitly added 'lists inside blockquotes' as a feature. With the prefix-strip bug, the list-detection regex can't match the space-prefixed lines, so the list never renders. This pins it.""" out = _render(driver_path, "> Steps:\n> - one\n> - two") assert "Line one
Line two
| field | " in out assert "expr | " in out # The code cell should contain both pipes assert "") == 2, ( f"Expected exactly 2 | cells, got {out.count(' | ')}: {out!r}"
)
def test_strikethrough_outside_quote(self, driver_path):
out = _render(driver_path, "This was ~~outdated~~ but is now fine.")
assert "" in out and " |
|---|