**Phase 182 — final verification pass (all 8 tasks already complete; this was verification + docs + one defect fix).**
- Verified: `app/rag/verbatim.py` (locator grammar, `splitlines()` slice, line-boundary cap, `resolve_answer_blocks`), `SHOW_TEXT_TOOL` + `shown_texts`, `SHOW_TEXT_CLAUSE`, `done.answer_text` (9 fields, key dropped when empty) + `ChatMessageRecord.answer_text` (≤50, 600 000-char cap counted in the 8 M budget), `verbatim.js` + 4 `{verbatim:true}` renderers, `Containerfile` minify step, `.env.example` 3 vars.
- `uv run python -c "…answer_text, show_text_max_chars, show_text_max_blocks"` → `True 12000 5`; each negative refuses startup naming its field; `0` accepted as inert ✅
- `bash .agents/validate.sh` → **validation OK**: 4320 unit+integration tests green, `TOTAL 6849 17 → 99%` (gate app/ >90% OK), `ruff check .` "All checks passed!", `pyright` 0 errors (re-run after my edits, 08:28).
- E2E, each in isolation `--no-cov`: `test_verbatim_show_text.py` 13 passed (panel bytes == `documents.content`, server caption, clipboard == server string, `/document.html?source=…&path=…`, honest unresolved state, reload+share, `show_image` regression, flag-off app) ✅; green: `test_answer_images` `test_harness_aligned_tools` `test_agent_document_tools` `test_markdown_tables` `test_markdown_inline_links_images` `test_svg_document_preview` `test_smoke` `test_document_viewer` `test_markdown_blockquotes` `test_chat_persistence` `test_thinking_timeline` `test_chat_rag` `test_settings_mobile_value_width`.
- **Defect found & fixed (pre-existing, not phase 182):** `tests/e2e/test_admin_settings_panel.py::test_admin_sees_groups_and_source_badges` failed — its whole-page `assert "reese" not in page.content()` tripped on the shell's onboarding chips, which follow the newest saved chats in the one shared E2E DB (a leftover `reeselink.json` question from a tool suite). Scoped the strict sweep to `.settings-shell` and kept a whole-page `reese:reese` credential needle → 12 passed with the leftovers still present.
- Docs: README fixed 2 inconsistencies — Quick Start omitted the **required** `BOR_ADMIN_PASSWORD`/`BOR_SESSION_SECRET` (the app fail-loud refuses boot) and the broken anchor `#chat--` → `#chat-`; full re-read of README (1816 lines) + AGENTS confirmed the phase-182 feature/settings/log/UI-contract text matches the code, and every `.env.example` var + every `Settings` field is documented.
- Notable: kept `resolve_answer_blocks` synchronous on the SSE terminal path (bounded indexed SELECTs, matching the existing `query_log` precedent and the phase's documented fence-lookup bound); no live-infra changes made.
- Next pending phase: none — `todo/` holds only 182 (its 8 task files are in `complete/`; the harness commits + moves `00_phase.md`).
868 lines
41 KiB
Python
868 lines
41 KiB
Python
"""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 <name>``."""
|
||
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 ``<script>`` immediately AFTER
|
||
``markdown.js`` (whose placeholders it hydrates) and BEFORE their page
|
||
module (which calls it as a global). Absolute paths on ``shared.html``
|
||
(the nested ``/shared/<token>`` route breaks a relative ref), relative on
|
||
``index.html`` exactly like its neighbours. No ``type="module"`` — a
|
||
module would defer past the pages' boot and expose no globals."""
|
||
index = _text(INDEX_HTML)
|
||
shared = _text(SHARED_HTML)
|
||
assert '<script src="assets/verbatim.js"></script>' in index
|
||
assert '<script src="/assets/verbatim.js"></script>' 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 "<script" in line)
|
||
assert 'type="module"' not in tag, "a classic script — the globals are the API"
|
||
# The document viewer never loads it (a file's own ```bor fence is text).
|
||
document_html = _text(FRONTEND / "document.html")
|
||
assert "verbatim.js" not in document_html
|
||
|
||
|
||
def test_containerfile_produces_it_as_a_minified_classic_script() -> 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 <path>, 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 ``<pre>`` 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 <source/path>`` —
|
||
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 ``<ol>`` 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 ``<pre>``
|
||
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 ``<pre>`` 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 ``<pre>`` 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 ``<script>``, no HTML string, no ``innerHTML`` (not
|
||
even ``+=``) in ``verbatim.js`` — every node is ``createElement``, every
|
||
value ``textContent``/``title``/a built ``href``, every handler
|
||
``addEventListener``."""
|
||
code = _strip_comments(_text(VERBATIM_JS))
|
||
for marker in (
|
||
"onclick=",
|
||
"oncopy=",
|
||
"onerror=",
|
||
"onload=",
|
||
".innerHTML",
|
||
".outerHTML",
|
||
"insertAdjacentHTML(",
|
||
"document.write(",
|
||
"eval(",
|
||
"<script",
|
||
):
|
||
assert marker not in code, f"verbatim.js must stay HTML/handler-free: {marker}"
|
||
assert "document.createElement(" in code
|
||
assert code.count("document.createElement(") > 15, (
|
||
"the panel is built node by node (the house rule)"
|
||
)
|
||
assert "addEventListener(" in code
|
||
|
||
|
||
def test_the_pages_new_code_adds_no_inline_handler_or_inner_html() -> None:
|
||
"""The task-07 additions to ``app.js`` / ``shared.js`` obey the same rule:
|
||
no ``innerHTML +=`` anywhere, and the only ``onclick=``-shaped text in the
|
||
two files stays inside their existing comments (none)."""
|
||
for path in (APP_JS, SHARED_JS):
|
||
js = _text(path)
|
||
assert "innerHTML += " not in js, f"{path.name}: no innerHTML concatenation"
|
||
assert "onclick=" not in js
|
||
assert "oncopy=" not in js
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# app.js: the live turn, the save point, the restore, the terminal paths
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def _done_branch(js: str) -> str:
|
||
start = js.index('ev.type === "done"')
|
||
end = js.index('ev.type === "tool_result"', start)
|
||
return js[start:end]
|
||
|
||
|
||
def test_the_done_handler_hydrates_before_the_source_clusters() -> None:
|
||
"""The order IS the layout: the answer image, then the hydrated panels,
|
||
then the blocks the answer never located, then the Copy buttons on the
|
||
settled code blocks, then the source clusters (phase-124/130 DOM order —
|
||
the panels sit below the answer text and above the chips)."""
|
||
branch = _done_branch(_text(APP_JS))
|
||
for call in (
|
||
"appendAnswerImage(wrap, ev.answer_image)",
|
||
"hydrateVerbatim(",
|
||
"appendUnlocatedVerbatim(wrap, ev.answer_text || [], VERBATIM_LINK_OPTS)",
|
||
"enhanceAnswerCodeBlocks(wrap)",
|
||
"appendSources(wrap, ev.sources)",
|
||
):
|
||
assert call in branch, f"the done handler must call {call}"
|
||
order = [
|
||
branch.index("appendAnswerImage("),
|
||
branch.index("hydrateVerbatim("),
|
||
branch.index("appendUnlocatedVerbatim("),
|
||
branch.index("enhanceAnswerCodeBlocks("),
|
||
branch.index("appendSources("),
|
||
]
|
||
assert order == sorted(order), "image → panels → unlocated → copy → sources"
|
||
assert "VERBATIM_LINK_OPTS" in branch[branch.index("hydrateVerbatim(") :][:200]
|
||
|
||
|
||
def test_the_answer_renderers_opt_in_and_nothing_else_does() -> None:
|
||
"""The per-delta write and the ``done``-time re-render both pass
|
||
``{ verbatim: true }`` (the third site is the restore renderer, pinned in
|
||
``test_the_restore_path_rehydrates_from_the_record``); the user bubble of
|
||
the same turn does NOT — a question is not a locator."""
|
||
js = _text(APP_JS)
|
||
branch = _done_branch(js)
|
||
assert "renderMarkdown(acc, { verbatim: true })" in js
|
||
assert "renderMarkdown(finalText, { verbatim: true })" in branch
|
||
assert 'addMessage(\n "user",\n renderMarkdown(text),' in js, (
|
||
"the sent question stays one-argument"
|
||
)
|
||
|
||
|
||
def test_the_save_point_persists_the_blocks_additively() -> None:
|
||
"""``answer_text: ev.answer_text || undefined`` on the ``done`` save point
|
||
— ``undefined`` drops the key from the JSON (the ``answer_image``
|
||
precedent), so a pre-phase record stays byte-identical and a
|
||
``answer_text=False`` turn stores nothing new."""
|
||
branch = _done_branch(_text(APP_JS))
|
||
assert "answer_text: ev.answer_text || undefined," in branch
|
||
turn = _fn(_text(APP_JS), "rememberBrainTurn")
|
||
assert "...meta" in turn, (
|
||
"the meta spread carries the key — rememberBrainTurn itself is untouched"
|
||
)
|
||
|
||
|
||
def test_the_restore_path_rehydrates_from_the_record() -> None:
|
||
"""Reload parity: the restore renderer opts in, then hydrates from
|
||
``m.answer_text`` and appends the unlocated blocks in the same band, then
|
||
settles the code blocks — the SAME builders as the live turn. A record
|
||
with no ``answer_text`` renders its fences as the unresolved state (the
|
||
record is what it is: never resurrect model bytes)."""
|
||
js = _text(APP_JS)
|
||
body = _fn(js, "renderStoredMessage")
|
||
assert 'addMessage("brain", renderMarkdown(m.text, { verbatim: true }), true)' in body
|
||
assert (
|
||
'hydrateVerbatim(wrap.querySelector(".bubble"), m.answer_text || [], VERBATIM_LINK_OPTS)'
|
||
in body
|
||
)
|
||
assert "appendUnlocatedVerbatim(wrap, m.answer_text || [], VERBATIM_LINK_OPTS)" in body
|
||
assert "enhanceAnswerCodeBlocks(wrap)" in body
|
||
order = [
|
||
body.index("appendAnswerImage(wrap, m.answer_image)"),
|
||
body.index("hydrateVerbatim("),
|
||
body.index("appendUnlocatedVerbatim("),
|
||
body.index("enhanceAnswerCodeBlocks("),
|
||
body.index("appendSources(wrap, m.sources)"),
|
||
]
|
||
assert order == sorted(order)
|
||
|
||
|
||
def test_every_terminal_path_settles_the_placeholders() -> None:
|
||
"""Nothing sits at \u201cRetrieving the exact text\u2026\u201d after a terminal
|
||
frame: the ``error`` frame handler, the 300 s guard, that guard's own
|
||
abort surface, the user-stop branch and the failed-turn funnel each call
|
||
``settleVerbatimPlaceholders(…, "unverified")`` — five sites, no more."""
|
||
js = _text(APP_JS)
|
||
assert js.count("settleVerbatimPlaceholders(") == 5, (
|
||
"the five terminal sites: error frame, timeout, aborted catch, stop "
|
||
"catch, failed-turn funnel"
|
||
)
|
||
assert js.count('"unverified"') >= 5, "every settle names the honest kind"
|
||
funnel = _fn(js, "finalizeFailedTurn")
|
||
assert 'settleVerbatimPlaceholders(wrap.querySelector(".bubble"), "unverified");' in funnel
|
||
assert funnel.index("closeAllRuns(wrap)") < funnel.index("settleVerbatimPlaceholders(")
|
||
stop = js[js.index("stoppedByUser || err?.name ===") :]
|
||
assert "settleVerbatimPlaceholders(" in stop[:800], "the Stop action settles"
|
||
frame = js[js.index('ev.type === "error"') :]
|
||
assert "settleVerbatimPlaceholders(" in frame[:700], "the error frame settles"
|
||
|
||
|
||
def test_the_chat_panel_links_back_to_the_chat_page() -> None:
|
||
"""The chat page's panels navigate EXACTLY like a source chip: the same
|
||
``documentUrl(…, "/")`` href (back → the chat, phase 153) and the same
|
||
left-click → same-page modal (phase 26), handed to the client as ONE
|
||
options object."""
|
||
js = _text(APP_JS)
|
||
opts = (
|
||
_fn(js, "VERBATIM_LINK_OPTS")
|
||
if "function VERBATIM_LINK_OPTS" in js
|
||
else js[
|
||
js.index("const VERBATIM_LINK_OPTS") : js.index(
|
||
"};", js.index("const VERBATIM_LINK_OPTS")
|
||
)
|
||
]
|
||
)
|
||
assert "documentHref:" in opts and 'documentUrl(source, path, "/")' in opts
|
||
assert "onOpen:" in opts and 'openDocumentModal(source, path, opener, "/")' in opts
|
||
# every app.js call site passes it
|
||
assert _text(APP_JS).count("VERBATIM_LINK_OPTS") >= 5
|
||
|
||
|
||
def test_the_share_path_delegates_to_the_one_clipboard_helper() -> None:
|
||
"""Task 07 consolidates the clipboard path: ``app.js``'s owner-locked
|
||
share copy delegates to ``copyTextWithFallback`` with the same host and
|
||
the same title, and the page no longer carries its own selection
|
||
helper."""
|
||
js = _text(APP_JS)
|
||
share = _fn(js, "copyShareLinkWithFallback")
|
||
assert "copyTextWithFallback(" in share
|
||
assert "composer," in share
|
||
assert '"Share link — click, then copy (Ctrl/Cmd+C)"' in share
|
||
assert "function selectAllInField" not in js, (
|
||
"the selection helper moved with the clipboard path — one implementation"
|
||
)
|
||
verbatim = _text(VERBATIM_JS)
|
||
assert "function selectAllInFallbackField" in verbatim
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# shared.js parity
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def test_shared_page_reaches_the_same_globals_with_a_plain_link() -> None:
|
||
"""Parity is a hard requirement: the shared renderer calls the SAME three
|
||
globals in the SAME band and order as the chat page's restore, and passes
|
||
a plain ``/document.html?source=&path=`` href with NO modal (that page has
|
||
none, and the documents API is admin-gated)."""
|
||
js = _text(SHARED_JS)
|
||
body = _fn(js, "renderSharedMessage")
|
||
assert "hydrateVerbatim(" in body
|
||
assert "appendUnlocatedVerbatim(wrap, m.answer_text || [], SHARED_VERBATIM_OPTS)" in body
|
||
assert "enhanceAnswerCodeBlocks(wrap)" in body
|
||
order = [
|
||
body.index("addAnswerImage(wrap, m.answer_image)"),
|
||
body.index("hydrateVerbatim("),
|
||
body.index("appendUnlocatedVerbatim("),
|
||
body.index("addSources(wrap, m.sources)"),
|
||
]
|
||
assert order == sorted(order)
|
||
opts = js[
|
||
js.index("const SHARED_VERBATIM_OPTS") : js.index(
|
||
"};", js.index("const SHARED_VERBATIM_OPTS")
|
||
)
|
||
]
|
||
assert "/document.html?source=" in opts and "encodeURIComponent(" in opts
|
||
assert "onOpen" not in opts, "no modal on the shared page"
|
||
assert "openDocumentModal" not in js
|
||
assert "document-modal" not in js
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# styles.css: the states the JS adds stay inside the phase-182 block
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def _css_block() -> str:
|
||
css = _text(STYLES_CSS)
|
||
start = css.index(BLOCK_BEGIN)
|
||
end = css.index(BLOCK_END, start)
|
||
return css[start : end + len(BLOCK_END)]
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"selector", [".verbatim-body", ".verbatim-text", ".verbatim-open", ".verbatim-more"]
|
||
)
|
||
def test_the_task_07_dom_selectors_are_declared_in_the_phase_182_block(
|
||
selector: str,
|
||
) -> None:
|
||
"""The DOM the panel builder really produces is styled INSIDE the labelled
|
||
region (so task 06's palette-only and contiguity pins keep holding): the
|
||
body is the flex row the gutter's ``flex: 0 0 auto`` was written for, and
|
||
the viewer link + the truncation line are themed, not unstyled."""
|
||
block = _css_block()
|
||
rules = re.findall(r"(?m)^" + re.escape(selector) + r" \{([^}]*)\}", block)
|
||
assert rules, f"{selector} must be declared inside the phase-182 block"
|
||
# task 06's `_body` idiom: task 07 extends a rule with its own declaration
|
||
# line (a second block for the same selector), so every body counts.
|
||
body = " ".join(rules)
|
||
assert not re.search(r"#[0-9a-fA-F]{3,8}\b", body), "no hex colour"
|
||
for literal in ("rgb(", "rgba(", "hsl(", "hsla("):
|
||
assert literal not in body, f"{selector} must stay palette-only ({literal})"
|
||
assert not re.search(r":\s*(?:white|black|#fff|#000)\s*[;}]", body), (
|
||
f"{selector} must not name a colour"
|
||
)
|
||
if selector == ".verbatim-body":
|
||
assert "display: flex" in body, "the gutter + text row the sticky gutter needs"
|
||
if selector == ".verbatim-body":
|
||
assert "display: flex" in body, "the gutter + text row the sticky gutter needs"
|
||
if selector == ".verbatim-open":
|
||
assert "text-decoration: underline" in body, "a link is not colour-only"
|
||
assert "var(--ink-soft)" in body, "the link is themed, not unstyled"
|
||
if selector == ".verbatim-more":
|
||
assert "var(--ink-soft)" in body and "var(--line)" in body
|
||
|
||
|
||
def test_the_viewer_link_reaches_the_mobile_touch_floor() -> None:
|
||
"""PLAN §7's 44px tap target applies to the panel's link too — it lives in
|
||
the ``max-width: 640px`` block beside the two Copy controls."""
|
||
css = _text(STYLES_CSS)
|
||
mobile = re.search(r"@media \(max-width: 640px\) \{([\s\S]*?)\n\}", css)
|
||
assert mobile, "the 640px block must exist"
|
||
hit = re.search(r"\.verbatim-open \{([^}]*)\}", mobile.group(1))
|
||
assert hit and "min-height: 44px" in hit.group(1)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# node: the panel's pure logic, run against the real file
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
_HARNESS = """
|
||
const fs = require("fs");
|
||
const src = fs.readFileSync(process.argv[2], "utf8");
|
||
const api = new Function(
|
||
src +
|
||
"\\nreturn { verbatimCaptionText, verbatimGutter, verbatimPanelLabel," +
|
||
" verbatimIdentity, verbatimSplitLocator, verbatimDocumentHref," +
|
||
" isVerbatimBlock, verbatimBlocks, matchVerbatimBlock, verbatimUnresolvedCopy," +
|
||
" verbatimBadgeTitle };"
|
||
)();
|
||
const cases = JSON.parse(fs.readFileSync(process.argv[3], "utf8"));
|
||
const out = {};
|
||
for (const [name, body] of Object.entries(cases)) {
|
||
try {
|
||
// `with` so the case bodies read the panel API as plain names (the file
|
||
// itself is sloppy-mode classic-script code, and the harness is its
|
||
// own scope — no strict-mode conflict).
|
||
const fn = new Function("api", "with (api) { return (" + body + "); }");
|
||
out[name] = { ok: true, value: fn(api) };
|
||
} catch (err) {
|
||
out[name] = { ok: false, error: String(err && err.message ? err.message : err) };
|
||
}
|
||
}
|
||
process.stdout.write(JSON.stringify(out));
|
||
"""
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def tmp_dir(tmp_path_factory: pytest.TempPathFactory) -> Path:
|
||
return tmp_path_factory.mktemp("verbatim")
|
||
|
||
|
||
def _node(cases: dict[str, str], tmp_dir: Path) -> dict[str, object]:
|
||
"""Run *cases* (JS bodies receiving the panel API) under node against the
|
||
real ``verbatim.js`` — the pure half needs no DOM."""
|
||
script = tmp_dir / "harness.cjs"
|
||
script.write_text(_HARNESS, encoding="utf-8")
|
||
case_file = tmp_dir / "cases.json"
|
||
case_file.write_text(
|
||
# the case bodies are plain JS EXPRESSIONS evaluated with the panel
|
||
# API in scope (the harness wraps them, see _HARNESS)
|
||
json.dumps(dict(cases)),
|
||
encoding="utf-8",
|
||
)
|
||
proc = subprocess.run(
|
||
["node", str(script), str(VERBATIM_JS), str(case_file)],
|
||
capture_output=True,
|
||
text=True,
|
||
timeout=60,
|
||
check=False,
|
||
)
|
||
assert proc.returncode == 0, proc.stderr
|
||
return json.loads(proc.stdout)
|
||
|
||
|
||
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
|
||
def test_caption_prints_the_servers_extent(tmp_dir: Path) -> None:
|
||
"""The caption is the SERVER's resolved extent (LOCKED A2 made visible):
|
||
a slice reads ``path · lines 12\u201340 of 210``, a whole document reads
|
||
``path · all 210 lines``, a CAPPED block is never labelled "all" even when
|
||
it starts at line 1, and a garbage count degrades to the bare path rather
|
||
than printing a lie."""
|
||
out = _node(
|
||
{
|
||
"slice": """verbatimCaptionText({ source: "homelab", path: "a.md",
|
||
start_line: 12, end_line: 40, total_lines: 210 })""",
|
||
"whole": """verbatimCaptionText({ source: "homelab", path: "a.md",
|
||
start_line: 1, end_line: 210, total_lines: 210 })""",
|
||
"capped": """verbatimCaptionText({ source: "homelab", path: "a.md",
|
||
start_line: 1, end_line: 30, total_lines: 210, truncated: true })""",
|
||
"no_total": """verbatimCaptionText({ source: "homelab", path: "a.md",
|
||
start_line: 3, end_line: 4 })""",
|
||
"garbage": """verbatimCaptionText({ source: "homelab", path: "a.md",
|
||
start_line: "x", end_line: null })""",
|
||
"label": """verbatimPanelLabel({ source: "homelab", path: "a.md",
|
||
start_line: 12, end_line: 40 })""",
|
||
"label_whole": """verbatimPanelLabel({ source: "homelab", path: "a.md",
|
||
start_line: 1, end_line: 1 })""",
|
||
"href": """verbatimDocumentHref("src a", "dir/x y.md")""",
|
||
},
|
||
tmp_dir,
|
||
)
|
||
assert out["slice"] == {"ok": True, "value": "homelab/a.md \u00b7 lines 12\u201340 of 210"}
|
||
assert out["whole"] == {"ok": True, "value": "homelab/a.md \u00b7 all 210 lines"}
|
||
assert out["capped"] == {"ok": True, "value": "homelab/a.md \u00b7 lines 1\u201330 of 210"}
|
||
assert out["no_total"] == {"ok": True, "value": "homelab/a.md \u00b7 lines 3\u20134"}
|
||
assert out["garbage"] == {"ok": True, "value": "homelab/a.md"}
|
||
assert out["label"] == {
|
||
"ok": True,
|
||
"value": "Verbatim text from homelab/a.md, lines 12\u201340",
|
||
}
|
||
assert out["label_whole"] == {
|
||
"ok": True,
|
||
"value": "Verbatim text from homelab/a.md, line 1",
|
||
}, "one line reads `line N`, never `lines 1–1`"
|
||
# SEC: the href is built from ENCODED halves (the `documentUrl` parity —
|
||
# the whole path is one query value, slashes included, and FastAPI decodes
|
||
# it back), and it is always a same-origin relative URL.
|
||
assert out["href"] == {
|
||
"ok": True,
|
||
"value": "/document.html?source=src%20a&path=dir%2Fx%20y.md",
|
||
}
|
||
|
||
|
||
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
|
||
def test_gutter_numbers_the_servers_lines_and_not_the_marker(tmp_dir: Path) -> None:
|
||
"""One gutter row per line of the server text (so the rows align with the
|
||
``<pre>``), numbered from the server's ``start_line``. The truncation
|
||
marker the cap appended is a real line of the panel but NOT a line of the
|
||
file, so it gets a blank cell (an empty cell keeps the rows aligned \u2014 the
|
||
gutter never claims a coordinate the file does not have)."""
|
||
out = _node(
|
||
{
|
||
"range": """verbatimGutter({ text: "one\\ntwo\\nthree", start_line: 12,
|
||
end_line: 14 }).map(r => r.number).join("|")""",
|
||
"truncated": """verbatimGutter({ text: "one\\ntwo\\n\\u2026truncated\\u2026",
|
||
start_line: 1, end_line: 2, truncated: true })
|
||
.map(r => JSON.stringify(r.number)).join("|")""",
|
||
"unnumbered": """verbatimGutter({ text: "a\\nb" }).map(r => r.number).join("|")""",
|
||
},
|
||
tmp_dir,
|
||
)
|
||
assert out["range"] == {"ok": True, "value": "12|13|14"}
|
||
assert out["truncated"] == {"ok": True, "value": '"1"|"2"|" "'}
|
||
assert out["unnumbered"] == {"ok": True, "value": " | "}
|
||
|
||
|
||
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
|
||
def test_match_rule_exact_then_unique_path_then_none(tmp_dir: Path) -> None:
|
||
"""The ONE hydration rule: the canonical locator first; else exactly ONE
|
||
block for the same ``source/path`` (the model's range was wrong \u2014 the panel
|
||
shows the server's extent); else null, INCLUDING the ambiguous case (two
|
||
blocks, same file, different ranges \u2014 never guess)."""
|
||
out = _node(
|
||
{
|
||
"exact": """(() => { const b = [
|
||
{ source: "h", path: "a.md", locator: "h/a.md#12-40" },
|
||
{ source: "h", path: "a.md", locator: "h/a.md#1-5" }];
|
||
return matchVerbatimBlock(b, "h/a.md#1-5").locator; })()""",
|
||
"path_only": """(() => { const b = [{ source: "h", path: "a.md",
|
||
locator: "h/a.md#1-9" }];
|
||
return matchVerbatimBlock(b, "h/a.md#99-120").locator; })()""",
|
||
"whole_doc_path_only": """(() => { const b = [{ source: "h", path: "a.md",
|
||
locator: "h/a.md" }];
|
||
return matchVerbatimBlock(b, "h/a.md").locator; })()""",
|
||
"ambiguous": """(() => { const b = [
|
||
{ source: "h", path: "a.md", locator: "h/a.md#1-9" },
|
||
{ source: "h", path: "a.md", locator: "h/a.md#20-29" }];
|
||
return matchVerbatimBlock(b, "h/a.md#7-8"); })()""",
|
||
"none": """matchVerbatimBlock([], "h/a.md#1-2")""",
|
||
"other_path": """matchVerbatimBlock([{ source: "h", path: "b.md",
|
||
locator: "h/b.md" }], "h/a.md")""",
|
||
"split": """JSON.stringify(verbatimSplitLocator("h/a.md#12-40"))""",
|
||
"split_no_range": """JSON.stringify(verbatimSplitLocator("h/a.md"))""",
|
||
},
|
||
tmp_dir,
|
||
)
|
||
assert out["exact"] == {"ok": True, "value": "h/a.md#1-5"}
|
||
assert out["path_only"] == {"ok": True, "value": "h/a.md#1-9"}
|
||
assert out["whole_doc_path_only"] == {"ok": True, "value": "h/a.md"}
|
||
assert out["ambiguous"] == {"ok": True, "value": None}
|
||
assert out["none"] == {"ok": True, "value": None}
|
||
assert out["other_path"] == {"ok": True, "value": None}
|
||
assert out["split"] == {
|
||
"ok": True,
|
||
"value": '{"identity":"h/a.md","hasRange":true}',
|
||
}
|
||
assert out["split_no_range"] == {
|
||
"ok": True,
|
||
"value": '{"identity":"h/a.md","hasRange":false}',
|
||
}
|
||
|
||
|
||
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
|
||
def test_malformed_blocks_are_dropped_and_never_throw(tmp_dir: Path) -> None:
|
||
"""A malformed / missing ``answer_text`` is a NON-event: every odd shape
|
||
(null, a bare object, a block without text, a block without a path,
|
||
numbers-as-strings) is filtered out instead of throwing \u2014 hydration stays
|
||
honest for the placeholders instead of painting garbage."""
|
||
out = _node(
|
||
{
|
||
"undefined_blocks": """verbatimBlocks(undefined).length""",
|
||
"not_an_array": """verbatimBlocks({ length: 1, 0: { source: "h", path: "a.md",
|
||
text: "x" } }).length""",
|
||
"mixed": """verbatimBlocks([null, 7, "s", {}, { source: "", path: "a.md", text: "x" },
|
||
{ source: "h", path: "", text: "x" },
|
||
{ source: "h", path: "a.md" },
|
||
{ source: "h", path: "a.md", text: "" }]).length""",
|
||
"identity_with_empty_text": """isVerbatimBlock({ source: "h",
|
||
path: "a.md", text: "" })""",
|
||
"identity": """verbatimIdentity("homelab", "runbooks/a.md")""",
|
||
"unresolved_copy": """verbatimUnresolvedCopy()""",
|
||
"badge_title": """verbatimBadgeTitle()""",
|
||
},
|
||
tmp_dir,
|
||
)
|
||
assert out["undefined_blocks"] == {"ok": True, "value": 0}
|
||
assert out["not_an_array"] == {"ok": True, "value": 0}
|
||
# only the last entry survives: the others miss source/path/text.
|
||
assert out["mixed"] == {"ok": True, "value": 1}
|
||
assert out["identity_with_empty_text"] == {"ok": True, "value": True}
|
||
assert out["identity"] == {"ok": True, "value": "homelab/runbooks/a.md"}
|
||
assert out["unresolved_copy"] == {"ok": True, "value": UNRESOLVED_COPY}
|
||
assert out["badge_title"] == {"ok": True, "value": BADGE_TITLE}
|