""" Sprint 16 Tests: safe HTML rendering in renderMd(), active session styling, session sidebar polish (SVG icons, dropdown actions). """ import html as _html import pathlib import re import urllib.request BASE = "http://127.0.0.1:8788" REPO_ROOT = pathlib.Path(__file__).parent.parent # ── Helpers ────────────────────────────────────────────────────────────────── def get_text(path): with urllib.request.urlopen(BASE + path, timeout=10) as r: return r.read().decode("utf-8"), r.status def esc(s): """Mirror of esc() in ui.js — HTML-escapes a string.""" return _html.escape(str(s), quote=True) SAFE_TAGS = re.compile( r"^<\/?(strong|em|code|pre|h[1-6]|ul|ol|li|table|thead|tbody|tr|th|td" r"|hr|blockquote|p|br|a|div)([\s>]|$)", re.I, ) SAFE_INLINE = re.compile(r"^<\/?(strong|em|code|a)([\s>]|$)", re.I) def inline_md(t): """Mirror of inlineMd() in ui.js — for use inside list items / blockquotes.""" t = re.sub(r"\*\*\*(.+?)\*\*\*", lambda m: "" + esc(m.group(1)) + "", t) t = re.sub(r"\*\*(.+?)\*\*", lambda m: "" + esc(m.group(1)) + "", t) t = re.sub(r"\*([^*\n]+)\*", lambda m: "" + esc(m.group(1)) + "", t) t = re.sub(r"`([^`\n]+)`", lambda m: "" + esc(m.group(1)) + "", t) t = re.sub( r"\[([^\]]+)\]\((https?://[^\)]+)\)", lambda m: f'{esc(m.group(1))}', t, ) t = re.sub(r"]*>", lambda m: m.group() if SAFE_INLINE.match(m.group()) else esc(m.group()), t) return t def render_md(raw): """ Python mirror of renderMd() in static/ui.js. Kept in sync with the JS implementation so tests catch regressions if the JS logic drifts from the documented behaviour. """ s = raw or "" # Pre-pass: stash code blocks/spans, convert safe HTML → markdown equivalents fence_stash = [] def stash(m): fence_stash.append(m.group()) return "\x00F" + str(len(fence_stash) - 1) + "\x00" s = re.sub(r"(```[\s\S]*?```|`[^`\n]+`)", stash, s) s = re.sub(r"([\s\S]*?)", lambda m: "**" + m.group(1) + "**", s, flags=re.I) s = re.sub(r"([\s\S]*?)", lambda m: "**" + m.group(1) + "**", s, flags=re.I) s = re.sub(r"([\s\S]*?)", lambda m: "*" + m.group(1) + "*", s, flags=re.I) s = re.sub(r"([\s\S]*?)", lambda m: "*" + m.group(1) + "*", s, flags=re.I) s = re.sub(r"([^<]*?)", lambda m: "`" + m.group(1) + "`", s, flags=re.I) s = re.sub(r"", "\n", s, flags=re.I) s = re.sub(r"\x00F(\d+)\x00", lambda m: fence_stash[int(m.group(1))], s) # Fenced code blocks def fenced(m): lang, code = m.group(1), m.group(2).rstrip("\n") h = f'
{esc(lang)}
' if lang else "" return h + "
" + esc(code) + "
" s = re.sub(r"```([\w+-]*)\n?([\s\S]*?)```", fenced, s) s = re.sub(r"`([^`\n]+)`", lambda m: "" + esc(m.group(1)) + "", s) # Inline formatting (top-level, outside list items) s = re.sub(r"\*\*\*(.+?)\*\*\*", lambda m: "" + esc(m.group(1)) + "", s) s = re.sub(r"\*\*(.+?)\*\*", lambda m: "" + esc(m.group(1)) + "", s) s = re.sub(r"\*([^*\n]+)\*", lambda m: "" + esc(m.group(1)) + "", s) # Block elements using inlineMd for their content s = re.sub(r"^### (.+)$", lambda m: "

" + inline_md(m.group(1)) + "

", s, flags=re.M) s = re.sub(r"^## (.+)$", lambda m: "

" + inline_md(m.group(1)) + "

", s, flags=re.M) s = re.sub(r"^# (.+)$", lambda m: "

" + inline_md(m.group(1)) + "

", s, flags=re.M) s = re.sub(r"^---+$", "
", s, flags=re.M) s = re.sub(r"^> (.+)$", lambda m: "
" + inline_md(m.group(1)) + "
", s, flags=re.M) def handle_ul(block): lines = block.strip().split("\n") out = "" s = re.sub(r"((?:^(?: )?[-*+] .+\n?)+)", lambda m: handle_ul(m.group()), s, flags=re.M) def handle_ol(block): lines = block.strip().split("\n") out = "
    " for l in lines: text = re.sub(r"^ {0,4}\d+\. ", "", l) out += f"
  1. {inline_md(text)}
  2. " return out + "
" s = re.sub(r"((?:^(?: )?\d+\. .+\n?)+)", lambda m: handle_ol(m.group()), s, flags=re.M) # Safety net: escape unknown tags in remaining text s = re.sub(r"]*>", lambda m: m.group() if SAFE_TAGS.match(m.group()) else esc(m.group()), s) # Paragraph wrap parts = s.split("\n\n") def wrap(p): p = p.strip() if not p: return "" if re.match(r"^<(h[1-6]|ul|ol|pre|hr|blockquote)", p): return p return "

" + p.replace("\n", "
") + "

" s = "\n".join(wrap(p) for p in parts) return s # ── Static analysis: verify key structures exist in ui.js ──────────────────── def test_render_md_pre_pass_converts_strong(cleanup_test_sessions): """ui.js renderMd() must have pre-pass that converts to **.""" src = REPO_ROOT / "static" / "ui.js" code = src.read_text() assert "" in code and "**" in code, "pre-pass for not found" # Verify the specific conversion pattern assert re.search(r".*?\*\*", code, re.S), \ "renderMd pre-pass should convert ... to **...**" def test_render_md_has_safety_net(cleanup_test_sessions): """ui.js must have a safety-net that escapes unknown HTML tags after the pipeline.""" src = REPO_ROOT / "static" / "ui.js" code = src.read_text() assert "SAFE_TAGS" in code, "SAFE_TAGS allowlist regex not found in ui.js" assert "esc(tag)" in code, "safety-net esc(tag) call not found in ui.js" def test_render_md_stashes_code_blocks(cleanup_test_sessions): """ui.js pre-pass must stash code blocks before replacing safe HTML tags.""" src = REPO_ROOT / "static" / "ui.js" code = src.read_text() assert "fence_stash" in code, "fence_stash not found in renderMd pre-pass" def test_render_md_handles_br_tag(cleanup_test_sessions): """ui.js must convert
to newline in pre-pass.""" src = REPO_ROOT / "static" / "ui.js" code = src.read_text() assert re.search(r" handling not found" def test_render_md_no_placeholder_remnants(cleanup_test_sessions): """Old Unicode placeholder approach (\\uE001-\\uE005) must be gone.""" src = REPO_ROOT / "static" / "ui.js" code = src.read_text() for old_ph in ["\\uE001", "\\uE002", "\\uE003", "\\uE004", "\\uE005"]: assert old_ph not in code, \ f"Old placeholder {old_ph} still present — broken implementation not cleaned up" def test_render_md_safe_tag_allowlist_complete(cleanup_test_sessions): """SAFE_TAGS allowlist must include all tags the pipeline emits.""" src = REPO_ROOT / "static" / "ui.js" code = src.read_text() required = ["strong", "em", "code", "pre", "ul", "ol", "li", "table", "blockquote", "hr", "br", "a", "div"] safe_tags_match = re.search(r"SAFE_TAGS\s*=\s*/(.+?)/i", code) assert safe_tags_match, "SAFE_TAGS regex not found" pattern = safe_tags_match.group(1) for tag in required: assert tag in pattern, f"Tag '{tag}' missing from SAFE_TAGS allowlist" # ── Behavioural: renderMd logic via Python mirror ───────────────────────────── def test_render_md_markdown_bold(cleanup_test_sessions): """**word** markdown renders as word.""" out = render_md("Hello **world**") assert "world" in out def test_render_md_html_strong_passthrough(cleanup_test_sessions): """word in AI output renders as bold.""" out = render_md("Hello world") assert "world" in out def test_render_md_html_b_tag(cleanup_test_sessions): """word renders as word.""" out = render_md("Hello world") assert "world" in out def test_render_md_html_em_passthrough(cleanup_test_sessions): """word renders as italic.""" out = render_md("Hello world") assert "world" in out def test_render_md_html_i_tag(cleanup_test_sessions): """word renders as word.""" out = render_md("Hello word") assert "word" in out def test_render_md_html_code_passthrough(cleanup_test_sessions): """text renders as inline code.""" out = render_md("use print()") assert "print()" in out def test_render_md_html_br_becomes_newline(cleanup_test_sessions): """
in AI output becomes a newline (rendered as
inside

later).""" out = render_md("line one
line two") assert "line one\nline two" in out or "line one
line two" in out def test_render_md_mixed_markdown_and_html(cleanup_test_sessions): """Markdown and HTML formatting can coexist in the same response.""" out = render_md("**markdown** and html") assert "markdown" in out assert "html" in out def test_render_md_html_strong_in_list_item(cleanup_test_sessions): """THE SCREENSHOT BUG: tags inside list items must render as bold, not as escaped literal text like <strong>.""" out = render_md( "- All items get `border-radius: 0 8px 8px 0`\n" "- Active item uses #e8a030\n" "- Project items show their color\n" "- Regular items stay muted" ) assert "<strong>" not in out, \ "Escaped literal found in list output — bold not rendering" assert "All items" in out assert "Active item" in out assert "border-radius: 0 8px 8px 0" in out assert "#e8a030" in out def test_render_md_exact_screenshot_content(cleanup_test_sessions): """Exact text from the ui-changes-unrendered-html-tags.png screenshot. This is the canonical regression test for the inlineMd fix. All four bullet points must render and as HTML, not literal text.""" out = render_md( "- All items now have border-radius: 0 8px 8px 0" " \u2014 straight left edge everywhere, rounded on the right\n" "- Active item is now gold/amber (#e8a030)" " \u2014 same warm gold used in the logo \u2014 instead of blue," " so it stands out distinctly from everything else\n" "- Project items still show their project color on the left" " border, but only when they're not the active item (active always wins with gold)\n" "- Regular items (no project) still have no left border color" ) # None of the safe tags should appear as literal escaped text assert "<strong>" not in out, \ "Literal <strong> found — is not rendering as bold" assert "</strong>" not in out, \ "Literal </strong> found — closing tag is not rendering" assert "<code>" not in out, \ "Literal <code> found — is not rendering as inline code" # Each item's bold label must render correctly assert "All items" in out assert "Active item" in out assert "Project items" in out assert "Regular items" in out # The code spans in items 1 and 2 must render correctly assert "border-radius: 0 8px 8px 0" in out assert "#e8a030" in out # The surrounding prose text must be preserved assert "straight left edge everywhere" in out assert "same warm gold used in the logo" in out assert "active always wins with gold" in out def test_render_md_markdown_bold_in_list_item(cleanup_test_sessions): """**bold** markdown inside list items must render as .""" out = render_md("- **First** item\n- **Second** item with `code`") assert "First" in out assert "Second" in out assert "code" in out def test_render_md_html_strong_in_blockquote(cleanup_test_sessions): """ inside blockquote must render as bold.""" out = render_md("> Note: pay attention") assert "<strong>" not in out assert "Note:" in out def test_render_md_html_strong_in_heading(cleanup_test_sessions): """ inside a heading must render as bold.""" out = render_md("## Important Section") assert "<strong>" not in out assert "Important" in out def test_render_md_xss_in_list_still_blocked(cleanup_test_sessions): """XSS attempts in list items must still be escaped.""" out = render_md("- bad") assert " ") assert " must be HTML-escaped.""" out = render_md("") assert "") assert "