"""Unit: the verbatim panel client and the answer's Copy buttons (phase 182, task 07). The browser half of the story lives in the phase's Playwright suite (task 08); this module pins the SOURCE contract of ``frontend/assets/verbatim.js`` and the three client surfaces that consume it, in the established ``test_frontend_*`` convention — plus a node-driven half (skipped when node is unavailable, the phase-44/138/180/182 idiom) that runs the panel's PURE logic (caption, gutter, locator match, block guard) against the real file. Why these particular pins (the phase's drift risks, in order): * **ONE locator grammar.** ``markdown.js::borLocator`` is the client's single normalizer (task 06); hydration may only compare the server's canonical ``locator`` against the already-normalized ``data-bor-locator`` with a plain string ``===``. A second grammar in a second file is the phase's main way to break silently — so this module pins that no normalizer exists in ``verbatim.js`` (no ``borLocator``, no range regex, no ``parseInt``). * **The model's bytes never reach the screen or the clipboard.** The panel's text node is ``pre.textContent = block.text`` (server bytes, verbatim) and the Copy button copies the same string — never the DOM, never the gutter, never the model's re-typed fence body (which task 06 dropped upstream). * **The claim is carried by words, not colours** (LOCKED A5): the badge text is ``VERBATIM``, the honest state is the pinned sentence ``I could not verify this against the knowledge base``, and an ordinary code block gets a Copy button with NO badge. * **Parity.** The live turn, the reload restore and the anonymous shared page call the SAME globals, in the SAME order (hydrate → append the unlocated → settle the code blocks, all inside the ``answer_image`` band and BEFORE the source clusters), so one answer cannot render three different ways. * **No panel is ever left lying.** Every terminal path — the ``error`` frame, the 300 s guard, the Stop action, the failed-turn funnel — settles the leftover placeholders; nothing reads "Retrieving the exact text…" after the turn is over. * **CSP + the house XSS rule.** Everything is ``createElement`` + ``textContent``, every handler is ``addEventListener``, and there is no ``innerHTML``/``onclick=``/``oncopy=`` anywhere in the new client. """ from __future__ import annotations import json import re import shutil import subprocess from pathlib import Path import pytest FRONTEND = Path(__file__).resolve().parents[2] / "frontend" ASSETS = FRONTEND / "assets" VERBATIM_JS = ASSETS / "verbatim.js" APP_JS = ASSETS / "app.js" SHARED_JS = ASSETS / "shared.js" DOCUMENT_JS = ASSETS / "document.js" MARKDOWN_JS = ASSETS / "markdown.js" STYLES_CSS = ASSETS / "styles.css" INDEX_HTML = FRONTEND / "index.html" SHARED_HTML = FRONTEND / "shared.html" CONTAINERFILE = FRONTEND.parent / "Containerfile" HAVE_NODE = shutil.which("node") is not None #: The labelled phase-182 CSS region (task 06's markers — task 07 extends #: INSIDE it, so the palette-only and contiguity pins keep holding). BLOCK_BEGIN = "/* ---------- Verbatim panels (phase 182, task 06) — BEGIN ----------" BLOCK_END = "/* ---------- Verbatim panels (phase 182, task 06) — END ---------- */" #: The honest state (pinned copy — task 07's Testing section). UNRESOLVED_COPY = "I could not verify this against the knowledge base" #: The badge's WORD (LOCKED A5) and the tooltip that explains it. BADGE_TEXT = "VERBATIM" BADGE_TITLE = "These bytes are copied from the knowledge base, not written by the model." #: The Copy control's accessible name template — the FULL path, never a #: clipped stand-in (the phase-156 clipped-label rule). COPY_ARIA = "Copy ${shown} lines from ${identity}" #: The globals task 07 exposes for the two page modules. PUBLIC_GLOBALS = ( "buildVerbatimPanel", "hydrateVerbatim", "appendUnlocatedVerbatim", "settleVerbatimPlaceholders", "enhanceAnswerCodeBlocks", "copyTextWithFallback", ) def _text(path: Path) -> str: assert path.is_file(), f"missing file: {path}" return path.read_text(encoding="utf-8") def _strip_comments(text: str) -> str: """Comments out — the CSP and literal pins below are about CODE. The file's own header documents the very patterns it forbids (``onclick=``, ``javascript:``), and a prose-blind grep would flag the documentation of the rule as the rule.""" text = re.sub(r"/\*[\s\S]*?\*/", "", text) return re.sub(r"(?m)^\s*//.*$", "", text) def _fn(js: str, name: str) -> str: """The source of a top-level (optionally ``async``) ``function ``.""" start = js.find(f"function {name}(") assert start != -1, f"{name}() must exist" return js[start : js.find("\n}\n", start) + 4] def _body(js: str, name: str) -> str: return _fn(js, name) # --------------------------------------------------------------------------- # the client ships, and ships in the right shape # --------------------------------------------------------------------------- def test_verbatim_js_is_a_classic_script_after_markdown_on_both_pages() -> None: """Both pages load it as a PLAIN ``' in index assert '' in shared assert index.index('src="assets/markdown.js"') < index.index('src="assets/verbatim.js"'), ( "verbatim.js hydrates markdown.js's placeholders — it loads after it" ) assert index.index('src="assets/verbatim.js"') < index.index( 'type="module" src="/assets/app.js"' ), "the globals must exist before app.js evaluates" assert shared.index('src="/assets/markdown.js"') < shared.index('src="/assets/verbatim.js"') assert shared.index('src="/assets/verbatim.js"') < shared.index( 'type="module" src="/assets/shared.js"' ) tag = next(line for line in index.splitlines() if "verbatim.js" in line and " None: """Stage 1 builds it the way it builds ``markdown.js``: ``--minify`` WITHOUT ``--bundle`` (bundling a classic script would break the global exposure), and the source itself stays free of top-level ``import``/``export``.""" container = _text(CONTAINERFILE) line = next( row for row in container.splitlines() if "assets/verbatim.js" in row ) assert "--minify" in line assert "--outfile=/out/assets/verbatim.js" in line assert "--bundle" not in line, "a classic script is never bundled" assert "--format=esm" not in line js = _text(VERBATIM_JS) assert "\nimport " not in js and "\nexport " not in js def test_every_public_global_exists() -> None: """The API the two page modules call, as top-level function declarations (the ``markdown.js`` exposure pattern — a classic script's top-level functions ARE the globals).""" js = _text(VERBATIM_JS) for name in PUBLIC_GLOBALS: assert f"function {name}(" in js, f"verbatim.js must expose {name}" def test_no_locator_normalizer_is_re_implemented() -> None: """THE phase's drift pin: hydration compares the server's canonical ``locator`` against the ``data-bor-locator`` markdown.js already normalized, with a PLAIN string compare. No grammar here — no ``borLocator``, no range regex, no en-dash handling, no ``parseInt``, no ``L``-prefix stripping (a second grammar in a second file is how a resolved block stops hydrating).""" js = _text(VERBATIM_JS) assert "function borLocator" not in js assert "parseInt" not in js assert "\u2013" not in _fn(js, "matchVerbatimBlock"), "no en-dash parsing here" for pattern in ("[Ll]", "L?\\d", "#L", "trimStart", "normalize("): assert pattern not in js, f"{pattern} looks like a re-implemented grammar" match = _fn(js, "matchVerbatimBlock") assert "block.locator === key" in match, "the exact match is a plain string compare" hydrate = _fn(js, "hydrateVerbatim") assert 'node.getAttribute("data-bor-locator")' in hydrate, ( "hydration reads the attribute markdown.js wrote — it never re-derives it" ) # --------------------------------------------------------------------------- # the panel's contract (LOCKED A2/A5/A7) # --------------------------------------------------------------------------- def test_panel_is_a_labelled_group_keyed_by_the_servers_locator() -> None: """``role="group"`` + ``aria-label="Verbatim text from , lines N\u2013M"`` + ``data-bor-locator`` = the SERVER's canonical locator (the key ``appendUnlocatedVerbatim`` later recognises, which is why a re-render never duplicates a panel).""" panel = _fn(_text(VERBATIM_JS), "buildVerbatimPanel") assert 'root.setAttribute("role", "group")' in panel assert 'root.setAttribute("aria-label", verbatimPanelLabel(block))' in panel assert "root.dataset.borLocator" in panel assert "block.locator" in panel, "the server's locator is the panel's identity" label = _fn(_text(VERBATIM_JS), "verbatimPanelLabel") assert '"Verbatim text from' in json.dumps(label) or "Verbatim text from" in label def test_badge_is_a_word_with_the_pinned_explanation() -> None: """LOCKED A5: the claim is the WORD ``VERBATIM`` (never an icon or a colour), with the tooltip that says what it means.""" js = _text(VERBATIM_JS) assert f'badge.textContent = "{BADGE_TEXT}"' in js assert BADGE_TITLE in js assert 'badge.className = "verbatim-badge"' in js def test_unresolved_state_is_the_pinned_sentence_and_nothing_else() -> None: """The honest state for a locator the server did not resolve: the pinned sentence, plus the document link ONLY when the identity parsed into a ``source/path``. NO badge, NO body, NONE of the model's bytes — and the builder creates neither a badge nor a ``
`` node at all, which is
    stronger than hiding one."""
    js = _text(VERBATIM_JS)
    assert f'"{UNRESOLVED_COPY}"' in js
    node = _fn(js, "buildVerbatimUnresolved")
    assert 'root.className = "verbatim verbatim-unresolved"' in node
    assert 'state.className = "verbatim-state"' in node
    assert "verbatim-badge" not in node, "an unresolved panel never badges"
    assert 'createElement("pre")' not in node, "and never has a body"
    assert 'createElement("ol")' not in node, "nor a gutter"
    assert "indexOf(" in node and "slash > 0" in node, (
        "the link appears only when the identity split into a source and a path"
    )


def test_copy_button_names_the_full_path_and_copies_the_servers_bytes() -> None:
    """The completion criterion at source level: the panel's Copy control is
    built from ``block.text`` (never from the DOM, never with the gutter's
    digits), and its accessible name is ``Copy N lines from `` —
    the full path, because the visible label is the CSS clip target
    (phase 156)."""
    js = _text(VERBATIM_JS)
    panel = _fn(js, "buildVerbatimPanel")
    assert COPY_ARIA in panel
    build = panel[panel.index("buildCopyButton(") :]
    assert '"verbatim-copy"' in build
    assert "block.text," in build.split("\n)")[0], (
        "the copied string is `block.text` — the exact bytes the panel shows"
    )
    control = _fn(js, "buildCopyButton")
    assert 'btn.setAttribute("aria-label", ariaLabel)' in control
    assert 'label.textContent = "Copy"' in control
    assert 'label.textContent = "Copied"' in control, "the state is TEXT (WCAG 1.4.1)"
    assert 'btn.classList.add("copied")' in control
    clipboard = _fn(js, "copyTextWithFallback")
    assert "await navigator.clipboard.writeText(text)" in clipboard, (
        "the caller's string goes to the clipboard untouched — no DOM read"
    )


def test_body_is_the_servers_text_with_the_servers_line_numbers() -> None:
    """The bounded body: the ``
    `` gutter (``aria-hidden`` — the range rides the panel's label, and the digits are ``user-select: none`` in CSS so a selection never drags them into the clipboard) and the ``
    ``
        whose ``textContent`` is ``block.text``, byte for byte."""
        js = _text(VERBATIM_JS)
        panel = _fn(js, "buildVerbatimPanel")
        assert 'body.className = "verbatim-body"' in panel
        assert 'numbers.className = "verbatim-lines"' in panel
        assert 'numbers.setAttribute("aria-hidden", "true")' in panel
        assert 'pre.className = "verbatim-text"' in panel
        assert "pre.textContent = block.text" in panel
        assert "li.textContent = row.number" in panel
        gutter = _fn(js, "verbatimGutter")
        assert 'split("\\n")' in gutter, "one gutter row per line of the SERVER text"
    
    
    def test_truncated_block_says_the_rest_is_in_the_document() -> None:
        """A capped block appends ONE muted line plus the same viewer link — the
        panel never pretends the slice is the whole file."""
        panel = _fn(_text(VERBATIM_JS), "buildVerbatimPanel")
        assert "if (block.truncated)" in panel
        assert 'more.className = "verbatim-more"' in panel
        assert "the rest is in the document" in panel
        assert "buildVerbatimOpenLink(" in panel.split("if (block.truncated)")[1]
    
    
    def test_open_link_is_a_real_href_with_the_modal_hook() -> None:
        """The panel links to the document viewer: an always-same-origin
        ``/document.html?source=…&path=…`` built with ``encodeURIComponent`` (so a
        path can never turn into ``javascript:``/``data:``), plus the chat page's
        click-to-modal hook through ``opts.onOpen`` — attached with
        ``addEventListener``, never an inline attribute."""
        js = _text(VERBATIM_JS)
        link = _fn(js, "buildVerbatimOpenLink")
        assert 'link.className = "verbatim-open"' in link
        assert "link.addEventListener(" in link
        assert "event.preventDefault()" in link
        assert "opts.onOpen(source, path, link)" in link
        href = _fn(js, "verbatimDocumentHref")
        assert "/document.html?source=" in href
        assert "encodeURIComponent(source)" in href and "encodeURIComponent(path)" in href
        code = _strip_comments(js)
        assert "javascript:" not in code and "data:" not in code, (
            "no scheme is ever assembled into a link target"
        )
    
    
    def test_plain_answer_code_blocks_get_copy_and_never_a_badge() -> None:
        """``enhanceAnswerCodeBlocks``: every ``
    `` in an answer bubble that
        is NOT inside a ``.verbatim`` gets ``.code-block`` + ``.code-block-copy``.
        NO badge and NO caption — verbatim-and-badged OR plainly the model's,
        never a hybrid (LOCKED A5). Idempotent: a ``
    `` already inside a
        ``.code-block`` is left alone."""
        fn = _fn(_text(VERBATIM_JS), "enhanceAnswerCodeBlocks")
        assert 'box.className = "code-block"' in fn
        assert '"code-block-copy"' in fn
        assert 'pre.closest(".verbatim")' in fn, "a panel owns its own Copy button"
        assert 'parent.classList.contains("code-block")' in fn, "idempotent on re-render"
        assert "verbatim-badge" not in fn
        assert "verbatim-caption" not in fn
    
    
    def test_hydration_is_idempotent_and_never_throws_on_garbage() -> None:
        """Only ``.verbatim-pending`` is replaced, so a hydrated panel (or one
        restored from storage) survives a re-run with its ``Copied`` state and
        scroll position; a missing/malformed ``blocks`` settles to the honest
        state instead of throwing."""
        js = _text(VERBATIM_JS)
        hydrate = _fn(js, "hydrateVerbatim")
        assert ".verbatim-pending[data-bor-locator]" in hydrate
        assert (
            "verbatim-unresolved"
            not in hydrate.split("matchVerbatimBlock")[1].split("replaceWith")[1].split("\n")[0]
        ), "the unresolved node comes from its own builder"
        assert "replaceWith(" in hydrate
        assert "try {" not in hydrate, "the guards make it total — no exception path"
        assert "verbatimBlocks(blocks)" in hydrate
        guard = _fn(js, "verbatimBlocks")
        assert "Array.isArray(blocks)" in guard, "a missing frame key is not an error"
    
    
    def test_unlocated_blocks_land_in_the_answer_image_band() -> None:
        """The blocks no placeholder matched (a ``show_text`` call the prose never
        named) append to ``.msg-body`` — the same band as the answer image, below
        the text and above the source clusters, in frame order — and a block
        already placed (a panel carrying its canonical locator) is never
        appended twice."""
        fn = _fn(_text(VERBATIM_JS), "appendUnlocatedVerbatim")
        assert 'wrap.querySelector(".msg-body")' in fn
        assert "host.appendChild(buildVerbatimPanel(block, opts))" in fn
        assert "placed.add(" in fn and "if (placed.has(key)) continue" in fn
        assert "verbatim-unresolved" in fn, (
            "an unresolved placeholder is NOT a placement (the block still belongs on screen)"
        )
    
    
    def test_terminal_settle_renders_the_honest_state() -> None:
        """``settleVerbatimPlaceholders`` turns every leftover ``.verbatim-pending``
        into the unresolved node and records the ``kind`` for the next reader."""
        js = _text(VERBATIM_JS)
        fn = _fn(js, "settleVerbatimPlaceholders")
        assert 'root.querySelectorAll(".verbatim-pending")' in fn
        assert "buildVerbatimUnresolved(" in fn
        assert "replaceWith(" in fn, "the pending node is REPLACED, never patched"
        assert 'node.getAttribute("data-bor-locator")' in fn, (
            "the locator the model named rides into the honest state"
        )
        unresolved = _fn(js, "buildVerbatimUnresolved")
        assert 'root.dataset.verbatimState = (opts && opts.kind) || "unverified"' in unresolved, (
            "the settle kind is recorded for the next reader"
        )
        assert "state.textContent = verbatimUnresolvedCopy()" in unresolved
    
    
    # ---------------------------------------------------------------------------
    # CSP + the house XSS rule (the new client, and the new code in the pages)
    # ---------------------------------------------------------------------------
    
    
    def test_no_inline_handlers_and_no_html_anywhere_in_the_client() -> None:
        """CSP is ``default-src 'self'``: no ``onclick=``/``oncopy=`` attribute
        strings, no inline ``