Files
hermes-webui/tests/test_issue1154_fenced_code_leak.py
T
2026-05-05 08:36:17 -07:00

192 lines
7.0 KiB
Python

"""
Regression tests for #1154 — fenced code block content leaking into
markdown passes and corrupting subsequent message rendering.
Root cause: fenced code blocks were rendered to <pre><code> early in
the pipeline, BEFORE list/heading/table regexes ran. Lines inside
code blocks that looked like markdown (e.g. `- removed`, `+ added`
in diff blocks) were matched by those regexes, injecting <ul>/<li>
HTML inside <pre>, breaking </pre> closure and corrupting layout.
Fix: fenced blocks are converted to <pre><code> HTML inside the stash
callback and kept as \\x00P tokens until AFTER all markdown passes.
"""
import re
import shutil
import pytest
REPO_ROOT = __import__("pathlib").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: '' }) };
const esc = s => String(s ?? '').replace(/[&<>\"']/g, c => (
{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
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):
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:
import subprocess
result = subprocess.run(
[NODE, driver_path, str(UI_JS_PATH)],
input=markdown, capture_output=True, text=True, timeout=10,
)
if result.returncode != 0:
raise RuntimeError(f"node driver failed: {result.stderr}")
return result.stdout
class TestFencedCodeBlockIsolation:
"""Content inside fenced code blocks must never be interpreted as markdown."""
def test_diff_block_plus_minus_lines_not_treated_as_list(self, driver_path):
html = _render(driver_path,
"before\n\n```diff\n- removed line\n+ added line\n```\nafter")
pre = _extract_pre(html)
assert pre is not None, "Expected a <pre> block"
assert "<ul>" not in pre, "List HTML must not appear inside <pre>"
assert "<li>" not in pre, "List items must not appear inside <pre>"
assert "- removed line" in pre
assert "+ added line" in pre
assert html.count("<pre") == html.count("</pre>"), \
"Every <pre> must have a matching </pre>"
def test_bash_block_asterisk_lines_not_treated_as_list(self, driver_path):
html = _render(driver_path,
"```bash\n* not a list item\n* another one\n```")
pre = _extract_pre(html)
assert pre is not None
assert "<ul>" not in pre
assert "<li>" not in pre
def test_markdown_block_heading_not_treated_as_h1(self, driver_path):
html = _render(driver_path,
"```markdown\n# Not a heading\n## Also not\n```")
pre = _extract_pre(html)
assert pre is not None
assert "<h1>" not in pre
assert "<h2>" not in pre
assert "# Not a heading" in pre
def test_markdown_block_bold_not_treated_as_strong(self, driver_path):
html = _render(driver_path, "```text\n**not bold**\n```")
pre = _extract_pre(html)
assert pre is not None
assert "<strong>" not in pre
assert "**not bold**" in pre
def test_content_after_code_block_renders_normally(self, driver_path):
html = _render(driver_path,
"```diff\n- old\n+ new\n```\n\n"
"# Real Heading\n\n- real list item\n\n**bold text**")
assert "<h1>Real Heading</h1>" in html
assert "<ul>" in html
assert "<strong>bold text</strong>" in html
pre = _extract_pre(html)
assert "<ul>" not in pre
def test_no_escaped_pre_closing_tag(self, driver_path):
html = _render(driver_path,
"```diff\n- old line\n+ new line\n```\n\nAfter the block")
assert "&lt;/pre&gt;" not in html, \
"Escaped </pre> indicates broken HTML nesting"
def test_code_block_without_language(self, driver_path):
html = _render(driver_path,
"```\n- item\n+ item\n```\n\n- real list")
pre = _extract_pre(html)
assert pre is not None
assert "<ul>" not in pre
assert html.count("<pre") == html.count("</pre>")
def test_inline_backtick_still_works(self, driver_path):
html = _render(driver_path, "Some **`code`** here")
assert "<strong><code>code</code></strong>" in html
def test_inline_backtick_inside_bold(self, driver_path):
html = _render(driver_path,
"Text with `inline code` and **bold**")
assert "<code>inline code</code>" in html
assert "<strong>bold</strong>" in html
def test_large_diff_output_no_corruption(self, driver_path):
diff_lines = "\n".join(
f"- old_line_{i}" if i % 2 == 0 else f"+ new_line_{i}"
for i in range(50)
)
html = _render(driver_path,
f"```diff\n{diff_lines}\n```\n\nSummary after diff")
pre = _extract_pre(html)
assert pre is not None
assert "<ul>" not in pre
assert "<li>" not in pre
assert html.count("<pre") == html.count("</pre>")
assert "Summary after diff" in html
def test_mixed_fenced_and_inline_code(self, driver_path):
html = _render(driver_path,
"Use `rm -rf` to delete:\n\n"
"```bash\nrm -rf /tmp/stuff\n```\n\nDone.")
assert "<code>rm -rf</code>" in html
pre = _extract_pre(html)
assert pre is not None
assert "rm -rf /tmp/stuff" in pre
class TestFencedBlockPreHeaderPreserved:
"""Pre-header div (language label) must still be generated."""
def test_language_header_present(self, driver_path):
html = _render(driver_path,
"```python\ndef hello():\n pass\n```")
assert '<div class="pre-header">python</div>' in html
def test_no_language_no_header(self, driver_path):
html = _render(driver_path, "```\nsome code\n```")
assert "pre-header" not in html
def test_language_class_on_code_tag(self, driver_path):
html = _render(driver_path,
"```javascript\nconsole.log('hi')\n```")
assert 'class="language-javascript"' in html
def _extract_pre(html):
m = re.search(r"<pre[^>]*>[\s\S]*?</pre>", html)
return m.group(0) if m else None