phase: 182_verbatim_show_text
Build and Push Containers / build-and-push-app (push) Successful in 1m46s
Build and Push Containers / build-and-push-db (push) Successful in 12s

**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`).
This commit is contained in:
2026-10-15 08:33:12 -04:00
parent 814a400e6d
commit b34b8ca672
104 changed files with 10852 additions and 203 deletions
+568 -34
View File
@@ -73,9 +73,17 @@ from app.models import Document, GitSource
from app.rag import agent
from app.rag.agent import (
AGENT_TOOLS,
MISSING_SHOW_TEXT_ARGS,
READ_TRUNCATION_NOTICE,
SHOW_TEXT_ALREADY_SHOWN,
SHOW_TEXT_BLOCK_CAP,
SHOW_TEXT_NOT_A_TEXT_DOC,
SHOW_TEXT_TOOL,
SHOWN_TEXT_TRUNCATION_NOTICE,
UNKNOWN_TOOL,
AgentHolder,
MalformedReplyError,
agent_tools,
run_agent,
)
from app.rag.llm import (
@@ -98,6 +106,20 @@ def _settings(**kwargs: Any) -> Settings:
return Settings(**kwargs) # pyright: ignore[reportCallIssue]
#: Phase 182 (task 03) pin update, deliberate: ``answer_text``
#: (``BOR_ANSWER_TEXT``) defaults ON, so the default grounded offer is
#: the three base tools (element-for-element ``AGENT_TOOLS``, the
#: byte-identity pinned separately by ``test_agent_tools_names_and_
#: parameters``) plus the ``show_text`` display tool — every
#: "tools stay offered" pin in this module now expects this list. The
#: pre-phase offer is still exactly ``AGENT_TOOLS`` with
#: ``answer_text=False`` and ``answer_images=False`` (pinned in the
#: phase-182 offer-matrix tests below). With ``answer_images=True`` the
#: offer is ``[*AGENT_TOOLS, SHOW_IMAGE_TOOL, SHOW_TEXT_TOOL]`` (pinned
#: in ``tests/unit/test_image_chips_and_answers.py``).
_OFFER = [*AGENT_TOOLS, SHOW_TEXT_TOOL]
#: The fixture documents' fixed creation date (phase 106, D5): the
#: agent formats ``doc.created_at`` on the ``read`` result's second
#: line — the detached fixture rows carry it exactly as the NOT NULL
@@ -547,9 +569,9 @@ def test_ls_then_read_then_answer(monkeypatch: pytest.MonkeyPatch) -> None:
# Phase 45: no per-tool budgets — the tools stay offered on every
# request (the round cap, not spent budgets, bounds the loop), so
# the answer request still carries them (2 rounds < default cap 10).
assert llm.requests[0][1] == AGENT_TOOLS
assert llm.requests[1][1] == AGENT_TOOLS
assert llm.requests[2][1] == AGENT_TOOLS
assert llm.requests[0][1] == _OFFER
assert llm.requests[1][1] == _OFFER
assert llm.requests[2][1] == _OFFER
assert len(llm.requests) == 3
# read splits the combined form at the FIRST slash — one exact
@@ -855,7 +877,7 @@ def test_ls_scoped_known_source_with_zero_docs_counts(
asyncio.run(_run(llm, holder, _settings()))
assert llm.requests[1][0][3]["content"].startswith("Homelab — 0 documents, 0 folders:")
assert holder.tool_calls == 1 # an executed ls, not a refusal
assert llm.requests[1][1] == AGENT_TOOLS
assert llm.requests[1][1] == _OFFER
def test_ls_scoped_unknown_source_refused(monkeypatch: pytest.MonkeyPatch) -> None:
@@ -873,7 +895,7 @@ def test_ls_scoped_unknown_source_refused(monkeypatch: pytest.MonkeyPatch) -> No
assert llm.requests[1][0][3]["content"].startswith(
agent.NO_SOURCE_NOT_A_DIRECTORY.format(scope="Ghost")
)
assert llm.requests[1][1] == AGENT_TOOLS # rejected → tools stay offered
assert llm.requests[1][1] == _OFFER # rejected → tools stay offered
def test_ls_path_like_scope_unknown_source_gets_no_source_refusal(
@@ -900,7 +922,7 @@ def test_ls_path_like_scope_unknown_source_gets_no_source_refusal(
assert llm.requests[1][0][3]["content"].startswith(
agent.NO_SOURCE_NOT_A_DIRECTORY.format(scope="app")
)
assert llm.requests[1][1] == AGENT_TOOLS # rejected → tools stay offered
assert llm.requests[1][1] == _OFFER # rejected → tools stay offered
def test_ls_dot_scope_gets_not_a_directory_teaching_refusal(
@@ -921,7 +943,7 @@ def test_ls_dot_scope_gets_not_a_directory_teaching_refusal(
assert llm.requests[1][0][3]["content"].startswith(
agent.NO_SOURCE_NOT_A_DIRECTORY.format(scope=".")
)
assert llm.requests[1][1] == AGENT_TOOLS # rejected → tools stay offered
assert llm.requests[1][1] == _OFFER # rejected → tools stay offered
def test_ls_unknown_top_level_folder_gets_not_a_folder_teaching(
@@ -958,7 +980,7 @@ def test_ls_unknown_top_level_folder_gets_not_a_folder_teaching(
"'Homelab/netwoking' is not a folder — Homelab has: "
"backups/ containers/ networking/"
)
assert llm.requests[1][1] == AGENT_TOOLS # rejected → tools stay offered
assert llm.requests[1][1] == _OFFER # rejected → tools stay offered
def test_ls_unknown_nested_folder_gets_not_a_folder_with_nested_parent(
@@ -1527,7 +1549,7 @@ def test_read_bare_source_name_refused_without_db(monkeypatch: pytest.MonkeyPatc
assert llm.requests[1][0][3]["content"].startswith(
"No document at 'Homelab' — check the ls output."
)
assert llm.requests[1][1] == AGENT_TOOLS
assert llm.requests[1][1] == _OFFER
def test_read_unknown_path_refused_echoing_argument(
@@ -1549,7 +1571,7 @@ def test_read_unknown_path_refused_echoing_argument(
assert llm.requests[1][0][3]["content"].startswith(
"No document at 'S/ghost.md' — check the ls output."
)
assert llm.requests[1][1] == AGENT_TOOLS # tools stay offered (cap bounds)
assert llm.requests[1][1] == _OFFER # tools stay offered (cap bounds)
# ---------- read/grep: the "did you mean …?" suggestions (phase 72, task 02) ----------
@@ -1649,7 +1671,7 @@ def test_read_bare_path_exact_match_gets_did_you_mean(
"No document at 'active/container_caddy/caddy.md' — "
"did you mean 'Homelab/active/container_caddy/caddy.md'?"
)
assert llm.requests[1][1] == AGENT_TOOLS # rejected → tools stay offered
assert llm.requests[1][1] == _OFFER # rejected → tools stay offered
def test_read_bare_path_suffix_match_gets_did_you_mean(
@@ -1767,7 +1789,7 @@ def test_read_bare_filename_without_slash_keeps_no_db_refusal(
assert llm.requests[1][0][3]["content"].startswith(
"No document at 'caddy.md' — check the ls output."
)
assert llm.requests[1][1] == AGENT_TOOLS
assert llm.requests[1][1] == _OFFER
def test_read_bare_path_of_suggested_doc_gets_suggestion_then_succeeds(
@@ -1850,7 +1872,7 @@ def test_read_missing_arguments_refused(
asyncio.run(_run(llm, holder, _settings()))
assert holder.read_docs == [] and holder.tool_calls == 0
assert llm.requests[1][0][3]["content"].startswith(agent.MISSING_READ_ARGS)
assert llm.requests[1][1] == AGENT_TOOLS
assert llm.requests[1][1] == _OFFER
def test_reading_a_suggested_seed_doc_adds_its_full_text(
@@ -1887,7 +1909,7 @@ def test_reading_a_suggested_seed_doc_adds_its_full_text(
assert llm.requests[1][0][3]["content"].startswith(
"Document Homelab/kubernetes.md:\ndate: 2024-06-15\nK8S-CONTENT"
)
assert llm.requests[1][1] == AGENT_TOOLS
assert llm.requests[1][1] == _OFFER
def test_re_reading_a_suggested_doc_is_deduped(monkeypatch: pytest.MonkeyPatch) -> None:
@@ -1938,7 +1960,7 @@ def test_re_reading_a_suggested_doc_is_deduped(monkeypatch: pytest.MonkeyPatch)
assert llm.requests[2][0][5]["content"].startswith(agent.ALREADY_IN_CONTEXT)
# Rejected → the tools are still offered on the next request (the
# round cap is the only bound).
assert llm.requests[2][1] == AGENT_TOOLS
assert llm.requests[2][1] == _OFFER
def test_reading_an_already_read_doc_is_deduped(monkeypatch: pytest.MonkeyPatch) -> None:
@@ -1960,7 +1982,7 @@ def test_reading_an_already_read_doc_is_deduped(monkeypatch: pytest.MonkeyPatch)
)
assert llm.requests[2][0][5]["content"].startswith(agent.ALREADY_IN_CONTEXT)
# Rejected → the tools are still offered on the next request…
assert llm.requests[2][1] == AGENT_TOOLS
assert llm.requests[2][1] == _OFFER
# ---------- phase 95: the read cap (bounded reads, honest truncation) ----------
@@ -2359,7 +2381,7 @@ def test_bad_range_args_refused_before_any_lookup(
assert llm.requests[1][0][3]["content"].startswith(agent.BAD_RANGE_ARGS.format(
offset=2.5, limit=None
))
assert llm.requests[1][1] == AGENT_TOOLS
assert llm.requests[1][1] == _OFFER
def test_bad_range_args_beats_the_dedupe(
@@ -3678,7 +3700,7 @@ def test_unknown_tool_name_refused(monkeypatch: pytest.MonkeyPatch) -> None:
asyncio.run(_run(llm, holder, _settings()))
assert holder.read_docs == [] and holder.tool_calls == 0
assert llm.requests[1][0][3]["content"].startswith(agent.UNKNOWN_TOOL)
assert llm.requests[1][1] == AGENT_TOOLS # rejected → tools stay offered
assert llm.requests[1][1] == _OFFER # rejected → tools stay offered
# ---------- phase 145 (task 01): the origin/branch lines in the read result ----------
@@ -4062,7 +4084,7 @@ def test_grep_whole_kb_grep_style_output(monkeypatch: pytest.MonkeyPatch) -> Non
)
assert holder.tool_calls == 1
assert holder.read_docs == [] # locked A5: a grep adds no context
assert llm.requests[1][1] == AGENT_TOOLS # tools stay offered
assert llm.requests[1][1] == _OFFER # tools stay offered
def test_grep_capped_at_20_matches_in_catalog_order(
@@ -4370,7 +4392,7 @@ def test_grep_scoped_missing_document_refused(monkeypatch: pytest.MonkeyPatch) -
"No document at 'S/ghost.md' — check the ls output."
)
assert holder.tool_calls == 0 and holder.read_docs == [] # a refusal
assert llm.requests[1][1] == AGENT_TOOLS # rejected → tools stay offered
assert llm.requests[1][1] == _OFFER # rejected → tools stay offered
def test_grep_scoped_missing_path_like_doc_gets_did_you_mean(
@@ -4408,7 +4430,7 @@ def test_grep_scoped_missing_path_like_doc_gets_did_you_mean(
)
)
assert holder.tool_calls == 0 and holder.read_docs == [] # a refusal
assert llm.requests[1][1] == AGENT_TOOLS # rejected → tools stay offered
assert llm.requests[1][1] == _OFFER # rejected → tools stay offered
def test_grep_scoped_bare_source_name_refused_without_db(
@@ -4467,7 +4489,7 @@ def test_grep_missing_arguments_refused(
asyncio.run(_run(llm, holder, _settings()))
assert llm.requests[1][0][3]["content"].startswith(agent.MISSING_SEARCH_ARGS)
assert holder.tool_calls == 0 and holder.read_docs == []
assert llm.requests[1][1] == AGENT_TOOLS # rejected → tools stay offered
assert llm.requests[1][1] == _OFFER # rejected → tools stay offered
def test_grep_no_matches_whole_kb(monkeypatch: pytest.MonkeyPatch) -> None:
@@ -4577,7 +4599,7 @@ def test_relist_executes_and_counts(monkeypatch: pytest.MonkeyPatch) -> None:
# after round 1, 8 left after round 2 (10-round budget).
assert llm.requests[2][0][3]["content"] == listing + agent.TOOL_BUDGET_REMAINING.format(n=9)
assert llm.requests[2][0][5]["content"] == listing + agent.TOOL_BUDGET_REMAINING.format(n=8)
assert llm.requests[2][1] == AGENT_TOOLS # still offered (no budgets)
assert llm.requests[2][1] == _OFFER # still offered (no budgets)
def test_multi_read_executes_without_budgets(monkeypatch: pytest.MonkeyPatch) -> None:
@@ -4604,7 +4626,7 @@ def test_multi_read_executes_without_budgets(monkeypatch: pytest.MonkeyPatch) ->
assert llm.requests[2][0][5]["content"].startswith(
"Document S/b.md:\ndate: 2024-06-15\nB-CONTENT"
)
assert llm.requests[2][1] == AGENT_TOOLS # the second read was still offered
assert llm.requests[2][1] == _OFFER # the second read was still offered
# ---------- round cap (phase 45: replaces the per-tool budgets) ----------
@@ -4632,9 +4654,9 @@ def test_always_ls_bounded_by_round_cap(monkeypatch: pytest.MonkeyPatch) -> None
]
assert len(llm.requests) == 4 # 3 tool rounds + the forced answer
# The three tool rounds were offered the tools…
assert llm.requests[0][1] == AGENT_TOOLS
assert llm.requests[1][1] == AGENT_TOOLS
assert llm.requests[2][1] == AGENT_TOOLS
assert llm.requests[0][1] == _OFFER
assert llm.requests[1][1] == _OFFER
assert llm.requests[2][1] == _OFFER
# …and the forced final request carries no tools, whatever is left.
assert llm.requests[3][1] is None
# Every re-list executed and counted.
@@ -4686,9 +4708,9 @@ def test_rejected_read_spam_runs_to_round_cap(monkeypatch: pytest.MonkeyPatch) -
)
asyncio.run(_run(llm, holder, _settings(agent_max_rounds=3)))
assert len(llm.requests) == 4 # 3 rejected rounds + the forced answer
assert llm.requests[0][1] == AGENT_TOOLS
assert llm.requests[1][1] == AGENT_TOOLS
assert llm.requests[2][1] == AGENT_TOOLS
assert llm.requests[0][1] == _OFFER
assert llm.requests[1][1] == _OFFER
assert llm.requests[2][1] == _OFFER
assert llm.requests[3][1] is None # the forced final request: no tools
assert holder.read_docs == [] and holder.tool_calls == 0 # nothing executed
refusal = "No document at 'S/ghost.md' — check the ls output."
@@ -5101,8 +5123,8 @@ def test_round_retried_before_first_piece(
# The restart is byte-identical: same messages, same tools offered.
assert len(llm.requests) == 3
assert llm.requests[0] == llm.requests[1]
assert llm.requests[0][1] == AGENT_TOOLS
assert llm.requests[2][1] == AGENT_TOOLS # the answer round still offered
assert llm.requests[0][1] == _OFFER
assert llm.requests[2][1] == _OFFER # the answer round still offered
# The flat delay was awaited exactly once, before the retry.
assert sleeps == [2.5]
tool_logs = [r for r in caplog.records if r.getMessage().startswith("agent tool=")]
@@ -5397,7 +5419,7 @@ def test_scaffolding_only_round_gets_exactly_one_recovery(
assert pieces == [StreamPiece("content", "The clean recovery answer.")]
assert len(llm.requests) == 2 # the round + the one recovery
# The round: the original system prompt, the tools offered.
assert llm.requests[0][1] == AGENT_TOOLS
assert llm.requests[0][1] == _OFFER
assert llm.requests[0][0] == [
{"role": "system", "content": "SYSTEM_PROMPT"},
{"role": "user", "content": "QUESTION"},
@@ -5534,7 +5556,7 @@ def test_scaffolding_round_with_tool_calls_needs_no_recovery(
StreamPiece("content", "the answer after the tool"),
]
assert len(llm.requests) == 2
assert llm.requests[1][1] == AGENT_TOOLS # a normal round, not a recovery
assert llm.requests[1][1] == _OFFER # a normal round, not a recovery
for messages, _tools in llm.requests:
assert all(
m["content"] is None or agent.CORRECTION_INSTRUCTION not in m["content"]
@@ -5615,3 +5637,515 @@ def test_forced_final_scaffolding_only_yields_the_budget_fallback(
# calls — this is the fallback path, not the rejection path.
assert holder.rejected_calls == 0
assert holder.tool_calls == 1
# ---------------------------------------------------------------------------
# Phase 182 (task 03) — the 5th agent tool: ``show_text(path, offset?, limit?)``
#
# The display sibling of ``show_image`` (the phase-124 family in
# ``tests/unit/test_image_chips_and_answers.py`` is the template): the model
# NAMES a location, the SERVER shows the bytes (LOCKED A1–A3/A6). The suite
# pins the offer matrix (``agent_tools``, four base + two display tools, each
# behind its own flag), the successful result shape (the ``read``/
# ``show_image`` header contract, the SERVER's extent in the ``lines:``
# header), ``holder.shown_texts`` (the read-after-stream signal task 05 puts
# on the ``done`` frame), and EVERY refusal — each counts in nothing,
# appends nothing, and consumes a round like every other refusal.
# ---------------------------------------------------------------------------
_SHOW_SRC = "showsrc"
_SHOW_PATH = "quadlets/gufo.container"
_SHOW_IDENTITY = f"{_SHOW_SRC}/{_SHOW_PATH}"
_SHOW_CONTENT = "L1-one\nL2-two\nL3-three\nL4-four\nL5-five"
def _show_doc(content: str = _SHOW_CONTENT, *, is_image: bool = False) -> Document:
"""The fixture row ``show_text`` resolves (detached, like every
agent-suite fixture); ``is_image`` flips the A6 refusal case."""
doc = _doc(_SHOW_SRC, _SHOW_PATH, "Gufo quadlet", content)
doc.is_image = is_image
return doc
def _patch_show_doc(
monkeypatch: pytest.MonkeyPatch, doc: Document | None
) -> None:
"""One resolvable document for ``_resolve_path`` (the same
monkeypatch style as the read family: the DB accessors are the seam)."""
def _find(_db: Session, source: str, path: str) -> Document | None:
if doc is not None and (source, path) == (doc.source, doc.path):
return doc
return None
monkeypatch.setattr(agent, "find_document", _find)
def _show_text(
arguments: dict[str, Any],
holder: AgentHolder,
settings: Settings | None = None,
) -> str:
"""One ``show_text`` call through the pure ``_execute_tool`` seam (the
budget trailer is appended by ``run_agent``, never here — the raw
result strings are what this suite pins)."""
return agent._execute_tool(
_mock_session(),
ToolCallPiece(id="c1", name="show_text", arguments=arguments),
[],
holder,
settings or _settings(),
)
# --- the offer matrix (the task-03 completion pin: 4 base + 2 display) ---
def test_show_text_offer_matrix_agent_tools_by_flags() -> None:
"""``agent_tools`` offers FOUR tools with ``answer_text=False`` (the
pre-phase-182 offer byte-identical — ``show_image`` last when
offered) and FIVE with it ON (``show_text`` present, correct order
``ls, read, grep, show_image, show_text``); both flags off is
element-for-element ``AGENT_TOOLS`` — a fresh list, never the
constant."""
pre182 = agent_tools(_settings(answer_images=True, answer_text=False))
assert [t["function"]["name"] for t in pre182] == [
"ls",
"read",
"grep",
"show_image",
]
assert pre182[:3] == AGENT_TOOLS # the base three byte-identical
both = agent_tools(_settings(answer_images=True, answer_text=True))
assert [t["function"]["name"] for t in both] == [
"ls",
"read",
"grep",
"show_image",
"show_text",
]
assert both[:3] == AGENT_TOOLS
pre124 = agent_tools(_settings(answer_images=False, answer_text=False))
assert pre124 == AGENT_TOOLS # element-for-element
assert pre124 is not AGENT_TOOLS # a fresh list, never the constant
pre124.append({"junk": True}) # mutating the offer must not touch it
assert len(AGENT_TOOLS) == 3
# The shipped default: ``answer_text`` is ON (LOCKED A8), so the
# default grounded offer already carries the 5th tool.
default = agent_tools(_settings())
assert [t["function"]["name"] for t in default] == [
"ls",
"read",
"grep",
"show_text",
]
assert default == _OFFER # the module-wide offer pin stays honest
def test_show_text_tool_definition_is_the_behavior_contract() -> None:
"""The same OpenAI function shape as the other tools (one required
string ``path`` + the OPTIONAL ``offset``/``limit`` integers), and
the description IS the behavior contract (the phase-124 discipline):
verbatim display, the do-NOT-retype instruction, the server-owned
extent (clamp + past-end teaching), the ``show_image`` pointer
(A6), the bare-path teaching mirrored from ``read``, and the
one-call-at-a-time discipline."""
assert SHOW_TEXT_TOOL["type"] == "function"
fn = SHOW_TEXT_TOOL["function"]
assert fn["name"] == "show_text"
params = fn["parameters"]
assert params["type"] == "object"
assert params["required"] == ["path"]
props = params["properties"]
assert set(props) == {"path", "offset", "limit"}
assert props["path"]["type"] == "string"
assert props["offset"]["type"] == "integer"
assert props["limit"]["type"] == "integer"
# The ``read`` bare-path teaching reused verbatim (one grammar).
assert (
"A bare document path (without the source name) will not resolve."
in props["path"]["description"]
)
desc = fn["description"]
assert "EXACTLY as stored" in desc
assert "do NOT retype the content yourself" in desc
assert "`offset` (1-based first line) and `limit` (line count)" in desc
assert "overshooting `limit` is clamped" in desc
assert "refused with the true line" in desc
assert "Only text documents — for an image use `show_image`" in desc
assert "Call one tool at a time" in desc
def test_show_text_refusal_texts_are_pinned() -> None:
"""The four refusal texts are pinned (the model's self-correction
relies on stable copy), and the range problems REUSE the read
constants — one copy of each string in the codebase."""
assert MISSING_SHOW_TEXT_ARGS == "show_text requires a string argument 'path'."
assert SHOW_TEXT_NOT_A_TEXT_DOC.format(arg="s/p.png") == (
"'s/p.png' is a standalone image document — use show_image to "
"display images."
)
assert SHOW_TEXT_BLOCK_CAP.format(max=5) == (
"5 text blocks are already displayed this answer — describe them "
"instead of showing more."
)
assert SHOW_TEXT_ALREADY_SHOWN.format(locator="s/p.md#12-40") == (
"s/p.md#12-40 is already on screen this answer — say so instead "
"of showing it again."
)
# The reuse pins: the branch formats THESE constants, not copies —
# the exact same teaching strings (and echoed values) ``read`` uses.
assert agent.BAD_RANGE_ARGS.format(offset=0, limit=None) == (
"`offset` and `limit` are whole line numbers — pass integers ≥ 1 "
"(you passed offset=0, limit=None)."
)
assert agent.OFFSET_PAST_END.format(
source=_SHOW_SRC, path=_SHOW_PATH, total_lines=5, offset=9
).startswith(f"{_SHOW_IDENTITY} is 5 lines long — `offset` 9 is past the end.")
# --- the successful show: whole document, ranged, capped ---
def test_show_text_whole_document_success_shape(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Whole-document show (the taught default): result headers in the
exact ``read``/``show_image`` order (a NEW first line, then
``Document …``, ``date:``, the ``lines:`` extent, the VERBATIM
slice), ``shown_texts`` carries the one block (canonical whole-doc
locator), ``tool_calls`` counts the executed call, and the READ
bookkeeping stays untouched (a show is not a context-adder)."""
_patch_show_doc(monkeypatch, _show_doc())
holder = AgentHolder()
result = _show_text({"path": _SHOW_IDENTITY}, holder)
assert result == (
"Text shown to the user — these lines are displayed verbatim; "
"describe them, do NOT retype them.\n"
f"Document {_SHOW_IDENTITY}:\n"
"date: 2024-06-15\n"
"lines: 1-5 of 5\n"
f"{_SHOW_CONTENT}"
)
assert result.splitlines()[1].startswith("Document ") # the mock's contract
assert len(holder.shown_texts) == 1
block = holder.shown_texts[0]
assert block.locator == _SHOW_IDENTITY # whole doc → no range part
assert (block.start_line, block.end_line, block.total_lines) == (1, 5, 5)
assert block.truncated is False
assert holder.tool_calls == 1 # executed-only counting
# A show NEVER touches the read paths (it is not a context-adder).
assert holder.read_docs == []
assert holder.read_coverage == []
assert holder.read_truncations == []
def test_show_text_ranged_show_clamps_limit_in_the_lines_header(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Ranged show with an OVERSHOOTING ``limit``: the header carries the
SERVER's clamped extent (``lines: 4-5 of 5`` — the caption never
lies), the slice is exactly the displayed lines, and the canonical
locator carries the clamped range (the D7 matching key)."""
_patch_show_doc(monkeypatch, _show_doc())
holder = AgentHolder()
result = _show_text({"path": _SHOW_IDENTITY, "offset": 4, "limit": 99}, holder)
assert result == (
"Text shown to the user — these lines are displayed verbatim; "
"describe them, do NOT retype them.\n"
f"Document {_SHOW_IDENTITY}:\n"
"date: 2024-06-15\n"
"lines: 4-5 of 5\n"
"L4-four\nL5-five"
)
block = holder.shown_texts[0]
assert block.locator == f"{_SHOW_IDENTITY}#4-5"
assert (block.start_line, block.end_line, block.total_lines) == (4, 5, 5)
assert holder.tool_calls == 1
def test_show_text_capped_block_carries_marker_and_notice(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Capped show (``settings.show_text_max_chars``): the slice is cut
at a LINE boundary, the shared ``TRUNCATION_MARKER`` rides the block,
the ``lines:`` extent SHRINKS to the last displayed line (never the
asked extent), and the pinned notice names that the rest is NOT
displayed and the user opens the document (no re-show loop implied)."""
_patch_show_doc(monkeypatch, _show_doc())
holder = AgentHolder()
result = _show_text(
{"path": _SHOW_IDENTITY},
holder,
_settings(show_text_max_chars=13),
)
assert TRUNCATION_MARKER in result
assert result.endswith(
SHOWN_TEXT_TRUNCATION_NOTICE.format(source=_SHOW_SRC, path=_SHOW_PATH)
)
block = holder.shown_texts[0]
assert block.truncated is True
# The budget (13 chars) lands exactly after line 2 — the caption
# shrinks to the lines ACTUALLY shown (never the asked 1-5).
assert block.end_line == 2
assert block.locator == f"{_SHOW_IDENTITY}#1-2" # cap-derived range
assert f"lines: 1-2 of 5\nL1-one\nL2-two\n{TRUNCATION_MARKER}" in result
assert holder.tool_calls == 1 # a capped show is still a successful call
def test_show_text_origin_lines_ridge_the_header_like_read(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The phase-145 origin contract: a git-sourced show carries the
``origin:``/``branch:`` lines immediately after ``date:`` and BEFORE
``lines:`` (the same ``_origin_lines`` block ``read`` renders; the
second line stays ``Document …:``)."""
doc = _show_doc()
_patch_show_doc(monkeypatch, doc)
def _factory() -> Session:
from app.models import GitSource
rows = [GitSource(url="https://github.com/owner/showsrc", kind="git", branch="main")]
mock = cast("Session", MagicMock())
mock.__enter__ = MagicMock(return_value=mock)
mock.__exit__ = MagicMock(return_value=False)
mock.scalars = MagicMock(return_value=MagicMock(all=MagicMock(return_value=rows)))
return mock
holder = AgentHolder()
result = agent._execute_tool(
_factory(),
ToolCallPiece(id="c1", name="show_text", arguments={"path": _SHOW_IDENTITY}),
[],
holder,
_settings(),
)
lines = result.splitlines()
assert lines[1] == f"Document {_SHOW_IDENTITY}:"
assert lines[2] == "date: 2024-06-15"
assert lines[3] == "origin: https://github.com/owner/showsrc"
assert lines[4] == "branch: main"
assert lines[5] == "lines: 1-5 of 5"
# --- the refusal matrix: each counts in nothing, appends nothing ---
def _assert_refusal(holder: AgentHolder) -> None:
assert holder.tool_calls == 0
assert holder.shown_texts == []
assert holder.read_docs == []
assert holder.read_coverage == []
def test_show_text_missing_or_non_string_path_refused() -> None:
"""Missing, blank and non-string ``path`` → ``MISSING_SHOW_TEXT_ARGS``
(the ``MISSING_SHOW_ARGS`` shape); counts in nothing."""
holder = AgentHolder()
for arguments in ({}, {"path": " "}, {"path": None}, {"path": 42}):
assert _show_text(arguments, holder) == MISSING_SHOW_TEXT_ARGS
_assert_refusal(holder)
def test_show_text_unknown_path_gets_the_did_you_mean_teaching(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The phase-72 teaching, REUSED unchanged: a bare argument keeps the
no-DB line, a path-like argument matching one indexed ``path`` gets
the combined-identity suggestion — nothing displayed either way."""
_patch_show_doc(monkeypatch, None)
doc = _show_doc()
monkeypatch.setattr(
agent,
"find_path_candidates",
lambda db, arg: [(_SHOW_SRC, _SHOW_PATH, doc.title)],
)
holder = AgentHolder()
result = _show_text({"path": "quadlets/gufo.container"}, holder)
assert result == agent.NO_DOCUMENT_DID_YOU_MEAN.format(
arg="quadlets/gufo.container", source=_SHOW_SRC, path=_SHOW_PATH
)
assert _show_text({"path": "nope"}, holder) == (
"No document at 'nope' — check the ls output."
)
_assert_refusal(holder)
def test_show_text_on_an_image_doc_is_refused_never_converted(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""LOCKED A6: a standalone IMAGE row is refused with the
``show_image`` pointer — never silently converted (the exact mirror
of ``show_image``'s ``NOT_AN_IMAGE_DOC``); counts in nothing."""
_patch_show_doc(monkeypatch, _show_doc(is_image=True))
holder = AgentHolder()
result = _show_text({"path": _SHOW_IDENTITY}, holder)
assert result == SHOW_TEXT_NOT_A_TEXT_DOC.format(arg=_SHOW_IDENTITY)
_assert_refusal(holder)
def test_show_text_bad_range_args_reuse_the_read_teaching(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A non-whole/``< 1`` ``offset``/``limit`` → :data:`BAD_RANGE_ARGS`
with the values echoed — the SAME constant the ``read`` branch
formats (one copy of the string), fired before any display."""
_patch_show_doc(monkeypatch, _show_doc())
holder = AgentHolder()
assert _show_text({"path": _SHOW_IDENTITY, "offset": 0}, holder) == (
agent.BAD_RANGE_ARGS.format(offset=0, limit=None)
)
assert _show_text({"path": _SHOW_IDENTITY, "offset": 1, "limit": "x"}, holder) == (
agent.BAD_RANGE_ARGS.format(offset=1, limit="x")
)
_assert_refusal(holder)
def test_show_text_offset_past_end_names_the_true_total(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""An ``offset`` past the last line → :data:`OFFSET_PAST_END` (the
SAME teaching as ``read``) naming the document's TRUE line count;
counts in nothing, a corrected retry still proceeds."""
_patch_show_doc(monkeypatch, _show_doc())
holder = AgentHolder()
result = _show_text({"path": _SHOW_IDENTITY, "offset": 9}, holder)
assert result == agent.OFFSET_PAST_END.format(
source=_SHOW_SRC, path=_SHOW_PATH, total_lines=5, offset=9
)
assert "is 5 lines long" in result
_assert_refusal(holder)
def test_show_text_empty_document_is_refused_like_past_end(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""An EMPTY document has nothing to display: the resolver's
past-the-end refusal carries the true total (``0 lines long``) —
never an empty block, never a crash."""
_patch_show_doc(monkeypatch, _show_doc(content=""))
holder = AgentHolder()
result = _show_text({"path": _SHOW_IDENTITY}, holder)
assert result == agent.OFFSET_PAST_END.format(
source=_SHOW_SRC, path=_SHOW_PATH, total_lines=0, offset=1
)
_assert_refusal(holder)
def test_show_text_block_cap_refuses_second_show(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The per-answer block budget (server-enforced): with
``show_text_max_blocks=1`` a SECOND, DIFFERENT show is refused with
the cap copy — the first block stays displayed, the refusal counts
in nothing."""
doc_a = _show_doc()
doc_b = _doc(_SHOW_SRC, "quadlets/other.container", "Other", "B1\nB2")
docs = {doc_a.path: doc_a, doc_b.path: doc_b}
def _find(_db: Session, source: str, path: str) -> Document | None:
return docs.get(path) if source == _SHOW_SRC else None
monkeypatch.setattr(agent, "find_document", _find)
holder = AgentHolder()
settings = _settings(show_text_max_blocks=1)
first = _show_text({"path": _SHOW_IDENTITY}, holder, settings)
assert first.startswith("Text shown to the user —")
second = _show_text({"path": f"{_SHOW_SRC}/quadlets/other.container"}, holder, settings)
assert second == SHOW_TEXT_BLOCK_CAP.format(max=1)
assert holder.tool_calls == 1 # the refusal counts in nothing
assert len(holder.shown_texts) == 1 # still exactly the first block
def test_show_text_zero_knobs_refuse_the_first_call(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The ``0`` kill switches (D8): ``show_text_max_blocks=0`` refuses
the FIRST call with the cap copy, and ``show_text_max_chars=0`` (the
inert per-block budget — nothing could display) refuses with the very
same copy instead of reaching the resolver's raise. The tool stays
OFFERED either way (the offer is ``answer_text``-gated only); counts
in nothing."""
_patch_show_doc(monkeypatch, _show_doc())
holder = AgentHolder()
assert _show_text(
{"path": _SHOW_IDENTITY}, holder, _settings(show_text_max_blocks=0)
) == SHOW_TEXT_BLOCK_CAP.format(max=0)
assert _show_text(
{"path": _SHOW_IDENTITY}, holder, _settings(show_text_max_chars=0)
) == SHOW_TEXT_BLOCK_CAP.format(max=0)
_assert_refusal(holder)
# The offer is untouched by the budgets (the gate is the flag).
assert [t["function"]["name"] for t in agent_tools(
_settings(show_text_max_blocks=0, show_text_max_chars=0)
)] == ["ls", "read", "grep", "show_text"]
def test_show_text_duplicate_identical_show_refused(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The canonical-locator dedupe: showing the EXACT same block twice
is refused (``SHOW_TEXT_ALREADY_SHOWN`` names the locator) — the
first block stays on screen, the refusal counts in nothing."""
_patch_show_doc(monkeypatch, _show_doc())
holder = AgentHolder()
first = _show_text({"path": _SHOW_IDENTITY, "offset": 2, "limit": 2}, holder)
assert first.startswith("Text shown to the user —")
second = _show_text({"path": _SHOW_IDENTITY, "offset": 2, "limit": 2}, holder)
assert second == SHOW_TEXT_ALREADY_SHOWN.format(locator=f"{_SHOW_IDENTITY}#2-3")
assert holder.tool_calls == 1
assert len(holder.shown_texts) == 1
def test_show_text_refused_when_answer_text_flag_off(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Defensive posture: with ``answer_text=False`` the tool was never
OFFERED, so a call anyway is answered like an un-offered name (the
tools-off posture — :data:`UNKNOWN_TOOL`) — counts in nothing,
displays nothing (the escape hatch stays byte-identical everywhere)."""
_patch_show_doc(monkeypatch, _show_doc())
holder = AgentHolder()
result = _show_text({"path": _SHOW_IDENTITY}, holder, _settings(answer_text=False))
assert result == UNKNOWN_TOOL
_assert_refusal(holder)
# --- the scripted turn (the completion-criteria wiring pin) ---
def test_show_text_scripted_turn_returns_the_slice_to_the_model(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Completion pin: with ``answer_text=True`` a scripted agent turn
returns a result whose 2nd line starts ``Document `` and whose
``lines:`` header matches the slice actually returned; the block
rides the holder for the done frame (task 05) and the tool-offer
on the request carried the 5th tool."""
_patch_show_doc(monkeypatch, _show_doc())
holder = AgentHolder()
llm = ScriptedLLM(
[ToolCallPiece(id="c1", name="show_text", arguments={"path": _SHOW_IDENTITY, "offset": 3})],
[StreamPiece("content", "ans")],
)
asyncio.run(_run(llm, holder, _settings())) # answer_text default ON
assert llm.requests[0][1] == _OFFER # the grounded turn offers show_text
content = llm.requests[1][0][3]["content"]
assert content.endswith(agent._budget_note(9)) # the trailer rides too
result = content.removesuffix(agent._budget_note(9))
lines = result.splitlines()
assert lines[0] == (
"Text shown to the user — these lines are displayed verbatim; "
"describe them, do NOT retype them."
)
assert lines[1].startswith("Document ")
assert lines[3] == "lines: 3-5 of 5" # offset-only: to the END, clamped
assert result.endswith("L3-three\nL4-four\nL5-five") # the slice verbatim
assert len(holder.shown_texts) == 1
assert holder.shown_texts[0].locator == f"{_SHOW_IDENTITY}#3-5"
assert holder.tool_calls == 1
+154 -6
View File
@@ -23,7 +23,7 @@ from app.config import Settings
from app.main import app as fastapi_app
from app.models import Document, KbOverview, QueryLog
from app.rag import agent as rag_agent
from app.rag.agent import AGENT_TOOLS
from app.rag.agent import AGENT_TOOLS, SHOW_TEXT_TOOL
from app.rag.llm import StreamPiece, ToolCallPiece
from app.rag.prompts import SUGGEST_INTRO, build_deflect_prompt
from app.rag.retriever import RetrievedChunk, weak_hit_titles
@@ -1135,10 +1135,13 @@ def test_endpoint_grounded_turn_runs_agent_loop_with_tools(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Phase 37: a grounded endpoint turn runs the agent loop — the
single no-tool-call request carries ``AGENT_TOOLS`` (the default
round cap keeps the tools offered), no ``tool`` frames stream, and
the ``done`` event is the plain retrieval shape (the tool-free
answer is byte-identical)."""
single no-tool-call request carries the offered tool list (the
default round cap keeps the tools offered), no ``tool`` frames
stream, and the ``done`` event is the plain retrieval shape (the
tool-free answer is byte-identical). Phase 182 (task 03) pin
update, deliberate: the offer is the three base tools plus
``show_text`` (``answer_text`` defaults ON), the base three staying
element-for-element ``AGENT_TOOLS`."""
_session, llm = gate_env
doc = _doc("Kubernetes Homelab Cluster", "TALOS_DOC_SENT")
monkeypatch.setattr(chat_api, "retrieve", _fake_retriever([_chunk(doc, 0.90)]))
@@ -1149,7 +1152,12 @@ def test_endpoint_grounded_turn_runs_agent_loop_with_tools(
assert frames[-1]["deflected"] is False
assert not any(f["type"] == "tool" for f in frames)
assert len(llm.seen) == 1
assert llm.seen_tools == [AGENT_TOOLS] # one request, tools offered
# Phase 182 (task 03): ``answer_text`` defaults ON — the offer is
# the base three (byte-identical) plus ``show_text``.
assert llm.seen_tools == [[*AGENT_TOOLS, SHOW_TEXT_TOOL]] # one request
offered = llm.seen_tools[0]
assert offered is not None
assert offered[:3] == AGENT_TOOLS # the base offer untouched
# The system prompt is the HIGH prompt with the <tools> instructions
# (phase 70: the harness-aligned ls/read/grep copy — new names in,
# old phase-37/68 names out).
@@ -1600,6 +1608,9 @@ def test_endpoint_log_line_records_suggested_after_summary_hits(
"total_ms=",
"retries=",
"scaffold_stripped=",
# phase 182 (task 05): the PLAN §9 additive tail — the blocks
# actually SENT this turn, last on the line.
"verbatim_blocks=",
)
idx = -1
for field in order:
@@ -1669,3 +1680,140 @@ def test_endpoint_log_line_records_suggested_after_summary_hits(
line = _last_log_line()
assert "thinking=- thinking_chars=" in line
assert "thinking=high" not in line
# ---------------------------------------------------------------------------
# Phase 182 (task 05): the per-turn log line's ``verbatim_blocks=N`` tail
# and the deflected turn's resolve-noting posture (D9). The count is the
# blocks ACTUALLY SENT (after the canonical-locator dedupe and the
# ``show_text_max_blocks`` cut), so the resolver is faked here — its own
# behaviour is pinned in ``tests/unit/test_verbatim_resolver.py`` and the
# real end-to-end fusion in ``tests/integration/test_chat_api.py``.
# ---------------------------------------------------------------------------
def _block(n: int) -> Any:
"""One fake resolved block (the resolver's return type — the counting
and the frame wiring are what these pins are about)."""
from app.rag.verbatim import VerbatimBlock
return VerbatimBlock(
source="Homelab",
path=f"quadlet/gufo.container{n}",
title=f"gufo{n}",
start_line=1,
end_line=4,
total_lines=12,
text=f"[Unit{n}\nExec=gufo",
truncated=False,
locator=f"Homelab/quadlet/gufo.container{n}#1-4",
)
def test_log_line_ends_with_verbatim_blocks_zero_on_a_plain_turn(
client: TestClient,
gate_env: tuple[_FakeSession, _CannedLLM],
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
"""A plain grounded turn (no tool blocks, no fence in the answer)
ends ``… scaffold_stripped=0 verbatim_blocks=0`` — the PLAN §9 field
is UNIFORM across turn shapes (the phase-67 ``retries=N`` pattern),
and the ``done`` frame carries no ``answer_text`` key (A9)."""
_session, _llm = gate_env
doc = _doc("Kubernetes Homelab Cluster", "TALOS_DOC_SENT")
monkeypatch.setattr(chat_api, "retrieve", _fake_retriever([_chunk(doc, 0.30)]))
with caplog.at_level("INFO", logger="app.chat"):
frames = _ask(client, "How is my Kubernetes cluster set up?")
lines = [r.getMessage() for r in caplog.records if "question=" in r.getMessage()]
assert lines
assert lines[-1].endswith("scaffold_stripped=0 verbatim_blocks=0")
done = [f for f in frames if f.get("type") == "done"]
assert done and "answer_text" not in done[0]
def test_log_line_counts_the_verbatim_blocks_sent(
client: TestClient,
gate_env: tuple[_FakeSession, _CannedLLM],
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Two resolved blocks → ``verbatim_blocks=2`` AND both ride the
``done`` frame's ``answer_text`` in the resolver's order (the frame
and the line count the SAME list — never the raw tool-call count)."""
_session, _llm = gate_env
doc = _doc("Kubernetes Homelab Cluster", "TALOS_DOC_SENT")
monkeypatch.setattr(chat_api, "retrieve", _fake_retriever([_chunk(doc, 0.30)]))
monkeypatch.setattr(
chat_api, "resolve_answer_blocks", lambda *_a: [_block(1), _block(2)]
)
with caplog.at_level("INFO", logger="app.chat"):
frames = _ask(client, "How is my Kubernetes cluster set up?")
lines = [r.getMessage() for r in caplog.records if "question=" in r.getMessage()]
assert lines and lines[-1].endswith("verbatim_blocks=2")
done = [f for f in frames if f.get("type") == "done"]
assert done
assert done[0]["answer_text"] == [
{
"source": "Homelab",
"path": "quadlet/gufo.container1",
"title": "gufo1",
"start_line": 1,
"end_line": 4,
"total_lines": 12,
"text": "[Unit1\nExec=gufo",
"truncated": False,
"locator": "Homelab/quadlet/gufo.container1#1-4",
},
{
"source": "Homelab",
"path": "quadlet/gufo.container2",
"title": "gufo2",
"start_line": 1,
"end_line": 4,
"total_lines": 12,
"text": "[Unit2\nExec=gufo",
"truncated": False,
"locator": "Homelab/quadlet/gufo.container2#1-4",
},
]
def test_deflected_turn_resolves_nothing_and_logs_zero(
client: TestClient,
gate_env: tuple[_FakeSession, _CannedLLM],
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
"""D9: a DEFLECTED turn resolves NOTHING even when its own answer
text carries a `` ```bor `` fence — a hallucinated locator must not
leak KB text through the turn that just disclaimed it. The resolver
is never called, the line reads ``verbatim_blocks=0`` and the frame
carries no ``answer_text`` key."""
_session, llm = gate_env
llm.answer = (
"I haven't done anything like that.\n"
"```bor Homelab/quadlet/gufo.container 1-4\n"
"Exec=/usr/bin/lie\n"
"```"
)
doc = _doc("Sourdough Starter", "BREAD_DOC_SENT")
monkeypatch.setattr(chat_api, "retrieve", _fake_retriever([_chunk(doc, 0.10)]))
calls: list[Any] = []
monkeypatch.setattr(
chat_api,
"resolve_answer_blocks",
lambda *_a: calls.append(1) or [], # pyright: ignore[reportFunctionMemberAccess]
)
with caplog.at_level("INFO", logger="app.chat"):
frames = _ask(client, "How do I bake sourdough bread?")
lines = [r.getMessage() for r in caplog.records if "question=" in r.getMessage()]
assert lines and lines[-1].endswith("verbatim_blocks=0")
assert calls == [] # the resolver never ran on a deflected turn
done = [f for f in frames if f.get("type") == "done"]
assert done and done[0]["deflected"] is True
assert "answer_text" not in done[0]
+6 -2
View File
@@ -113,7 +113,9 @@ def test_raw_text_only_stored_and_re_rendered_on_restore() -> None:
# per-call behavior override (default SCROLL instead of forced
# "auto" — documented at the call site).
assert 'addMessage("user", renderMarkdown(m.text), true)' in js
assert 'addMessage("brain", renderMarkdown(m.text), true)' in js
# Phase 182 (task 07): the brain restore renderer opts INTO the verbatim
# pass (the user renderer does not — a question is never a locator).
assert 'addMessage("brain", renderMarkdown(m.text, { verbatim: true }), true)' in js
assert "wrap.classList.add(\"is-deflected\")" in js
assert "appendMaybeTry(wrap, m.suggestions)" in js
assert "appendSources(wrap, m.sources)" in js
@@ -289,7 +291,9 @@ def test_brain_turn_persists_optional_thinking_field() -> None:
'const finalText = acc || (sawThinking ? EMPTY_ANSWER_FALLBACK : "")'
in branch
)
assert "renderMarkdown(finalText)" in branch, (
# Phase 182 (task 07): the fallback renders through the SAME opt-in as
# the per-delta write (ONE answer renderer, one shape).
assert "renderMarkdown(finalText, { verbatim: true })" in branch, (
"the substituted fallback must render into the bubble"
)
+70
View File
@@ -426,6 +426,76 @@ def test_read_max_chars_rejects_negative(monkeypatch: pytest.MonkeyPatch) -> Non
_settings()
def test_answer_text_default_true_and_env_override(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Phase 182 (LOCKED A8): the verbatim-display master switch defaults
to ON — an answer whose code block silently differs from the file is a
correctness bug, so the fix ships on and the switch is an escape hatch
(unlike phase 124's ``answer_images``, which defaults OFF). Env-tunable
in both directions; ``0``/``false`` is the byte-identical pre-phase
state (no tool, no clause, no resolution, no ``answer_text`` key)."""
monkeypatch.delenv("BOR_ANSWER_TEXT", raising=False)
assert _settings().answer_text is True
for off in ("0", "false"):
monkeypatch.setenv("BOR_ANSWER_TEXT", off)
assert _settings().answer_text is False
for on in ("1", "true"):
monkeypatch.setenv("BOR_ANSWER_TEXT", on)
assert _settings().answer_text is True
def test_show_text_max_chars_default_and_env_override(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Phase 182 (LOCKED A8): one displayed verbatim slice is capped at
``BOR_SHOW_TEXT_MAX_CHARS`` (default 12 000 chars — 5 blocks is a 60 KB
worst case against the 256 KB response budget). ``0`` is accepted and
means INERT (no block displays) — not a startup error."""
monkeypatch.delenv("BOR_SHOW_TEXT_MAX_CHARS", raising=False)
assert _settings().show_text_max_chars == 12_000
monkeypatch.setenv("BOR_SHOW_TEXT_MAX_CHARS", "500")
assert _settings().show_text_max_chars == 500
monkeypatch.setenv("BOR_SHOW_TEXT_MAX_CHARS", "0")
assert _settings().show_text_max_chars == 0
def test_show_text_max_chars_rejects_negative(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A negative per-block cap is a typo — it would slice from the END of
the content instead of failing, so startup refuses naming the field
(the ``read_max_chars`` pattern, phase 182)."""
monkeypatch.setenv("BOR_SHOW_TEXT_MAX_CHARS", "-1")
with pytest.raises(ValidationError, match="show_text_max_chars"):
_settings()
def test_show_text_max_blocks_default_and_env_override(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Phase 182 (LOCKED A8): at most ``BOR_SHOW_TEXT_MAX_BLOCKS`` verbatim
blocks display per answer (default 5, counted in answer order). ``0``
is accepted and means INERT (no block displays)."""
monkeypatch.delenv("BOR_SHOW_TEXT_MAX_BLOCKS", raising=False)
assert _settings().show_text_max_blocks == 5
monkeypatch.setenv("BOR_SHOW_TEXT_MAX_BLOCKS", "1")
assert _settings().show_text_max_blocks == 1
monkeypatch.setenv("BOR_SHOW_TEXT_MAX_BLOCKS", "0")
assert _settings().show_text_max_blocks == 0
def test_show_text_max_blocks_rejects_negative(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A negative per-answer block cap is a typo — it would count down
from the END of the block list instead of failing, so startup refuses
naming the field (the ``agent_max_rounds`` pattern, phase 182)."""
monkeypatch.setenv("BOR_SHOW_TEXT_MAX_BLOCKS", "-1")
with pytest.raises(ValidationError, match="show_text_max_blocks"):
_settings()
def test_recency_boost_default_and_env_override(
monkeypatch: pytest.MonkeyPatch,
) -> None:
+4 -1
View File
@@ -887,7 +887,10 @@ def test_renderer_extracted_to_shared_markdown_js() -> None:
and BOTH pages load it via a relative <script src> before their module
scripts — so the globals exist when app.js/document.js run."""
md = _read(MARKDOWN_JS)
assert "function renderMarkdown(md)" in md
# Phase 182 (task 06) added the SECOND, optional `opts` argument — the
# renderer still lives here exactly once, which is what this pin is
# about (the argument is pinned by tests/unit/test_frontend_verbatim_markdown.py).
assert "function renderMarkdown(md" in md
assert "function escapeHtml(s)" in md
app_js = _read(APP_JS)
+72 -6
View File
@@ -982,6 +982,10 @@ PINNED_RUN_TURN = """function runTurn(text, { reask = false, image = null } = {}
turnAbort?.abort(); // phase 48: same outcome, one owner — aborted is
// set first, so the catch never reads the guard's
// abort as a user stop
// Phase 182 (task 07): the 300 s guard is a terminal frame — a panel
// still reading "Retrieving the exact text…" would be a lie by then
// (the hydrate at `done` never runs), so settle it to the honest state.
settleVerbatimPlaceholders(wrap?.querySelector(".bubble"), "unverified");
setUiState(UI_STATE.error, "That's taking a long time — the answer may be stuck.");
});
@@ -1283,7 +1287,14 @@ PINNED_RUN_TURN = """function runTurn(text, { reask = false, image = null } = {}
// NEW run in the container — the D15 re-open of the shared
// block is gone). Idempotent — a no-op without an active run.
closeCurrentRun();
wrap.querySelector(".bubble").innerHTML = renderMarkdown(acc);
// Phase 182 (task 07): the OPT-IN verbatim pass on the ONE answer
// renderer. A ` ```bor ` locator fence becomes a pending placeholder
// here (the model's re-typed body DROPPED, locked A7) that the `done`
// branch below hydrates against the frame's server-resolved blocks —
// mid-stream the reader sees "Retrieving the exact text…", never a
// copy that might be wrong. Every other renderer on the page (user
// bubbles, thinking runs, the document viewer) stays one-argument.
wrap.querySelector(".bubble").innerHTML = renderMarkdown(acc, { verbatim: true });
// No page scroll (phase 42): the answer never follows the viewport.
} else if (ev.type === "done") {
sawDone = true;
@@ -1315,6 +1326,25 @@ PINNED_RUN_TURN = """function runTurn(text, { reask = false, image = null } = {}
// figure is additive — "summaries should still be provided");
// a frame without it renders byte-identically (no figure DOM).
if (ev.answer_image) appendAnswerImage(wrap, ev.answer_image);
// Phase 182 (task 07, locked A1/A2): the answer's verbatim blocks —
// `ev.answer_text`, the bytes the SERVER read out of the index — land
// in the SAME band as the answer image (below the text, above the
// source clusters): hydrate every placeholder the renderer painted,
// then append the blocks the answer never located (a `show_text` call
// the prose never named). A locator the server could not resolve
// renders the honest unresolved sentence with NONE of the model's
// bytes, and no panel is left at "Retrieving the exact text…".
hydrateVerbatim(
wrap.querySelector(".bubble"),
ev.answer_text || [],
VERBATIM_LINK_OPTS
);
appendUnlocatedVerbatim(wrap, ev.answer_text || [], VERBATIM_LINK_OPTS);
// The turn SETTLED — this is where the ordinary code blocks get their
// Copy buttons (never mid-stream: per-delta the bubble is rebuilt from
// scratch, and a button that vanishes on the next token is not a
// control). NO badge, NO caption on those (locked A5).
enhanceAnswerCodeBlocks(wrap);
appendSources(wrap, ev.sources);
// Phase 113 (task 02): the cited tier stays the citation
// surface (the chips above); the related tier (scored docs
@@ -1332,7 +1362,11 @@ PINNED_RUN_TURN = """function runTurn(text, { reask = false, image = null } = {}
// what gets persisted.
const finalText = acc || (sawThinking ? EMPTY_ANSWER_FALLBACK : "");
if (!acc && sawThinking) {
wrap.querySelector(".bubble").innerHTML = renderMarkdown(finalText);
// Phase 182 (task 07): the same opt-in as the per-delta write —
// this line paints the answer the user keeps (the fallback text,
// so no placeholder exists in practice, but the ONE answer
// renderer must stay ONE code path in shape).
wrap.querySelector(".bubble").innerHTML = renderMarkdown(finalText, { verbatim: true });
}
// Persistence save point 2: the answer lands only when the turn is
// complete (raw text + the done metadata; phase 17: + optional
@@ -1359,6 +1393,13 @@ PINNED_RUN_TURN = """function runTurn(text, { reask = false, image = null } = {}
// the precedent): an image-less / pre-phase turn stores a
// byte-identical record.
answer_image: ev.answer_image || undefined,
// Phase 182 (task 07): the verbatim blocks persist with the turn
// (task 05's additive `answer_text` key) so a reload — and a shared
// page — re-renders the identical panel. `undefined` drops the key
// from the JSON (the house optional-meta pattern, the answer_image
// line above is the precedent): a pre-phase or answer_text=False
// record stays byte-identical.
answer_text: ev.answer_text || undefined,
// Phase 113 (task 02): the related tier persists with the
// turn (undefined drops the key from the JSON — the house
// optional-meta pattern), so the restore path re-renders the
@@ -1431,6 +1472,14 @@ PINNED_RUN_TURN = """function runTurn(text, { reask = false, image = null } = {}
}
}
} else if (ev.type === "error") {
// Phase 182 (task 07): an `error` frame is terminal — the `done`
// hydrate will never run, so a placeholder that streamed in stays at
// "Retrieving the exact text…" forever unless it settles HERE (the
// honest unresolved state, never a stale promise of the exact text).
settleVerbatimPlaceholders(
wrap?.querySelector(".bubble"),
"unverified"
);
// Phase 114 (TODO L6): carry the frame's optional hint (the
// "question too long" frame has one) through the throw — the
// banner shows it in place of the default reachability hint.
@@ -1485,6 +1534,10 @@ PINNED_RUN_TURN = """function runTurn(text, { reask = false, image = null } = {}
}
} catch (err) {
if (aborted) {
// Phase 182 (task 07): defensive settle for the guard's own abort
// surface (the timeout callback already ran it — idempotent, and the
// turn must not leave a pending panel on ANY terminal path).
settleVerbatimPlaceholders(wrap?.querySelector(".bubble"), "unverified");
// The 300s guard already took the turn to the error state — its
// own abort surfaces here, and it is never read as a user stop.
// Phase 159 (task 03): the aborted settle must not strand the
@@ -1505,6 +1558,10 @@ PINNED_RUN_TURN = """function runTurn(text, { reask = false, image = null } = {}
// convention; no sources/suggestions — the turn never settled).
// The persistedOnLeave guard (phase 20) keeps a navigate-away —
// which aborts the same fetch — from saving the partial twice.
// Phase 182 (task 07): the STOP is terminal for this answer too — a
// placeholder in the kept partial settles to the honest state (the
// blocks never arrive without a `done` frame).
settleVerbatimPlaceholders(wrap?.querySelector(".bubble"), "unverified");
if (wrap && acc && !persistedOnLeave) {
// Phase 158 (D5): the turn was stopped — fold every open run
// (replaces the phase-117 closeToolCalls fold + the D15 block
@@ -1635,14 +1692,23 @@ def test_run_turn_is_pinned_at_phase_159_state() -> None:
list-of-objects no longer stringifies to ``[object Object]``; the
task-03 pre-stream throw carries ``bubbleText`` (FAILED_REQUEST_TEXT)
+ the phase-114 ``hint``, and the catch forwards ``bubbleText:
err?.bubbleText`` into the failed-turn funnel — locked A3): the
full source must match the pin — the ``reask`` path, the turn-local
err?.bubbleText`` into the failed-turn funnel — locked A3)
and phase 182 (task 07)'s sanctioned additions (the two answer
renderers pass the verbatim opt-in — the per-delta write and the
``done``-time re-render; the done settle hydrates the `` ```bor ``
placeholders against ``ev.answer_text``, appends the blocks the answer
never located, and gives the settled answer's ordinary code blocks their
Copy buttons BEFORE ``appendSources``; the ``answer_text`` meta key rides
``rememberBrainTurn`` with ``|| undefined``; and the terminal paths — the
``error`` frame, the 300 s guard, its own abort surface, and the Stop
branch — settle any leftover placeholder to the unresolved state):
the full source must match the pin — the ``reask`` path, the turn-local
resets, the
SSE contract, the save points, and the sanctioned additions (the
task-01 ``lastUserWrap`` assignment + the idle-settle mark
pass, the task-03 aborted-settle mark pair, the phase-160
done-settle pair, the phase-161 live-container additions, and
the D5 refinement) included."""
done-settle pair, the phase-161 live-container additions, the
D5 refinement, and the phase-182 verbatim settle/hydrate pair) included."""
assert _fn_source(_js(), "runTurn") == PINNED_RUN_TURN
+5 -1
View File
@@ -173,7 +173,11 @@ def test_restore_landing_is_one_shot() -> None:
js = _js()
body = _fn_body(js, "renderStoredMessage")
assert 'addMessage("user", renderMarkdown(m.text), true)' in body
assert 'addMessage("brain", renderMarkdown(m.text), true)' in body
# Phase 182 (task 07): the brain renderer gained the verbatim option —
# the SCROLL intent (the trailing `true`) is untouched, and the option
# object (`{ verbatim: true }`) never ends in `", true)"`, so the count
# below still counts the two restore landings alone.
assert 'addMessage("brain", renderMarkdown(m.text, { verbatim: true }), true)' in body
assert js.count('"auto", true') == 0, "the old forced 'auto' landing must be gone"
# Submit reveal + the two restore landings — nothing else scrolls.
# Phase 123 (task 02): the submit call is multi-line (the optional
+867
View File
@@ -0,0 +1,867 @@
"""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}
+465
View File
@@ -0,0 +1,465 @@
"""Unit: the phase-182 verbatim-panel stylesheet contract (task 06).
The panel DOM is built by ``verbatim.js`` (task 07) and its streaming
placeholder by ``markdown.js`` (task 06); this module pins the CSS contract
that makes the phase readable, in the ``test_frontend_cluster_css.py`` /
``test_markdown_inline_links_images.py`` house pattern (no browser — the
browser-level evidence is the phase's Playwright suite, task 08):
* the phase-182 block EXISTS as one clearly-labelled region of
``frontend/assets/styles.css`` (BEGIN/END markers) and every selector the
contract names is styled there — ``.verbatim``, ``.verbatim-head``,
``.verbatim-caption``, ``.verbatim-badge``, ``.verbatim-body``,
``.verbatim-lines``, ``.verbatim-pending``, ``.verbatim-state``,
``.verbatim-unresolved``, ``.code-block``, ``.code-block-copy``,
``.verbatim-copy``;
* the panel is a BOUNDED scroll panel (PLAN §7.1): ``max-height`` in the
``min(…, 70vh)`` form + ``overflow: auto``, with the head row STICKY
(``position: sticky; top: 0``) on an opaque background so the caption
never scrolls away and rows never show through it;
* the code surface never wraps (phase 116 locked A1, the phase-165 prose/code
split): ``white-space: pre`` + ``overflow-wrap: normal`` + ``tab-size: 2``,
and the line-number gutter is ``user-select: none`` so a Copy/select never
drags numbers into the clipboard;
* text + color, never color alone (WCAG 1.4.1): the badge is a TEXT badge
with ``text-transform: none``, the unresolved state hides the badge, and
the ``.copied`` state exists beside the text swap task 07 performs;
* the whole block is PALETTE-VARIABLES ONLY (the phase-92 zero-literal /
monochrome invariant): no hex/rgb/hsl literal anywhere, and every
``var()`` is a declared ``:root`` token — so a light theme keeps this code
surface readable and no new hue enters the system;
* the ≤640px block lifts the Copy controls to the 44px touch floor, and
nothing in the block animates (so ``prefers-reduced-motion`` has nothing
to still — the rule is written as a guard so a future animation cannot be
added without its still).
"""
from __future__ import annotations
import re
from pathlib import Path
import pytest
STYLES_CSS = Path(__file__).resolve().parents[2] / "frontend" / "assets" / "styles.css"
#: The phase-182 region markers (the block is one contiguous, labelled
#: region, so "no literals in the phase-182 block" is a parseable claim).
BEGIN = "/* ---------- Verbatim panels (phase 182, task 06) — BEGIN ----------"
END = "/* ---------- Verbatim panels (phase 182, task 06) — END ---------- */"
#: Every selector the task's contract names.
PANEL_SELECTORS = (
".verbatim",
".verbatim-head",
".verbatim-caption",
".verbatim-badge",
".verbatim-body",
".verbatim-lines",
".verbatim-pending",
".verbatim-state",
".verbatim-unresolved",
".code-block",
".code-block-copy",
".verbatim-copy",
)
def _css() -> str:
return STYLES_CSS.read_text(encoding="utf-8")
def _phase182_block(css: str) -> str:
"""The phase-182 region, markers included (comments intact — the block
carries the contract in prose too)."""
start = css.find(BEGIN)
assert start != -1, "styles.css must carry the labelled phase-182 block (BEGIN marker)"
end = css.find(END, start)
assert end != -1, "the phase-182 block must be closed by its END marker"
return css[start : end + len(END)]
def _strip_comments(css: str) -> str:
return re.sub(r"/\*[\s\S]*?\*/", "", css)
def _rule_bodies(block: str, selector: str) -> list[str]:
"""The bodies of every rule whose selector list contains ``selector``
EXACTLY (the phase-180 parse pattern, comment-stripped)."""
bodies: list[str] = []
for match in re.finditer(r"([^{}]+)\{([^{}]*)\}", _strip_comments(block)):
selectors = [s.strip() for s in " ".join(match.group(1).split()).split(",")]
if selector in selectors:
bodies.append(match.group(2))
return bodies
def _body(block: str, selector: str) -> str:
bodies = _rule_bodies(block, selector)
assert bodies, f"styles.css must style {selector}"
return " ".join(bodies)
def _root_palette(css: str) -> set[str]:
"""Every variable name the ``:root`` block declares."""
root = re.search(r":root\s*\{", css)
assert root, "styles.css must define :root"
body = css[root.end() : css.find("}", root.end())]
palette = set(re.findall(r"(--[a-z0-9-]+)\s*:", body))
assert palette, "the :root palette must declare variables"
return palette
def _media_block(css: str, query: str) -> str:
"""The body of the ``@media (<query>)`` block — the house non-greedy
pattern (nested closing braces are indented, so the first column-0 ``}``
is the block's own end)."""
m = re.search("@media \\(" + re.escape(query) + r"\) \{([\s\S]*?)\n\}", css)
assert m, f"{query} media query missing"
return m.group(1)
_CSS = _css()
_BLOCK = _phase182_block(_CSS)
_ROOT = _root_palette(_CSS)
# ---------------------------------------------------------------------------
# the block and its selectors
# ---------------------------------------------------------------------------
def test_phase_182_block_is_labelled_and_contains_every_selector() -> None:
"""The block is ONE clearly-labelled region carrying every selector of
the contract (task 06 item 5) — a rule that drifted outside the region
would escape the palette-only and reduced-motion pins below."""
for selector in PANEL_SELECTORS:
assert _rule_bodies(_BLOCK, selector), f"the phase-182 block must style {selector}"
# …and the block says what it is.
assert "phase 182" in _BLOCK
assert "VERBATIM" in _BLOCK, "the block documents the badge it styles"
def test_panel_is_a_themed_surface_card() -> None:
"""``.verbatim`` is a block on the theme's own canvas: the bubble's
rhythm (a vertical margin), a --line hairline, --radius-sm corners, the
--surface fill, ``min-width: 0`` (the long-line escape hatch every
scroller here carries) and ``overflow: hidden`` so the rounded corners
clip the scrolled body."""
body = _body(_BLOCK, ".verbatim")
for prop in (
"display: block",
"min-width: 0",
"background: var(--surface)",
"border: 1px solid var(--line)",
"border-radius: var(--radius-sm)",
"overflow: hidden",
):
assert prop in body, f".verbatim must carry {prop}"
assert re.search(r"margin:\s0\.[0-9]+rem 0;", body), (
".verbatim sits in the bubble's rhythm (a rem margin, not a px one)"
)
def test_head_row_is_sticky_with_an_opaque_background() -> None:
"""PLAN §7.1's bounded-panel rule: the head row STICKS to the top of the
scroll panel, so the caption (``source/path · lines 12–40 of 210``) is
still on screen in a 40-line slice. OPAQUE ``--surface`` background — a
translucent head would let the bytes read through it (the phase-156
``.table-wrap table thead th`` idiom)."""
body = _body(_BLOCK, ".verbatim-head")
assert "position: sticky" in body, "the head row must be sticky"
assert "top: 0" in body, "the sticky offset is the panel top"
assert "background: var(--surface)" in body, "the sticky head must be opaque"
assert "z-index: 1" in body, "the sticky head paints above the scrolled body"
assert "display: flex" in body, "the head is a row (caption, badge, copy)"
def test_caption_is_mono_soft_and_clipped_the_house_way() -> None:
"""``.verbatim-caption`` uses the phase-156 ``.cell-clip`` ellipsis
convention (``min-width: 0`` + ``overflow: hidden`` +
``text-overflow: ellipsis`` + ``white-space: nowrap``) so a long
``source/path`` clips to ``…`` instead of breaking the head row — the
FULL text stays reachable through the control's ``aria-label``/``title``
(the clip is visual only). ``--ink-soft`` on ``--surface`` is 8.2:1
(AA), and the mono family matches every other path in the app."""
body = _body(_BLOCK, ".verbatim-caption")
for prop in (
"min-width: 0",
"overflow: hidden",
"text-overflow: ellipsis",
"white-space: nowrap",
"font-family: var(--mono)",
"color: var(--ink-soft)",
):
assert prop in body, f".verbatim-caption must carry {prop}"
# the ellipsis convention must not rely on a fixed max-width here: the
# caption is the flexible item of a wrapping flex row.
assert "flex: 1" in body, "the caption takes the row's flexible space"
def test_badge_is_text_with_no_uppercase_surprise() -> None:
"""The badge is the WORD ``VERBATIM`` in the --ok family (10.6:1 on its
own fill) with ``text-transform: none`` — an uppercase-forced word is
spelled out letter-by-letter by some screen readers, and an icon-only
badge would fail WCAG 1.4.1 (the claim must be TEXT)."""
body = _body(_BLOCK, ".verbatim-badge")
assert "text-transform: none" in body
assert "color: var(--ok-ink)" in body, "the badge is the --ok family"
assert "background: var(--ok-bg)" in body
assert "content:" not in body, "the badge text is DOM text, never a CSS ::before"
def test_body_is_a_bounded_scroll_panel_of_code() -> None:
"""``.verbatim-body`` is the bounded scroll panel: ``max-height`` in the
``min(…, 70vh)`` form (PLAN §7.1 — the panel never eats the page),
``overflow: auto`` both axes, the mono family, ``white-space: pre`` and
``tab-size: 2`` (the file's own indentation survives), and
``overflow-wrap: normal`` — CODE scrolls horizontally and prose never
does (phase 116 locked A1, the phase-165 split), which also cancels the
``.bubble``'s inherited ``overflow-wrap: anywhere``. ``min-width: 0``
keeps the long-line escape hatch."""
body = _body(_BLOCK, ".verbatim-body")
assert re.search(r"max-height:\s*min\(\s*[0-9.]+rem,\s*70vh\s*\)", body), (
".verbatim-body must cap with max-height: min(<rem>, 70vh)"
)
for prop in (
"overflow: auto",
"min-width: 0",
"font-family: var(--mono)",
"white-space: pre",
"tab-size: 2",
"overflow-wrap: normal",
):
assert prop in body, f".verbatim-body must carry {prop}"
assert "white-space: pre-wrap" not in body, "code never wraps (locked A1)"
def test_line_numbers_are_not_selectable() -> None:
"""The gutter is ``user-select: none`` (with the ``-webkit-`` prefix) on
BOTH the container and its items, right-aligned in ``--ink-soft``: a
reader who selects the panel — or a Copy path that reads the selection —
never drags line numbers into the clipboard. ``list-style: none`` means
the digits are TEXT, so the rule behaves identically in every engine."""
container = _body(_BLOCK, ".verbatim-lines")
for prop in (
"user-select: none",
"-webkit-user-select: none",
"text-align: right",
"color: var(--ink-soft)",
"list-style: none",
):
assert prop in container, f".verbatim-lines must carry {prop}"
items = _body(_BLOCK, ".verbatim-lines li")
assert "user-select: none" in items, "the <li> line numbers must be unselectable too"
def test_pending_state_is_muted_and_static() -> None:
"""The streaming state (``.verbatim-pending`` + ``.verbatim-state``, the
markdown.js placeholder) is MUTED — ``--ink-soft`` on ``--surface``,
8.2:1 — and it never announces itself with motion: the pending line is a
``role="status"`` live region, so the announcement IS the feedback."""
pending = _body(_BLOCK, ".verbatim-pending")
state = _body(_BLOCK, ".verbatim-state")
assert "color: var(--ink-soft)" in state, "the pending line is muted (not an answer)"
assert "background: var(--surface)" in pending
assert "animation" not in pending and "transition" not in pending
assert "animation" not in state and "transition" not in state
def test_unresolved_state_carries_no_badge() -> None:
"""The honest failure state: the --err boundary + --err-bg canvas with
--ink-soft text (7.9:1, AA), and CSS-side enforcement that a ``VERBATIM``
badge NEVER appears on an unresolved panel whatever DOM reaches it — the
badge belongs only to a server-resolved block (locked A5)."""
body = _body(_BLOCK, ".verbatim-unresolved")
assert "var(--err" in body, "the unresolved state is the --err family"
assert "color: var(--ink-soft)" in body
hide = _body(_BLOCK, ".verbatim-unresolved .verbatim-badge")
assert "display: none" in hide, "an unresolved panel never badges"
def test_copy_controls_are_pills_with_a_ring_and_an_ellipsis_label() -> None:
"""``.code-block-copy`` / ``.verbatim-copy``: the house pill (mono text,
--line hairline, --surface fill, ink-soft label), the label is the clip
target (``text-overflow: ellipsis`` on its span, the control's
``aria-label`` naming the full path), the ``:focus-visible`` ring is the
app's 3px --brand ring, and the ``.copied`` rule exists to SUPPORT the
text swap (``Copy`` → ``Copied``) with colour — never colour alone."""
both = _body(_BLOCK, ".code-block-copy") + " " + _body(_BLOCK, ".verbatim-copy")
for prop in (
"border: 1px solid var(--line)",
"background: var(--surface)",
"color: var(--ink-soft)",
"font-family: var(--mono)",
"cursor: pointer",
"min-height:",
):
assert prop in both, f"the copy controls must carry {prop}"
label = _body(_BLOCK, ".code-block-copy > span")
assert "text-overflow: ellipsis" in label, "a clipped copy label shows the hint"
for selector in (".code-block-copy:focus-visible", ".verbatim-copy:focus-visible"):
ring = _body(_BLOCK, selector)
assert "outline: 3px solid var(--brand)" in ring, f"{selector} needs the house ring"
assert "outline-offset: 2px" in ring
for selector in (".code-block-copy.copied", ".verbatim-copy.copied"):
copied = _body(_BLOCK, selector)
assert "var(--ok" in copied, f"{selector} must use the --ok family as SUPPORT"
def test_plain_code_block_wrapper_is_not_a_verbatim_panel() -> None:
"""``.code-block`` is the plain wrapper task 07 puts around an ordinary
answer ``<pre>``: a Copy button belongs to it, the ``VERBATIM`` badge
does NOT — so the wrapper rule carries no --ok fill and no panel chrome
(locked A5: verbatim-and-badged OR plainly the model's, never a hybrid)."""
body = _body(_BLOCK, ".code-block")
assert "position: relative" in body
assert "min-width: 0" in body
assert "--ok" not in body, "an ordinary code block never wears the verbatim badge"
assert not _rule_bodies(_BLOCK, ".code-block .verbatim-badge"), (
"no badge rule may exist inside the plain wrapper"
)
# ---------------------------------------------------------------------------
# the palette-only invariant (phase 92 / the :root-drift guard)
# ---------------------------------------------------------------------------
def test_phase_182_block_uses_palette_variables_only() -> None:
"""NO colour literal anywhere in the phase-182 block: every colour comes
from a declared ``:root`` token (no hex, no ``rgb()``/``rgba()``, no
``hsl()``, no named colour). A literal would be a second source of truth
for the built-in palette (the ``test_theming.py`` drift guard parses the
built-ins out of this file) and a new hue would break the monochrome
invariant — this is also what keeps a code surface readable when the
owner switches the app to a light or a monochrome theme."""
code = _strip_comments(_BLOCK)
refs = re.findall(r"var\((--[a-z0-9-]+)\)", code)
assert refs, "the phase-182 block must reference palette vars"
for ref in set(refs):
assert ref in _ROOT, f"{ref} is not a :root palette var"
assert not re.search(r"#[0-9a-fA-F]{3,8}\b", code), "no hex color literal in the block"
for fn in ("rgb(", "rgba(", "hsl(", "hsla("):
assert fn not in code, f"no {fn} color literal in the block"
# named colours are the other way a second source of truth sneaks in
for name in (": white", ": black", "white;", "black;", "white }", "black }"):
assert name not in code, f"no named colour in the block ({name.strip()})"
def test_phase_182_block_restates_no_global_focus_rule_wrongly() -> None:
"""The restatement of the global ``:focus-visible`` ring uses the SAME
values as the app-wide rule (3px --brand, 2px offset) — a different
ring here would be a second, drifting source of focus truth."""
global_rule = _css()[: _css().find("\n::selection")]
assert re.search(r":focus-visible \{\s*outline: 3px solid var\(--brand\);\s*"
r"outline-offset: 2px;", global_rule), (
"the app-wide :focus-visible rule is the baseline this block copies"
)
# ---------------------------------------------------------------------------
# mobile + reduced motion
# ---------------------------------------------------------------------------
def test_mobile_block_keeps_a_44px_control() -> None:
"""The ≤640px block lifts BOTH Copy controls to the 44px touch floor
(PLAN §7: the phone tap target) — the desktop pill is smaller, so the
rule must live in the media block, not only in the base rule."""
mobile = _media_block(_CSS, "max-width: 640px")
assert "Phase 182" in mobile, "the mobile block must name the phase it serves"
hit = re.search(r"\.code-block-copy, \.verbatim-copy \{([^}]*)\}", mobile)
assert hit, "the mobile block must raise both Copy controls together"
assert "min-height: 44px" in hit.group(1)
def test_nothing_in_the_block_animates_without_a_reduced_motion_still() -> None:
"""``prefers-reduced-motion`` is honored by anything that animates. The
phase-182 block animates NOTHING (the pending state is a live region, so
the text swap is the feedback), which this pins as a GUARD: if a future
edit adds an ``animation``/``transition``, the same selectors must appear
in a ``prefers-reduced-motion: reduce`` block that stills them."""
code = _strip_comments(_BLOCK)
animated: list[str] = []
for match in re.finditer(r"([^{}]+)\{([^{}]*)\}", code):
body = match.group(2)
if re.search(r"(^|[;\s])(animation|transition)\s*:", body):
animated.append(" ".join(match.group(1).split()))
if not animated:
assert "prefers-reduced-motion" in _BLOCK, (
"a static block must say so, so the next reader does not add "
"a pulse 'to match the app'"
)
return
reduced = "\n".join(
m.group(0)
for m in re.finditer(r"@media \(prefers-reduced-motion: reduce\)[\s\S]*?\n\}", _CSS)
)
for selectors in animated:
for selector in selectors.split(","):
assert selector.strip() in reduced, (
f"{selector} animates but is never stilled under "
"prefers-reduced-motion"
)
def test_the_panel_never_repaints_the_existing_code_and_bubble_rules() -> None:
"""The neighbours are untouched: ``.bubble pre`` (the ordinary answer
code block) and ``.doc-raw`` (the viewer's scroller) keep their
declarations, so the phase adds a SURFACE and does not restyle every
code block in the app."""
css = _CSS
pre = re.search(r"\.bubble pre \{([^}]*)\}", css)
assert pre, ".bubble pre must still exist"
for prop in ("background: var(--surface)", "border: 1px solid var(--line)", "overflow-x: auto"):
assert prop in pre.group(1), f".bubble pre must keep {prop}"
raw = re.search(r"\.doc-raw \{([^}]*)\}", css)
assert raw, ".doc-raw must still exist"
assert "white-space: pre" in raw.group(1)
def test_block_is_one_contiguous_region_of_the_sheet() -> None:
"""The BEGIN/END markers wrap ONE region (a phase that appends a stray
``.verbatim-*`` rule elsewhere escapes every pin in this module)."""
css = _CSS
assert css.count(BEGIN) == 1 and css.count(END) == 1
assert css.index(BEGIN) < css.index(END)
between = css[css.index(BEGIN) : css.index(END)]
# every verbatim selector lives inside it
for selector in PANEL_SELECTORS:
assert re.search(r"(?m)^" + re.escape(selector) + r"[^\n]*\{", between), (
f"{selector} must be declared inside the labelled phase-182 region"
)
outside = _strip_comments(css[: css.index(BEGIN)] + css[css.index(END) :])
for selector in PANEL_SELECTORS:
assert not re.search(
r"(?m)^" + re.escape(selector) + r"[^\n]*\{", outside
), f"{selector} must not be restyled outside the phase-182 region"
def test_selector_names_match_the_dom_the_renderer_and_task_07_build() -> None:
"""The CSS class names are the contract between the renderer's pending
template (``markdown.js``) and the panel builder (``verbatim.js``, task
07): the pending template's classes must be exactly the ones styled here,
so the streaming placeholder is never an unstyled div."""
js = (STYLES_CSS.parent / "markdown.js").read_text(encoding="utf-8")
for cls in ("verbatim", "verbatim-pending", "verbatim-caption", "verbatim-state"):
assert cls in js, f"markdown.js must emit .{cls}"
assert _rule_bodies(_BLOCK, "." + cls), f"styles.css must style .{cls}"
assert "verbatim-unresolved" in _BLOCK, (
"the unresolved class is part of THIS contract (task 07 renders it)"
)
@pytest.mark.parametrize("pair", [("--ink", "--surface"), ("--ink-soft", "--surface")])
def test_block_pairs_the_documented_text_tokens(pair: tuple[str, str]) -> None:
"""The block pairs its text with the canvas the :root comment documents
(``--ink`` and ``--ink-soft`` on ``--surface``) rather than inventing a
third ink — the AA duty is inherited from the palette, not restated."""
code = _strip_comments(_BLOCK)
assert re.search(r"color: var\(" + re.escape(pair[0]) + r"\)", code), (
f"the block must use {pair[0]}"
)
assert re.search(r"background: var\(" + re.escape(pair[1]) + r"\)", code), (
f"the block must sit on {pair[1]}"
)
@@ -0,0 +1,636 @@
"""Unit: the shared renderer's opt-in ``bor`` locator pass (phase 182, task 06).
Browser behavior lands in the phase's Playwright suite (task 08); here the
established ``test_frontend_*`` convention pins the SOURCE contract of
``frontend/assets/markdown.js`` without a browser — the second, optional
argument and the single truthy guard, the pass ORDER (verbatim BEFORE
fences), the ``\\u0000VERBATIMn\\u0000`` placeholder + restore step, the exact
pending template (``verbatim-pending`` / ``data-bor-locator`` /
``verbatim-caption`` / ``role="status"`` / the pinned copy
``Retrieving the exact text…``), the local ``borLocator`` normalizer and the
separator/range grammar it accepts, and the fact that the ordinary-fence
``<pre><code>`` output string is UNCHANGED (task 06 item 4: task 07 wraps
``<pre>`` nodes at the DOM level instead, so the document viewer, the
thinking blocks and every other caller keep byte-identical HTML).
The node-driven behavioral half (skipped when node is unavailable, the
phase-44/138/180 idiom) proves the story shapes: the completion criterion —
``renderMarkdown("```bor homelab/a.md 12-40\\nGARBAGE\\n```", {verbatim:
true})`` yields the placeholder DOM with
``data-bor-locator="homelab/a.md#12-40"`` and NO ``GARBAGE`` anywhere in the
returned HTML (locked A7, the phase's core contract) — while the SAME call
without the second argument returns the pre-phase output byte-for-byte; a
bare `` ```bor `` (no path) survives to the fence pass untouched; an
unparseable locator is put back byte-for-byte (no placeholder, no badge, the
model's own body — neither improved nor worsened); a fence left OPEN at the
very end of the input shows the pending state; two fences restore in answer
order; a locator carrying markup stays inert escaped text; and a standalone
placeholder is not wrapped in a ``<p>``.
Parity half: ``borLocator`` is the documented CLIENT MIRROR of Python
``app.rag.verbatim.canonical_locator``. The phase's E2E (task 08) pins that
parity end to end (both sides must agree or hydration misses); this module
pins the GRAMMAR too, by running both implementations over the same table
and over a deterministic fuzz — the only way to catch a grammar drift
before there is a browser in the loop.
"""
from __future__ import annotations
import json
import random
import re
import shutil
import subprocess
from pathlib import Path
import pytest
FRONTEND = Path(__file__).resolve().parents[2] / "frontend"
MARKDOWN_JS = FRONTEND / "assets" / "markdown.js"
HAVE_NODE = shutil.which("node") is not None
#: The single truthy path of the phase-182 second argument.
VERBATIM_GUARD = "opts && opts.verbatim === true"
#: The verbatim fence pass — the pinned regex of task 06 item 2.
VERBATIM_FENCE_RE = r"/^```bor[ \t]+\S[^\n]*\n(?:[\s\S]*?^```[ \t]*$|(?![\s\S]))/gm"
#: The pre-existing fence pass, untouched (task 06 item 4).
ORDINARY_FENCE_RULE = (
'codeBlocks.push(`<pre><code>${escapeHtml(code.replace(/\\n$/, ""))}</code></pre>`);'
)
#: The untouched pre-phase paragraph guard (the phase-138/180 pin).
PARAGRAPH_GUARD = r"/^<(h\d|ul|ol|pre|li|blockquote)/"
#: The placeholder's block-element clause added by task 06.
PLACEHOLDER_GUARD = r'/^\u0000VERBATIM\d+\u0000$/'
#: The pinned pending copy (``…`` is ONE character, not three dots).
PENDING_COPY = "Retrieving the exact text\u2026"
#: The completion criterion's input.
CRITERION_INPUT = "```bor homelab/a.md 12-40\nGARBAGE\n```"
#: The pre-phase (verbatim-OFF) output of :data:`CRITERION_INPUT` — the
#: byte-pin of the "inert until opted in" contract. A `` ```bor path `` info
#: string never matched the ordinary fence rule (``/```(\w*)\n/`` needs the
#: language token ALONE on that line), so OFF it is escaped prose.
CRITERION_OFF_OUTPUT = "<p>```bor homelab/a.md 12-40<br>GARBAGE<br>```</p>"
#: The option task 07 will pass (never passed by any call site yet).
ON: dict[str, bool] = {"verbatim": True}
#: The inputs the node cases render, keyed by case name.
CASE_INPUT: dict[str, str] = {
"criterion": CRITERION_INPUT,
"bare_bor": "```bor\nfoo\n```",
"open_bare": "```bor\n",
"malformed": "```bor not-a-locator 12-40\nSECRET_BODY\n```",
"open_fence": "```bor homelab/a.md 12-40\n",
"two_fences": "```bor a/b 1-2\nBODY_ONE\n```\n\n```bor c/d\nBODY_TWO\n```",
"en_dash": "```bor a/b 12\u201340\nBODY\n```",
"l_prefix": "```bor a/b#L12-L40\nBODY\n```",
"xss": "```bor a\"/><script>alert(1)</script>/b.md 1-2\nBODY\n```",
"standalone": "prose\n\n```bor homelab/a.md\nBODY\n```\n\nafter",
"attached": "prose\n```bor homelab/a.md 1-2\nBODY\n```",
"nested": "```\ntext\n```bor a/b 1-2\nBODY\n```\n",
"no_bor": "# Title\n\nsome **text** and `code`\n\n| a |\n|---|\n| 1 |\n\n> quote\n",
}
def _text(path: Path) -> str:
return path.read_text(encoding="utf-8")
def _run_node(script: str) -> str:
proc = subprocess.run(["node", "-e", script], capture_output=True, text=True, timeout=60)
assert proc.returncode == 0, f"node failed: {proc.stderr}"
return proc.stdout
def _render(cases: dict[str, dict[str, bool] | None]) -> dict[str, str]:
"""Run every case through the REAL renderer under node.
``None`` means the ONE-argument call (every pre-phase call site); a dict
is passed as the second argument as-is.
"""
parts = []
for name, opts in cases.items():
arg = "" if opts is None else f", {json.dumps(opts)}"
parts.append(f"out.{name} = renderMarkdown({json.dumps(CASE_INPUT[name])}{arg}); ")
driver = _text(MARKDOWN_JS) + "\nconst out = {}; " + "".join(parts)
driver += "console.log(JSON.stringify(out));"
results: dict[str, str] = json.loads(_run_node(driver))
return results
def _bor_locator_source() -> str:
js = _text(MARKDOWN_JS)
start = js.index("function borLocator(raw) {")
end = js.index("\nfunction renderMarkdown", start)
return js[start:end]
# ---------------------------------------------------------------------------
# the signature + the guard (task 06 item 1)
# ---------------------------------------------------------------------------
def test_render_markdown_takes_the_optional_opts_argument() -> None:
"""``renderMarkdown(md)`` became ``renderMarkdown(md, opts)`` — a SECOND,
OPTIONAL argument, so every existing call site (the document viewer, the
thinking blocks, the user bubbles) keeps working untouched — and the
ONLY truthy path is ``opts.verbatim === true`` (a strict ``===``, so
``{verbatim: 1}`` / ``{verbatim: "yes"}`` stay OFF)."""
js = _text(MARKDOWN_JS)
assert "function renderMarkdown(md, opts) {" in js, (
"renderMarkdown must gain the second, optional `opts` argument"
)
assert "function renderMarkdown(md) {" not in js, "the one-argument signature is gone"
body = js[js.index("function renderMarkdown(md, opts) {") :]
assert body.count(VERBATIM_GUARD) == 1, (
f"the pass must be guarded by exactly one `{VERBATIM_GUARD}` test"
)
assert "if (opts && " in js, "a missing opts object must be handled (no TypeError)"
def test_header_comment_records_the_phase_182_pass() -> None:
"""The header comment carries the three things the next reader needs: the
pass exists and is opt-in, WHY the model's body is discarded (locked A7),
and the byte-identical-when-off contract."""
js = _text(MARKDOWN_JS)
header = js[: js.find("function escapeHtml")]
flat = " ".join(
" ".join(line.strip().lstrip("*").strip() for line in header.splitlines()).split()
)
for needle, label in [
("Phase 182", "the phase reference"),
("opts && opts.verbatim === true", "the single truthy path"),
("BODY IS DISCARDED", "why the model's bytes are dropped"),
("locked A7", "the locked decision behind it"),
("canonical_locator", "the Python function this mirrors"),
("BYTE-IDENTICAL WHEN OFF", "the inert-when-off contract"),
("verbatim \u2192 fences \u2192 tables", "the new pass order"),
("STRICTER", "the deliberate asymmetry with the server's fences_in"),
("model's own body", "the malformed-locator degradation"),
]:
assert needle in flat, f"the header comment must record {label}: {needle!r}"
# ---------------------------------------------------------------------------
# the pass + the placeholder (task 06 item 2)
# ---------------------------------------------------------------------------
def test_verbatim_pass_runs_before_the_fence_pass() -> None:
"""Order is verbatim → fences → tables → escape+inline → blockquotes →
paragraphs → restore. The verbatim regex MUST sit ahead of the ordinary
fence replace, otherwise a locator fence is eaten as a code block first
and the placeholder never exists."""
js = _text(MARKDOWN_JS)
assert VERBATIM_FENCE_RE in js, f"the pinned fence regex is missing: {VERBATIM_FENCE_RE}"
fence_pass = js.find(ORDINARY_FENCE_RULE)
assert fence_pass != -1, "the ordinary fence pass must still be there"
assert js.find(VERBATIM_FENCE_RE) < fence_pass, (
"the verbatim pass must run BEFORE the fence pass"
)
# …and the rest of the pipeline keeps its order behind it.
for marker in ("const tables = []", "text = inline(escapeHtml(text));", r"split(/\n{2,}/)"):
assert fence_pass < js.find(marker), f"the pass order broke at {marker!r}"
def test_placeholder_is_created_and_restored() -> None:
"""A parsed locator becomes ``\\u0000VERBATIMn\\u0000`` and the FINAL restore
step (alongside TABLE/CODE) turns it back into the panel — the
placeholder survives escaping (NUL and digits are not escaped) and never
reaches the DOM itself."""
js = _text(MARKDOWN_JS)
assert "`\\u0000VERBATIM${verbatim.length - 1}\\u0000`" in js, (
"the placeholder must be the indexed VERBATIM marker"
)
restore = r".replace(/\u0000VERBATIM(\d+)\u0000/g"
assert restore in js, "the restore step must replace the VERBATIM placeholder"
table = js.find(r".replace(/\u0000TABLE(\d+)\u0000/g")
code = js.find(r".replace(/\u0000CODE(\d+)\u0000/g")
assert table < code < js.find(restore), "the restore order is TABLE → CODE → VERBATIM"
def test_pending_template_is_exact() -> None:
"""The restore emits EXACTLY the pinned pending template: the
``verbatim verbatim-pending`` panel, the canonical locator in
``data-bor-locator`` (task 07's hydration key), the clipped
``verbatim-caption``, and the ``role="status"`` state line carrying the
pinned copy — every interpolated value through ``escapeHtml``."""
js = _text(MARKDOWN_JS)
for marker in (
'<div class="verbatim verbatim-pending" data-bor-locator="${escapeHtml(canonical)}">',
'<span class="verbatim-caption">${escapeHtml(identity)}</span>',
f'<span class="verbatim-state" role="status">{PENDING_COPY}</span>',
):
assert marker in js, f"the pending template must contain {marker!r}"
assert 'data-bor-locator="${canonical}"' not in js, (
"the locator is model-written text landing in an ATTRIBUTE — it must "
"go through escapeHtml (the escape-first boundary)"
)
def test_malformed_locator_is_put_back_byte_for_byte() -> None:
"""``borLocator`` returning ``""`` returns the ORIGINAL match, so a
malformed locator degrades to whatever the fence pass does with this text
today (no placeholder, no badge, the model's own body) — this phase
neither improves nor worsens it."""
js = _text(MARKDOWN_JS)
assert 'if (canonical === "") return match;' in js, (
"an unparseable locator must be returned to the text unchanged"
)
def test_paragraph_guard_blocks_placeholder_and_untouched_guard_stays() -> None:
"""The step-3 guard keeps the pre-phase test verbatim (the phase-138/180
pin) and adds the placeholder clause, so a standalone panel restores as a
``<div>`` instead of landing inside a ``<p>``."""
js = _text(MARKDOWN_JS)
assert PARAGRAPH_GUARD in js, "the pre-phase paragraph guard must stay"
assert js.count(PARAGRAPH_GUARD) == 1, "there is exactly one paragraph guard"
assert PLACEHOLDER_GUARD in js, "the placeholder must be a block element too"
# ---------------------------------------------------------------------------
# borLocator — the client mirror (task 06 item 3)
# ---------------------------------------------------------------------------
def test_bor_locator_exists_and_handles_every_grammar_token() -> None:
"""``borLocator`` exists, is documented as the CLIENT MIRROR of
``app.rag.verbatim.canonical_locator``, and its body carries every
separator (`` ` ``, ``:``, ``#``, the ``L`` prefix) and range shape
(``12-40``, the en dash, ``L12-L40``, a bare ``12``) of the D7 grammar —
plus the refusals (non-numeric, ``< 1``, ``start > end``, trailing
junk)."""
body = _bor_locator_source()
for token, label in [
(r"[ \t:#]+", "the ` `/`:`/`#` separator class"),
(r"L?\d+", "the optional `L` line prefix"),
(r"[-\u2013]", "the hyphen AND the en dash range separator"),
("(?:", "the OPTIONAL range group (a bare `12` and no range at all)"),
("$/", "the end anchor (trailing junk is not a range)"),
('includes("/")', "the first-`/` identity requirement"),
("start < 1", "the `< 1` refusal"),
("start > end", "the `start > end` refusal"),
('replace(/^[Ll]+/, "")', "the `L` prefix strip on each bound"),
]:
assert token in body, f"borLocator must handle {label}: {token!r}"
def test_bor_locator_is_documented_as_the_python_mirror() -> None:
"""The docstring above ``borLocator`` names the Python function it
mirrors and states that parity is load-bearing (a divergence means
hydration misses), so nobody 'fixes' one side alone."""
js = _text(MARKDOWN_JS)
comment = js[: js.index("function borLocator(raw) {")]
flat = " ".join(
" ".join(line.strip().lstrip("/").strip() for line in comment.splitlines()).split()
)
for needle, label in [
("canonical_locator", "the Python function"),
("CLIENT MIRROR", "that this is the mirror, not the source"),
("PARITY IS LOAD-BEARING", "why both sides must agree"),
("#12-40", "the canonical range form"),
]:
assert needle in flat, f"the docstring must record {label}: {needle!r}"
def test_bor_locator_returns_canonical_digits_or_empty() -> None:
"""Source shape: the canonical output is ``identity#start-end`` (digits
only) and every refusal path returns ``""`` — never ``null``/``undefined``
(the renderer compares against ``""``)."""
body = _bor_locator_source()
assert "return `${identity}#${start}-${end}`;" in body
assert body.count('return "";') >= 6, "every refusal path must return the empty string"
assert "return null" not in body and "return undefined" not in body, (
'borLocator\'s not-a-locator value is "", so the renderer\'s '
'`if (canonical === "")` test cannot miss'
)
# ---------------------------------------------------------------------------
# ordinary fences are NOT wrapped in markdown (task 06 item 4)
# ---------------------------------------------------------------------------
def test_ordinary_fence_output_string_is_unchanged() -> None:
"""Task 06 item 4: do NOT wrap ordinary fences in markdown — task 07
wraps ``<pre>`` nodes at the DOM level, so the document viewer, the
thinking blocks and every other caller keep byte-identical HTML. The
fence rule and its ``<pre><code>`` output string are pinned verbatim."""
js = _text(MARKDOWN_JS)
assert r"/```(\w*)\n([\s\S]*?)```/g" in js, "the ordinary fence regex is unchanged"
assert ORDINARY_FENCE_RULE in js, "the ordinary fence output string is unchanged"
assert 'class="code-block"' not in js, (
"markdown.js must NOT emit the task-07 wrapper — that is a DOM-level wrap"
)
#: The answer renderers task 07 opted IN (the whole client switch — the
#: per-delta write, the ``done``-time re-render, the brain restore).
OPTED_IN_CALLS: tuple[str, ...] = (
"renderMarkdown(acc, { verbatim: true })",
"renderMarkdown(finalText, { verbatim: true })",
"renderMarkdown(m.text, { verbatim: true })",
)
def test_only_the_answer_renderers_opted_in() -> None:
"""Task 07 opted in exactly the THREE answer renderers (the per-delta
write, the ``done``-time re-render, the brain restore) — and the shared
page's one brain renderer. NOTHING else: the user-bubble renders, the
live thinking runs, the legacy thinking restore and the whole document
viewer stay one-argument calls, because a question and a scratchpad are
not locators (task 06's byte-identity contract for every other surface).
The count is the pin: a fourth ``verbatim: true`` in either file is a
surface the phase never claimed."""
app = _text(FRONTEND / "assets" / "app.js")
shared = _text(FRONTEND / "assets" / "shared.js")
for call in OPTED_IN_CALLS:
assert call in app, f"app.js must render the answer through {call}"
assert app.count("verbatim: true") == 3, (
"exactly the three answer renderers may opt in in app.js"
)
assert (
'addSharedMessage("brain", renderMarkdown(m.text, { verbatim: true }))' in shared
)
assert shared.count("verbatim: true") == 1, "the shared page opts in its one renderer"
# The neighbours stay one-argument, in both files.
assert 'addMessage("user", renderMarkdown(m.text), true)' in app
assert 'addSharedMessage("user", renderMarkdown(m.text))' in shared
for name in ("app.js", "shared.js"):
js = _text(FRONTEND / "assets" / name)
for site in re.findall(r"renderMarkdown\([^\n]*\)", js):
if "thinking" in site or "entry.text" in site:
assert "verbatim" not in site, f"{name}: {site} must stay one-argument"
# A document's own ```bor fence is the document's text — never a locator.
assert "verbatim: true" not in _text(FRONTEND / "assets" / "document.js")
# ---------------------------------------------------------------------------
# behavior (node): the story shapes + the completion criterion
# ---------------------------------------------------------------------------
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_locator_fence_yields_the_placeholder_and_drops_the_model_bytes() -> None:
"""THE completion criterion: the criterion input WITH ``{verbatim: true}``
yields the placeholder DOM with
``data-bor-locator="homelab/a.md#12-40"`` and NO ``GARBAGE`` anywhere in
the returned HTML (locked A7). Two fences restore in answer order, the en
dash and the ``L`` prefix canonicalize, and a standalone panel is not
wrapped in a ``<p>``."""
out = _render(
{
"criterion": ON,
"two_fences": ON,
"en_dash": ON,
"l_prefix": ON,
"standalone": ON,
}
)
html = out["criterion"]
assert 'class="verbatim verbatim-pending"' in html
assert 'data-bor-locator="homelab/a.md#12-40"' in html
assert ">homelab/a.md</span>" in html, "the caption carries the source/path"
assert f">{PENDING_COPY}</span>" in html
assert 'role="status"' in html
assert "GARBAGE" not in html, "the model's body must appear NOWHERE (locked A7)"
assert "\u0000" not in html, "no placeholder byte survives"
assert "<p>" not in html, "a standalone panel is a block, not a <p>"
two = out["two_fences"]
assert two.index("a/b#1-2") < two.index('data-bor-locator="c/d"'), (
"two fences keep answer order"
)
assert two.count("verbatim-pending") == 2
assert "BODY_ONE" not in two and "BODY_TWO" not in two
assert out["en_dash"].count('data-bor-locator="a/b#12-40"') == 1
assert "BODY" not in out["en_dash"]
assert out["l_prefix"].count('data-bor-locator="a/b#12-40"') == 1
assert "BODY" not in out["l_prefix"]
assert out["standalone"] == (
'<p>prose</p><div class="verbatim verbatim-pending" '
'data-bor-locator="homelab/a.md"><span class="verbatim-caption">homelab/a.md</span>'
f'<span class="verbatim-state" role="status">{PENDING_COPY}</span></div><p>after</p>'
)
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_off_paths_are_byte_identical_to_the_pre_phase_renderer() -> None:
"""Every call WITHOUT ``{verbatim: true}`` — no second argument, the
document viewer's one-argument call, and an explicit
``{verbatim: false}`` — returns the PRE-PHASE output byte-for-byte: the
criterion input is escaped prose (a `` ```bor path `` info string never
matched the ``/```(\\w*)\\n/`` fence rule), and a locator-free document
renders identically with and without the option."""
out = _render(
{
"criterion": None,
"malformed": None,
"two_fences": None,
"standalone": None,
"nested": None,
"no_bor": None,
}
)
assert out["criterion"] == CRITERION_OFF_OUTPUT, "the OFF path must not change a byte"
for name, html in out.items():
assert "verbatim" not in html, f"{name}: no placeholder markup with the option OFF"
# the option object exists but is false → the same bytes as no argument
on_false = _render({"criterion": {"verbatim": False}})
assert on_false["criterion"] == CRITERION_OFF_OUTPUT
truthy_but_strict = _render({"criterion": {}})
assert truthy_but_strict["criterion"] == CRITERION_OFF_OUTPUT, (
"the guard is `=== true`: an options object without the flag stays OFF"
)
# a locator-free document is untouched by the whole feature
assert _render({"no_bor": ON})["no_bor"] == out["no_bor"]
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_bare_and_malformed_fences_fall_through_to_the_ordinary_pass() -> None:
"""A bare `` ```bor `` (no path) is an ordinary code block and survives to
the fence pass UNTOUCHED even with the option on; a bare `` ```bor ``
with no body at all still paints its raw marker as prose; and an
unparseable locator degrades to exactly what the fence pass does with
this text today — the model's own body, no placeholder, no badge."""
out = _render({"bare_bor": ON, "open_bare": ON, "malformed": ON})
assert out["bare_bor"] == "<p><pre><code>foo</code></pre></p>", (
"a bare ```bor fence must render EXACTLY as it does pre-phase"
)
assert out["open_bare"] == "<p>```bor</p>"
assert out["malformed"] == "<p>```bor not-a-locator 12-40<br>SECRET_BODY<br>```</p>", (
"a malformed locator is put back byte-for-byte: no placeholder, "
"no badge, the model's own body"
)
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_open_fence_at_the_end_shows_the_pending_state() -> None:
"""The second alternative: a fence left OPEN at the very end of the input
(the info line typed, the body not yet streamed) shows the pending
placeholder instead of raw backticks — the deliberate asymmetry with the
server's stricter ``fences_in`` (the server only ever sees a finished
answer, the client sees a partial one)."""
out = _render({"open_fence": ON})
html = out["open_fence"]
assert 'data-bor-locator="homelab/a.md#12-40"' in html
assert "```" not in html, "no raw backticks while a locator fence is open"
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_locator_markup_stays_inert_escaped_text() -> None:
"""Escape-first holds on the new surface: a locator carrying ``"><script>``
is HTML-escaped in BOTH the attribute and the caption, so it can never
become a live element (the renderer's whole reason for existing)."""
html = _render({"xss": ON})["xss"]
assert "<script>" not in html
assert "&lt;script&gt;" in html
assert '&quot;/&gt;&lt;script&gt;' in html
assert "#1-2" in html, "the markup still canonicalized to a locator"
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_a_bor_line_inside_an_ordinary_fence_agrees_with_the_server() -> None:
"""Documented limitation shared with ``fences_in`` (both sides scan for
the opening line, neither supports a nested `` ``` ``): a ``bor`` locator
line inside an ordinary fence is a LOCATOR on both sides, so hydration
still matches — it is never silently half-code, half-panel on one side
only."""
from app.rag.verbatim import fences_in
html = _render({"nested": ON})["nested"]
assert 'data-bor-locator="a/b#1-2"' in html
assert fences_in(CASE_INPUT["nested"]) == ["a/b 1-2"], (
"the server must see the SAME locator in the same text"
)
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_fence_attached_to_prose_still_loses_its_body() -> None:
"""A locator fence with NO blank line above it still loses the model's
body (the browser closes the surrounding ``<p>`` when it parses the
``<div>``, the same fix-up an ordinary fenced ``<pre>`` already relies
on) — the body never paints either way."""
html = _render({"attached": ON})["attached"]
assert 'data-bor-locator="homelab/a.md#1-2"' in html
assert "BODY" not in html
# ---------------------------------------------------------------------------
# parity with the server's canonical_locator (task 06 item 3)
# ---------------------------------------------------------------------------
#: The D7 grammar table: input → the canonical string BOTH sides must
#: produce, or ``""`` for "not a locator at all".
PARITY_TABLE: dict[str, str] = {
"homelab/a.md": "homelab/a.md",
"homelab/a.md 12-40": "homelab/a.md#12-40",
"homelab/a.md:12-40": "homelab/a.md#12-40",
"homelab/a.md#12-40": "homelab/a.md#12-40",
"homelab/a.md#L12-L40": "homelab/a.md#12-40",
"homelab/a.md 12\u201340": "homelab/a.md#12-40",
"homelab/a.md 12": "homelab/a.md#12-12",
"homelab/a.md 12-40": "homelab/a.md#12-40",
"homelab/a.md 12 - 40": "homelab/a.md#12-40",
" homelab/a.md ": "homelab/a.md",
"homelab/deep/dir/a.md#L3": "homelab/deep/dir/a.md#3-3",
"homelab/a.md#l3-l7": "homelab/a.md#3-7",
"homelab/a.md L7": "homelab/a.md#7-7",
# the en dash is a RANGE separator, never the identity/range separator,
# so this row is ONE whole-document identity whose path contains one:
"homelab/a.md\u201312-40": "homelab/a.md\u201312-40",
# refusals — not locators at all:
"a.md": "",
"": "",
" ": "",
"homelab/a.md 0-5": "",
"homelab/a.md 40-12": "",
"homelab/a.md x-y": "",
"homelab/a.md 12-40 extra": "",
"bor homelab/a.md": "",
"homelab/a.md#": "",
"homelab/a.md:": "",
"homelab/a.md-": "",
"homelab/my note.md 12": "",
}
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_bor_locator_matches_the_python_canonical_locator() -> None:
"""The mirror is exact: for every row of the grammar table the JS
``borLocator`` and the Python ``canonical_locator`` agree — and the table
itself is checked against the D7 spec, so a SIMULTANEOUS drift on both
sides is caught too. A divergence here means hydration misses: the panel
would sit unresolved forever."""
from app.rag.verbatim import canonical_locator
script = (
_bor_locator_source()
+ "\nconst t = "
+ json.dumps(PARITY_TABLE)
+ ";\nconst out = {};\nfor (const k of Object.keys(t)) out[k] = borLocator(k);\n"
"console.log(JSON.stringify(out));\n"
)
got: dict[str, str] = json.loads(_run_node(script))
for raw, expected in PARITY_TABLE.items():
assert got[raw] == expected, f"borLocator({raw!r}) != {expected!r} (got {got[raw]!r})"
python = canonical_locator(raw) or ""
assert python == expected, (
f"canonical_locator({raw!r}) = {python!r} disagrees with the D7 "
f"table value {expected!r}"
)
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_fuzz_locator_parity() -> None:
"""A deterministic fuzz over the grammar's alphabet: the two
implementations must agree on every string, including the shapes nobody
hand-wrote (dangling separators, multi-bound ranges, lone ``L``s)."""
from app.rag.verbatim import canonical_locator
rng = random.Random(182)
alphabet = [
"a", "b", "/", ".", ":", "#", " ", "L", "l", "-", "\u2013", "1", "2", "9", "x", "\t",
]
samples = [
"".join(rng.choice(alphabet) for _ in range(rng.randint(0, 9))) for _ in range(400)
]
script = (
_bor_locator_source()
+ "\nconst t = "
+ json.dumps(samples)
+ ";\nconst out = [];\nfor (const k of t) out.push(borLocator(k));\n"
"console.log(JSON.stringify(out));\n"
)
got: list[str] = json.loads(_run_node(script))
for raw, js_value in zip(samples, got, strict=True):
python = canonical_locator(raw) or ""
assert js_value == python, f"parity break on {raw!r}: js={js_value!r} py={python!r}"
# ---------------------------------------------------------------------------
# no regression on the neighboring passes (the phase's byte-identity mandate)
# ---------------------------------------------------------------------------
def test_adjacent_renderer_fixtures_stay_pinned_where_they_live() -> None:
"""The phase-44 table, phase-138 blockquote and phase-180 image fixtures
are the byte-identity evidence for the passes this task sits next to —
they live in their own suites, so this pins that those suites still pin
them (a deleted fixture here would silently drop the guarantee)."""
for name, needle in [
("test_markdown_tables.py", "md-table-wrap"),
("test_markdown_blockquotes.py", '<blockquote class="md-quote">'),
("test_markdown_inline_links_images.py", 'class="md-img"'),
]:
text = _text(Path(__file__).resolve().parent / name)
assert needle in text, f"{name} must still pin {needle!r}"
+33 -11
View File
@@ -123,8 +123,12 @@ def test_agent_tools_off_is_the_three_base_tools() -> None:
"""LOCKED A2: toggle off (the default) → exactly the three base
tools, element-for-element equal to ``AGENT_TOOLS`` — and a FRESH
list (not the constant), so a caller mutating the offer can never
mutate the constant."""
off = agent_tools(_settings())
mutate the constant. Phase 182 (task 03) pin update, deliberate:
the flag pin passes ``answer_text=False`` explicitly — with
``answer_text`` ON (the shipped default) the offer additionally
carries ``show_text`` (pinned in ``tests/unit/test_agent.py``); the
``answer_images`` gate itself is unchanged."""
off = agent_tools(_settings(answer_text=False))
assert [t["function"]["name"] for t in off] == ["ls", "read", "grep"]
assert off == AGENT_TOOLS # element-for-element
assert off is not AGENT_TOOLS # a fresh list, never the constant itself
@@ -136,8 +140,12 @@ def test_agent_tools_on_appends_show_image() -> None:
"""Toggle on → the three base tools (byte-identical) plus the 4th
tool, ``show_image`` — the offer list the grounded turn passes to
the model request (LOCKED A2: ``BOR_ANSWER_IMAGES`` gates ONLY the
offer; the base three are unchanged)."""
on = agent_tools(_settings(answer_images=True))
offer; the base three are unchanged). Phase 182 (task 03) pin
update, deliberate: ``answer_text=False`` isolates the
``answer_images`` gate — with ``answer_text`` ON (the shipped
default) ``show_text`` rides LAST (pinned in
``tests/unit/test_agent.py``)."""
on = agent_tools(_settings(answer_images=True, answer_text=False))
assert [t["function"]["name"] for t in on] == ["ls", "read", "grep", "show_image"]
assert on[:3] == AGENT_TOOLS # the base three byte-identical
assert on[3] == SHOW_IMAGE_TOOL
@@ -166,7 +174,11 @@ def test_run_agent_gates_the_show_image_offer() -> None:
"""The wiring pin (completion criteria): a grounded turn's offer
(the ``tools`` on the model request, ``max_rounds`` > 0) is the
three base tools when the toggle is off — element-for-element the
pre-phase-124 list — and gains ``show_image`` only when it is on."""
pre-phase-124 list — and gains ``show_image`` only when it is on.
Phase 182 (task 03) pin update, deliberate: both apps pass
``answer_text=False`` to isolate the ``answer_images`` gate (with
the shipped default ON, ``show_text`` additionally rides last —
pinned in ``tests/unit/test_agent.py``)."""
class _ScriptedLLM:
"""One canned content-only round; records the ``tools`` every
@@ -207,12 +219,12 @@ def test_run_agent_gates_the_show_image_offer() -> None:
pass
llm_off = _ScriptedLLM()
asyncio.run(_consume(llm_off, _settings()))
asyncio.run(_consume(llm_off, _settings(answer_text=False)))
assert len(llm_off.requests) == 1
assert llm_off.requests[0][1] == AGENT_TOOLS # off: element-for-element pre-124
llm_on = _ScriptedLLM()
asyncio.run(_consume(llm_on, _settings(answer_images=True)))
asyncio.run(_consume(llm_on, _settings(answer_images=True, answer_text=False)))
sent = llm_on.requests[0][1]
assert sent is not None # the grounded turn (max_rounds > 0) offers tools
assert [t["function"]["name"] for t in sent] == [
@@ -427,14 +439,24 @@ def test_plan_turn_grounded_carries_clause_exactly_when_toggle_on() -> None:
IDENTICAL to the pre-task build when ``answer_images`` is off (the
default — the pre-phase pins pass untouched) and gains the clause
(inside ``<tools>``) exactly when it is on."""
off = _plan3(0.9, _settings())
off_settings = _settings()
off = _plan3(0.9, off_settings)
assert off.deflected is False
assert off.system_prompt == build_high_prompt([_P3_DOC])
# Phase 182 (task 04): the reconstruction mirrors the OTHER display
# clause's flag too (BOR_ANSWER_TEXT, default ON) so this pin stays
# about answer_images alone — the show_text clause is pinned in
# tests/unit/test_prompts.py.
assert off.system_prompt == build_high_prompt(
[_P3_DOC], show_text=off_settings.answer_text
)
assert SHOW_IMAGE_CLAUSE not in off.system_prompt
on = _plan3(0.9, _settings(answer_images=True))
on_settings = _settings(answer_images=True)
on = _plan3(0.9, on_settings)
assert on.deflected is False
assert on.system_prompt == build_high_prompt([_P3_DOC], show_image=True)
assert on.system_prompt == build_high_prompt(
[_P3_DOC], show_image=True, show_text=on_settings.answer_text
)
assert SHOW_IMAGE_CLAUSE in on.system_prompt
assert on.system_prompt.index("<tools>") < on.system_prompt.index(SHOW_IMAGE_CLAUSE)
assert on.system_prompt.index(SHOW_IMAGE_CLAUSE) < on.system_prompt.index("</tools>")
+8 -3
View File
@@ -599,13 +599,18 @@ def test_meta_rows_append_to_msg_body_never_into_the_bubble() -> None:
def test_restore_renders_brain_text_through_the_pinned_call_shape() -> None:
"""CORNER: the restore. The phase-14 restore renders every brain
record through `addMessage("brain", renderMarkdown(m.text), true)`
— with `m.text === ""` the node-pinned `renderMarkdown("") === ""
record through `addMessage("brain", renderMarkdown(m.text, { verbatim:
true }), true)` (the option is phase 182's verbatim opt-in — it changes
which FENCES render, never whether the text does) — with `m.text === ""`
the node-pinned `renderMarkdown("") === ""`
` (pinned above) makes the bubble CHILD-LESS ⇒ `:empty` ⇒ hidden:
an empty persisted answer paints no box, through the SAME rule
(locked A3) — no restore-path branch was added."""
js = _js()
assert js.count('addMessage("brain", renderMarkdown(m.text), true)') == 1
assert (
js.count('addMessage("brain", renderMarkdown(m.text, { verbatim: true }), true)')
== 1
)
def test_shared_page_builds_the_identical_msg_structure_no_change_needed() -> None:
+50
View File
@@ -18,10 +18,13 @@ from app.rag.prompts import (
ORIGIN_RULE,
PERSONA,
SEARCH_DISCIPLINE_RULE,
SHOW_IMAGE_CLAUSE,
SHOW_TEXT_CLAUSE,
SUGGEST_INTRO,
TOOLS_SECTION,
_base,
build_deflect_prompt,
build_high_prompt,
)
@@ -323,3 +326,50 @@ def test_suggest_intro_byte_locked() -> None:
) in SUGGEST_INTRO
# The retired phase-118 final sentence is gone from the constant.
assert "Cite the document(s) you used, by path." not in SUGGEST_INTRO
# ---------- SHOW_TEXT_CLAUSE (phase 182, task 04: verbatim display) ----------
#: Anchors for ``SHOW_TEXT_CLAUSE`` — the ``show_text`` / ```bor``` locator
#: teaching appended INSIDE ``<tools>`` when ``BOR_ANSWER_TEXT`` is on
#: (phase 182, LOCKED A1–A3/A6). Pinned the way ``TOOLS_SECTION`` is
#: (sha256 + exact prefix + total length) because this clause IS the fix
#: for the retype bug the phase exists for: a silent wording drift here
#: re-opens it (the tool alone never teaches that the user copies from
#: what is DISPLAYED, nor the fence syntax).
SHOW_TEXT_CLAUSE_SHA256 = "e03a8153ca6cde61c9437f3b66b4fa07cd2a3f9f2e24e3bb8c37ca1d7c7e9e3b"
SHOW_TEXT_CLAUSE_LEN = 1179
SHOW_TEXT_CLAUSE_PREFIX = (
"`show_text` displays part of a document to the user EXACTLY as stored"
)
def test_show_text_clause_byte_locked() -> None:
"""The clause is LOCKED copy: sha256 + exact prefix + total length,
plus the two sentences the phase's contract depends on verbatim (the
copy-from-the-display rule and the server-owns-the-numbers rule)."""
assert len(SHOW_TEXT_CLAUSE) == SHOW_TEXT_CLAUSE_LEN
assert _sha256(SHOW_TEXT_CLAUSE) == SHOW_TEXT_CLAUSE_SHA256
assert SHOW_TEXT_CLAUSE.startswith(SHOW_TEXT_CLAUSE_PREFIX)
assert SHOW_TEXT_CLAUSE.endswith("For an image use `show_image` instead.")
assert "the user copies from what is displayed, not from your prose" in SHOW_TEXT_CLAUSE
assert "the numbers you display are the server's, never yours" in SHOW_TEXT_CLAUSE
def test_high_prompt_display_clause_composition_is_byte_locked() -> None:
"""One mechanism, four combinations (phase 182, task 04): neither
flag reproduces the untouched :data:`TOOLS_SECTION` tail,
``show_image`` alone reproduces the phase-124 bytes exactly,
``show_text`` alone appends only the text clause, and both append
the image clause, a blank line, the text clause, then the closing
tag — ``TOOLS_SECTION`` itself is never edited (its own sha pin
above holds)."""
off = build_high_prompt([])
assert off.endswith(TOOLS_SECTION)
head = off[: -len("</tools>")]
assert build_high_prompt([], show_image=True) == head + SHOW_IMAGE_CLAUSE + "\n</tools>"
assert build_high_prompt([], show_text=True) == head + SHOW_TEXT_CLAUSE + "\n</tools>"
assert (
build_high_prompt([], show_image=True, show_text=True)
== head + SHOW_IMAGE_CLAUSE + "\n\n" + SHOW_TEXT_CLAUSE + "\n</tools>"
)
+175
View File
@@ -30,6 +30,15 @@ discipline rules stay byte-identical): the teaching refusals in
:mod:`app.rag.agent` re-state the same contract; the ``<tools>``
marker keying (HIGH only) is unchanged.
And the phase-182 ``show_text`` clause (task 04, LOCKED A1–A3/A6): the
verbatim-display teaching appended INSIDE ``<tools>`` when
``BOR_ANSWER_TEXT`` is on — the user copies from what is DISPLAYED, never
from the model's retype, and the inline ```bor <source/path> [range]```
fence places a server-resolved block. The clause composes with the
phase-124 ``show_image`` clause through one mechanism, and every build
the flags do not select stays byte-identical (the constant's own sha pin
lives in :mod:`tests.unit.test_prompt_lock`).
And the phase-119 cite discipline (task 04, LOCKED A5): the intro's
final sentence ("cite the document(s) you used, by path", phase 118)
is REPLACED — the HIGH prompt carries the discipline sentence exactly
@@ -40,6 +49,7 @@ the LOW prompt and ``PERSONA`` / ``TOOLS_SECTION`` stay byte-identical
from __future__ import annotations
import inspect
import re
import uuid
from datetime import UTC, datetime
@@ -52,6 +62,7 @@ from app.rag.prompts import (
PERSONA,
SEARCH_DISCIPLINE_RULE,
SHOW_IMAGE_CLAUSE,
SHOW_TEXT_CLAUSE,
SUGGEST_INTRO,
TOOLS_SECTION,
_base,
@@ -1281,6 +1292,170 @@ def test_show_image_clause_is_last_for_every_high_shape() -> None:
assert SHOW_IMAGE_CLAUSE not in off # the toggle-off twin carries none of it
# ---------- phase 182 (task 04, LOCKED A1–A3/A6): the show_text clause ----------
#
# The clause is the fix for the retype bug: nothing in the pre-phase prompt
# ever said the user copies from what is DISPLAYED, so the model retyped
# files from context and every retype differed from the stored bytes by a
# character. Pinned three ways: the locked sentence order (the behaviour
# the E2E and the persona depend on), the byte-identity of every build the
# flag does NOT select, and the composition of the two display clauses
# through the one "section minus its final </tools>" mechanism.
#: The clause's sentence anchors, in the locked order (task 04's work
#: list): display contract → when to call → do-NOT-retype → the ```bor```
#: placement syntax → the numbers belong to the server → the failure rule
#: → the image pointer.
SHOW_TEXT_ANCHORS: tuple[str, ...] = (
"displays part of a document to the user EXACTLY as stored",
"the user copies from what is displayed, not from your prose",
"Call it whenever the user wants to SEE, COPY or RUN a file",
"a quadlet, a config, a script, a unit file",
"do NOT repeat the displayed lines in your answer",
"may differ from the file by a character, and the user cannot tell",
"write a locator line on its own: three backticks followed by `bor`",
"the combined `source/path`, and optionally the line range (` 12-40`)",
"Leave its body EMPTY — the app replaces it with the exact stored text",
"Never type the file's content inside a `bor` block",
"Never guess line numbers — use the numbers `grep` and `read` reported",
"OMIT the range and display the whole document",
"the numbers you display are the server's, never yours",
"If `show_text` refuses, say what you could not display",
"do not reconstruct it from memory",
"For an image use `show_image` instead",
)
def test_show_text_clause_carries_every_locked_rule_in_order() -> None:
"""Every sentence of the contract is present, in the locked order, and
the clause is a clause — one paragraph, no ``<tools>`` markup of its
own (the ``SHOW_IMAGE_CLAUSE`` precedent)."""
positions = [SHOW_TEXT_CLAUSE.index(anchor) for anchor in SHOW_TEXT_ANCHORS]
assert positions == sorted(positions), SHOW_TEXT_ANCHORS
assert "\n" not in SHOW_TEXT_CLAUSE
assert SHOW_TEXT_CLAUSE.startswith("`show_text`")
assert SHOW_TEXT_CLAUSE.endswith("instead.")
assert "<tools>" not in SHOW_TEXT_CLAUSE and "</tools>" not in SHOW_TEXT_CLAUSE
# The fence language is named as a token, never as a stray substring.
assert re.search(r"(?<![A-Za-z0-9_])bor(?![A-Za-z0-9_])", SHOW_TEXT_CLAUSE)
def test_high_prompt_default_stays_byte_identical_without_show_text() -> None:
"""Completion pin: ``show_text`` defaults to ``False`` — the default
build and the explicit ``show_text=False`` are byte-identical to each
other and end with the untouched ``TOOLS_SECTION``, with no trace of
the tool or the fence language (the sha pins in
:mod:`tests.unit.test_prompt_lock` pass UNMODIFIED)."""
doc = _doc("kubernetes.md", "Talos Linux on three nodes.", "Kubernetes Homelab Cluster")
plain = build_high_prompt([doc])
assert build_high_prompt([doc], show_text=False) == plain
assert plain.endswith(TOOLS_SECTION)
assert SHOW_TEXT_CLAUSE not in plain
assert "show_text" not in plain
assert not re.search(r"(?<![A-Za-z0-9_])bor(?![A-Za-z0-9_])", plain)
empty = build_high_prompt([])
assert build_high_prompt([], show_text=False) == empty
assert empty.endswith(TOOLS_SECTION)
assert SHOW_TEXT_CLAUSE not in empty
def test_high_prompt_show_text_clause_lands_inside_tools_after_the_copy() -> None:
"""``show_text=True`` builds EXACTLY the toggle-off prompt with its
final ``</tools>`` replaced by the clause (on its own line, after the
existing three-tool copy) + the closing tag — the three-tool copy is
byte-identical, ``TOOLS_SECTION`` is unedited, and the clause is the
last thing the model reads."""
doc = _doc("kubernetes.md", "Talos Linux on three nodes.", "Kubernetes Homelab Cluster")
off = build_high_prompt([doc])
on = build_high_prompt([doc], show_text=True)
assert on == off[: -len("</tools>")] + SHOW_TEXT_CLAUSE + "\n</tools>"
clause_start = on.index(SHOW_TEXT_CLAUSE)
assert on[:clause_start] == off[: -len("</tools>")]
assert on[on.index("<tools>") : clause_start] == TOOLS_SECTION[: -len("</tools>")]
assert "Answer as soon as you have what you need." in on[:clause_start]
assert on.endswith(SHOW_TEXT_CLAUSE + "\n</tools>")
assert on.index("Answer as soon as you have what you need.") < clause_start
assert clause_start < on.index("</tools>")
assert on.count(SHOW_TEXT_CLAUSE) == 1
assert on.count("<tools>") == 1 and on.count("</tools>") == 1
def test_high_prompt_display_clauses_compose_independently() -> None:
"""The two display flags are independent (LOCKED A1): ``show_image``
only reproduces the phase-124 bytes exactly, ``show_text`` only is the
three-tool copy plus the text clause, both puts the image clause
FIRST and the text clause last, and neither is byte-identical to
pre-phase — no combination leaks the other's clause."""
doc = _doc("kubernetes.md", "Talos Linux on three nodes.", "Kubernetes Homelab Cluster")
neither = build_high_prompt([doc])
head = neither[: -len("</tools>")]
image = build_high_prompt([doc], show_image=True)
text = build_high_prompt([doc], show_text=True)
both = build_high_prompt([doc], show_image=True, show_text=True)
assert neither.endswith(TOOLS_SECTION)
assert image == head + SHOW_IMAGE_CLAUSE + "\n</tools>"
assert text == head + SHOW_TEXT_CLAUSE + "\n</tools>"
assert both == head + SHOW_IMAGE_CLAUSE + "\n\n" + SHOW_TEXT_CLAUSE + "\n</tools>"
assert SHOW_TEXT_CLAUSE not in image and SHOW_IMAGE_CLAUSE not in text
assert both.index(SHOW_IMAGE_CLAUSE) < both.index(SHOW_TEXT_CLAUSE)
for on in (image, text, both):
assert on[: on.index("<tools>")] == neither[: on.index("<tools>")]
assert on.count("<tools>") == 1 and on.count("</tools>") == 1
def test_high_prompt_empty_documents_show_text_clause_still_in_tools() -> None:
"""The "(no documents matched…)" body + ``show_text=True``: the clause
still lands inside ``<tools>`` — the grounded turn may still ``ls`` /
``grep`` its way to a file and display it."""
off = build_high_prompt([])
on = build_high_prompt([], show_text=True)
assert on == off[: -len("</tools>")] + SHOW_TEXT_CLAUSE + "\n</tools>"
assert "(no documents matched — do not invent specifics)" in on
assert on.index("<tools>") < on.index(SHOW_TEXT_CLAUSE) < on.index("</tools>")
assert on.endswith(SHOW_TEXT_CLAUSE + "\n</tools>")
def test_show_text_clause_is_last_for_every_high_shape() -> None:
"""Steering + KB overview + folder lines + documents all present: the
section order is UNCHANGED (``<relevance>`` → ``<knowledge_base>`` →
``<tuning>`` → ``<documents>`` → ``<tools>``) and the text clause is
still the last thing inside ``<tools>`` before the closing tag."""
doc = _doc("deploy/k8s.md", "CONTENT_SENTINEL", "K8s Deploy", summary="K8s deploy notes.")
lines = ["Homelab/deploy/: k8s.md (1 doc)"]
off = build_high_prompt([doc], notes=["be concise"], kb_overview=OVERVIEW, folder_lines=lines)
on = build_high_prompt(
[doc],
notes=["be concise"],
kb_overview=OVERVIEW,
folder_lines=lines,
show_text=True,
)
assert on == off[: -len("</tools>")] + SHOW_TEXT_CLAUSE + "\n</tools>"
clause_start = on.index(SHOW_TEXT_CLAUSE)
assert on[on.index("<tools>") : clause_start] == TOOLS_SECTION[: -len("</tools>")]
for section in ("<relevance>", "<knowledge_base>", "<tuning>", "<documents>"):
assert on.index(section) < on.index("<tools>")
assert on.index("K8s deploy notes.") < on.index("<tools>")
assert on.endswith(SHOW_TEXT_CLAUSE + "\n</tools>")
assert SHOW_TEXT_CLAUSE not in off
def test_show_text_clause_rides_only_the_high_prompt() -> None:
"""Neither the tool name nor the fence language ever reaches the
deflection prompt (phase 182's D9: a deflected turn has no tools, and
resolving a locator there would leak KB text through a turn that just
disclaimed it) — and ``build_deflect_prompt`` takes no such flag."""
assert "show_text" not in inspect.signature(build_deflect_prompt).parameters
for prompt in (
build_deflect_prompt(["T1", "T2"]),
build_deflect_prompt(["T1"], notes=["be concise"], kb_overview=OVERVIEW),
build_deflect_prompt([]),
):
assert SHOW_TEXT_CLAUSE not in prompt
assert "show_text" not in prompt
assert not re.search(r"(?<![A-Za-z0-9_])bor(?![A-Za-z0-9_])", prompt)
# ---------- phase 125 (task 05, LOCKED A4): the image context line ----------
# An enriched image doc (tasks 03/04: non-NULL ``image_context`` in the
# "Referenced in …" block shape) leads its ``<document>`` suggestion
+33 -10
View File
@@ -63,6 +63,11 @@ from pathlib import Path
FRONTEND = Path(__file__).resolve().parents[2] / "frontend"
APP_JS = FRONTEND / "assets" / "app.js"
# Phase 182 (task 07): the ONE clipboard implementation moved to the shared
# verbatim panel client; the share path delegates to it, so the fallback's
# internals are pinned there (the owner-locked behavior is re-pinned, not
# dropped).
VERBATIM_JS = FRONTEND / "assets" / "verbatim.js"
# Phase 76 (task 02): SOURCES_HTML + GIT_SOURCES_HTML dropped — both
# views are folded into the shell (index.html); both files deleted.
INDEX_HTML = FRONTEND / "index.html"
@@ -533,18 +538,36 @@ def test_share_current_chat_save_then_share_branch() -> None:
assert finally_idx != -1 and "shareBtn.disabled = false" in body[finally_idx:], (
"the button is re-enabled in the finally — never stale"
)
# The clipboard + fallback helpers live in app.js (the chat page's
# copy of the per-page helper).
# Phase 182 (task 07): the clipboard + fallback pair MOVED to
# assets/verbatim.js so the answer's verbatim panels and ordinary code
# blocks share ONE degradation with the share link. The owner-locked
# behavior is unchanged and re-pinned on its new home: the same host (the
# composer, beside the status line — it rides the delegation), the same
# transient `.share-link-fallback` field, the same select-on-focus, the
# same XSS contract (textContent only), the same title string.
copy = _fn(js, "copyShareLinkWithFallback")
assert "navigator.clipboard.writeText(absoluteUrl)" in copy
assert 'field.className = "share-link-fallback"' in copy
assert "field.href = absoluteUrl" in copy
assert "field.textContent = absoluteUrl" in copy, "XSS contract: textContent only"
assert 'field.addEventListener("focus", () => selectAllInField(field))' in copy, (
"select-on-focus — the field-like behavior"
assert "copyTextWithFallback(" in copy, (
"the share path delegates — ONE clipboard implementation"
)
assert "composer.appendChild(field)" in copy, "near the status line (the composer)"
sel = _fn(js, "selectAllInField")
assert "absoluteUrl," in copy, "the shared URL is what gets copied"
assert "composer," in copy, "near the status line (the composer)"
assert '"Share link — click, then copy (Ctrl/Cmd+C)"' in copy, (
"the owner-locked field title rides the delegation byte-identically"
)
ver = VERBATIM_JS.read_text(encoding="utf-8")
impl = _fn(ver, "copyTextWithFallback")
assert "await navigator.clipboard.writeText(text)" in impl
assert 'field.className = "share-link-fallback"' in impl
assert "field.href = text" in impl, "a URL field carries the href"
assert "field.textContent = text" in impl, "XSS contract: textContent only"
assert (
'field.addEventListener("focus", () => selectAllInFallbackField(field))' in impl
), "select-on-focus — the field-like behavior"
assert "host.appendChild(field)" in impl, "appended to the caller's host"
assert (
'document.querySelectorAll(".share-link-fallback").forEach((el) => el.remove())' in impl
), "one field at a time (a new offer replaces the old)"
sel = _fn(ver, "selectAllInFallbackField")
assert "document.createRange()" in sel and "selectNodeContents(el)" in sel
+106
View File
@@ -24,6 +24,8 @@ report).
"""
from __future__ import annotations
from typing import Any
import pytest
from pydantic import ValidationError
@@ -39,6 +41,7 @@ from app.schemas import (
SavedChatUpdate,
SourceRef,
ToolCall,
VerbatimBlockRef,
)
# --- boundary constants (phase 83, A1 — mirror sources in app/schemas.py) ---
@@ -1136,3 +1139,106 @@ def test_segments_alias_is_the_discriminated_union() -> None:
assert isinstance(thinking, ChatThinkingSegment)
tool = adapter.validate_python({"kind": "tool", "name": "ls"})
assert isinstance(tool, ChatToolSegment)
# ---------------------------------------------------------------------------
# Phase 182 (task 05): VerbatimBlockRef + the saved record's answer_text
# ---------------------------------------------------------------------------
def _verbatim_block(**overrides: Any) -> dict[str, Any]:
"""One resolved block — the done frame's item shape."""
block: dict[str, Any] = {
"source": "Homelab",
"path": "quadlet/gufo.container",
"title": "gufo.container",
"start_line": 3,
"end_line": 9,
"total_lines": 21,
"text": "[Unit]\nDescription=gufo",
"truncated": False,
"locator": "Homelab/quadlet/gufo.container#3-9",
}
block.update(overrides)
return block
def test_verbatim_block_ref_round_trips_every_field() -> None:
"""Every field of the frame's item validates and serializes back
unchanged (the client matches on ``locator`` and renders the
SERVER's ``text`` + extent, so none of them may be dropped)."""
block = VerbatimBlockRef.model_validate(_verbatim_block())
assert block.model_dump() == _verbatim_block()
def test_verbatim_block_ref_carries_the_phase_83_text_cap() -> None:
"""``text`` is capped at the SAME 600 000 as the answer text (so an
operator raising ``BOR_SHOW_TEXT_MAX_CHARS`` never hits the wire
limit), and one character past it rejects."""
assert len(VerbatimBlockRef.model_validate(_verbatim_block(text="x" * 600_000)).text) == (
600_000
)
with pytest.raises(ValidationError):
VerbatimBlockRef.model_validate(_verbatim_block(text="x" * 600_001))
@pytest.mark.parametrize("field", ["start_line", "end_line", "total_lines"])
def test_verbatim_block_ref_refuses_negative_line_numbers(field: str) -> None:
with pytest.raises(ValidationError):
VerbatimBlockRef.model_validate(_verbatim_block(**{field: -1}))
def test_saved_record_answer_text_round_trips_and_stays_optional() -> None:
"""A brain record accepts ``answer_text`` (the phase-113 lesson — the
done-time auto-save must never 422) and drops the key when it is
``None`` or empty (A9: absent, never ``null``, never ``[]``)."""
with_blocks = ChatMessage.model_validate(
{"who": "brain", "text": "shown above", "answer_text": [_verbatim_block()]}
)
assert with_blocks.model_dump()["answer_text"] == [_verbatim_block()]
for value in (None, []):
record = ChatMessage.model_validate(
{"who": "brain", "text": "nothing shown", "answer_text": value}
)
assert "answer_text" not in record.model_dump()
def test_saved_record_answer_text_list_is_capped() -> None:
"""The list cap (the phase-83 SEC-05 style, the ``tools`` /
``segments`` number): 51 blocks reject, 50 fit — far past the
``show_text_max_blocks`` default of 5."""
blocks = [_verbatim_block(locator=f"Homelab/a{i}.md") for i in range(51)]
with pytest.raises(ValidationError):
ChatMessage.model_validate(
{"who": "brain", "text": "too many", "answer_text": blocks}
)
assert len(
ChatMessage.model_validate(
{"who": "brain", "text": "enough", "answer_text": blocks[:50]}
).answer_text
or []
) == 50
def test_saved_chat_total_budget_counts_verbatim_block_text() -> None:
"""Phase 182 (task 05): the blocks are a SECOND byte channel on the
anonymous save surface, so they count against the same
8 000 000-character total (the SEC-05 invariant) — 15 records whose
blocks total 8 400 000 reject even though their ``text`` is small."""
turns = 15
size = 560_000
assert turns * size > TOTAL_BUDGET
heavy = [
{
"who": "brain",
"text": "short",
"answer_text": [_verbatim_block(text="x" * size)],
}
for _ in range(turns)
]
with pytest.raises(ValidationError) as exc:
SavedChatCreate.model_validate({"messages": heavy})
assert "8,000,000-character budget" in exc.value.errors()[0]["msg"]
# 14 × 560 000 = 7 840 000 fits (plus the small text, still inside).
assert len(SavedChatCreate.model_validate({"messages": heavy[:-1]}).messages) == 14
+39
View File
@@ -149,6 +149,45 @@ def test_docs_token_is_a_secret_live_overridable_row() -> None:
assert "docs_token" not in NON_OVERRIDABLE
def test_verbatim_display_rows_are_plain_live_overridable() -> None:
"""Phase 182 (task 01, LOCKED A8): the three verbatim-display rows sit
in ``Agent tools`` (the ``read_max_chars`` group, beside it) with the
``Answer text`` / ``Show text max chars`` / ``Show text max blocks``
labels and are plain — not ``secret`` (a bool and two ints), not
``restart_required`` (the chat request path reads them per turn via
``get_settings()`` — live), overridable (the overlay is generic over
``bool``/``int`` fields). ``answer_text`` is the master switch and the
two budgets accept ``0`` (inert) — a negative value fails startup, not
the panel."""
for key, label in (
("answer_text", "Answer text"),
("show_text_max_chars", "Show text max chars"),
("show_text_max_blocks", "Show text max blocks"),
):
meta = CATALOG[key]
assert meta.group == "Agent tools"
assert meta.label == label
assert meta.secret is False
assert meta.restart_required is False
assert meta.overridable is True
assert key not in SECRETS
assert key not in RESTART_REQUIRED
assert key not in NON_OVERRIDABLE
def test_verbatim_display_field_types() -> None:
"""Phase 182 (task 01): the master switch is a ``bool`` and the two
budgets are ``int``s with the LOCKED A8 defaults (``True`` /
``12000`` / ``5``) — the types the panel renders its input for and the
overlay coerces from the stored raw string."""
assert Settings.model_fields["answer_text"].annotation is bool
assert Settings.model_fields["show_text_max_chars"].annotation is int
assert Settings.model_fields["show_text_max_blocks"].annotation is int
assert Settings.model_fields["answer_text"].default is True
assert Settings.model_fields["show_text_max_chars"].default == 12_000
assert Settings.model_fields["show_text_max_blocks"].default == 5
def test_labels_nonempty_and_unique() -> None:
labels = [meta.label for meta in CATALOG.values()]
assert all(label.strip() for label in labels), "empty label in the catalogue"
+30 -9
View File
@@ -139,21 +139,31 @@ def test_shared_html_identical_header_guest_safe() -> None:
def test_shared_html_scripts_and_no_controls() -> None:
"""Script load order (the house pattern): brand.js classic FIRST,
the classic markdown renderer second, the shared.js module last —
ALL with ABSOLUTE /assets/ paths: the page is served from the
the classic markdown renderer second, the classic verbatim panel client
third (phase 182 task 07 — it must be a global BEFORE the module, and
AFTER markdown.js whose placeholders it hydrates), the shared.js module
last — ALL with ABSOLUTE /assets/ paths: the page is served from the
NESTED /shared/<token> route, where a relative "assets/…" ref
would resolve to /shared/assets/… and 404. NO direct header.js
<script> tag (single-evaluation design — shared.js imports it
relatively). Zero controls (owner-locked): no document-modal, no
composer, no Send/Save/Share/New chat buttons, no form element,
and the ONLY <button>s on the page are the shared bar's own (the
hamburger + the two sign-out copies)."""
and the ONLY <button>s the page itself ships are the shared bar's own
(the hamburger + the two sign-out copies). Phase 182's narrow exception
(the panel's Copy button + its plain viewer link) is injected by the
SHARED verbatim.js client, not by this markup, and is pinned in
tests/unit/test_frontend_verbatim_blocks.py."""
html = _html()
srcs = re.findall(r'<script[^>]*src="([^"]+)"', html)
assert srcs == ["/assets/brand.js", "/assets/markdown.js", "/assets/shared.js"], (
f"shared.html must load brand.js + markdown.js (classic, absolute "
f"/assets/ paths — the nested route breaks relative refs) and the "
f"shared.js module, in that order, got {srcs}"
assert srcs == [
"/assets/brand.js",
"/assets/markdown.js",
"/assets/verbatim.js",
"/assets/shared.js",
], (
f"shared.html must load brand.js + markdown.js + verbatim.js (classic, "
f"absolute /assets/ paths — the nested route breaks relative refs) and "
f"the shared.js module, in that order, got {srcs}"
)
# The stylesheet too (a relative href would break the same way).
assert '<link rel="stylesheet" href="/assets/styles.css">' in html
@@ -289,7 +299,18 @@ def test_render_shared_message_record_shape() -> None:
js = _js()
body = _fn(js, "renderSharedMessage")
assert 'addSharedMessage("user", renderMarkdown(m.text))' in body
assert 'addSharedMessage("brain", renderMarkdown(m.text))' in body
# Phase 182 (task 07): the brain renderer opts INTO the verbatim pass
# (the chat page's restore does the same — parity), and hydrates the
# placeholders + appends the unlocated blocks from the record's
# `answer_text` before the source chips. The user renderer stays
# one-argument.
assert 'addSharedMessage("brain", renderMarkdown(m.text, { verbatim: true }))' in body
assert "hydrateVerbatim(" in body, "the shared page hydrates (parity)"
assert "appendUnlocatedVerbatim(" in body
assert "enhanceAnswerCodeBlocks(wrap)" in body
assert body.index("addAnswerImage(") < body.index("hydrateVerbatim(") < body.index(
"addSources(wrap, m.sources)"
), "the panel band matches the chat page: image, panels, then the chips"
assert "renderMarkdown" in body, "the escape-first global renderer"
assert "function renderMarkdown" not in js, (
"shared.js must NOT define its own renderer — markdown.js is the one copy"
+705
View File
@@ -0,0 +1,705 @@
"""Unit: the phase-182 verbatim resolver (``app/rag/verbatim.py``, task 02).
The module is the single source of truth for "which exact lines of which
indexed document, and what are their bytes", so the suite covers its three
layers: the pure grammar (:func:`canonical_locator`, :func:`fences_in`),
the pure slicing (:func:`slice_lines`, :func:`cap_slice`), and the
resolver (:func:`document_by_identity`, :func:`block_for`,
:func:`resolve_answer_blocks`).
Session-fixture style follows the agent suite (``tests/unit/test_agent.py``):
DETACHED :class:`~app.models.Document` rows plus a stand-in session for the
one bare ``(source, path)`` lookup — the resolver's SQL is a single
``WHERE`` on the table's unique pair, so a database adds nothing the
contract needs (and the suite runs without the stack up).
"""
from __future__ import annotations
import ast
import inspect
import re
import uuid
from typing import Any, cast
from unittest.mock import MagicMock
import pytest
from sqlalchemy import Select
from sqlalchemy.orm import Session
import app.rag.verbatim as verbatim_module
from app.config import Settings
from app.models import Document
from app.rag.retriever import TRUNCATION_MARKER
from app.rag.verbatim import (
LOCATOR_LANG,
SliceResult,
VerbatimBlock,
block_for,
canonical_locator,
cap_slice,
document_by_identity,
fences_in,
resolve_answer_blocks,
slice_lines,
)
def _settings(**kwargs: Any) -> Settings:
kwargs.setdefault("_env_file", None)
return Settings(**kwargs) # pyright: ignore[reportCallIssue]
def _doc(
source: str = "homelab",
path: str = "a.md",
title: str = "The Title",
content: str = "one\ntwo\nthree",
*,
is_image: bool = False,
) -> Document:
"""A DETACHED row — the resolver reads ``source``/``path``/``title``/
``content``/``is_image`` and nothing else, so no session is needed."""
return Document(
id=uuid.uuid4(),
source=source,
path=path,
full_path=f"/srv/sources/{source}/{path}",
title=title,
content=content,
content_hash="0" * 64,
is_image=is_image,
)
class _FakeDB:
"""Stand-in session for :func:`document_by_identity`'s single lookup.
Records every ``(source, path)`` pair it was asked for, so a test can
assert the FIRST-slash split reaches the ``WHERE`` clause verbatim —
including the traversal-shaped identity, which is simply a pair no row
has.
"""
def __init__(self, docs: list[Document]) -> None:
self.docs = {(d.source, d.path): d for d in docs}
self.lookups: list[tuple[str, str]] = []
def scalar(self, stmt: Select[tuple[Document]]) -> Document | None:
params = stmt.compile().params
pair = (str(params["source_1"]), str(params["path_1"]))
self.lookups.append(pair)
return self.docs.get(pair)
def _session(docs: list[Document]) -> _FakeDB:
return _FakeDB(docs)
def _real(session: _FakeDB) -> Session:
return cast("Session", session)
# --------------------------------------------------------------------------
# canonical_locator — the tolerant grammar in, ONE canonical string out
# --------------------------------------------------------------------------
def test_whole_document_locator_is_itself() -> None:
assert canonical_locator("homelab/a.md") == "homelab/a.md"
@pytest.mark.parametrize(
"raw",
[
"homelab/a.md 12-40",
"homelab/a.md:12-40",
"homelab/a.md#12-40",
"homelab/a.md#L12-L40",
"homelab/a.md L12-L40",
"homelab/a.md 12\u201340", # en dash, the grep-style range
"homelab/a.md 12 - 40 ", # stray spaces around the dash
],
)
def test_every_range_spelling_canonicalizes(raw: str) -> None:
assert canonical_locator(raw) == "homelab/a.md#12-40"
def test_bare_line_number_is_one_line_block() -> None:
assert canonical_locator("homelab/a.md 12") == "homelab/a.md#12-12"
def test_identity_with_slashes_and_case_survives_untouched() -> None:
"""Paths are case-sensitive row values; only the FIRST slash splits."""
assert canonical_locator("Homelab/Notes/Deep/a.md") == "Homelab/Notes/Deep/a.md"
@pytest.mark.parametrize(
"raw",
[
"a.md", # no source
"",
" ",
"homelab/a.md 0-5", # non-positive bound
"homelab/a.md 40-12", # start > end
"homelab/a.md x-y", # non-numeric bound
"homelab/a.md 12-40 extra", # trailing junk after the range
"bor homelab/a.md", # the caller left the language token in
"bor",
"homelab/a.md#", # dangling separator, no range
"homelab/a.md:",
],
)
def test_non_locators_are_refused(raw: str) -> None:
assert canonical_locator(raw) is None
def test_whitespace_bearing_path_with_a_range_is_refused() -> None:
"""The documented limit of the tolerant grammar: the fence's info
string is ONE whitespace-free identity plus the range."""
assert canonical_locator("homelab/my notes/a.md 12-40") is None
# --------------------------------------------------------------------------
# fences_in — the raw locator strings, in answer order
# --------------------------------------------------------------------------
def test_single_fence_yields_its_raw_locator() -> None:
answer = "Before.\n```bor homelab/a.md#12-40\nanything the model typed\n```\nAfter."
assert fences_in(answer) == ["homelab/a.md#12-40"]
def test_fences_come_back_in_answer_order_and_deduped_first() -> None:
answer = (
"```bor homelab/b.md\nb\n```\n"
"words\n"
"```bor homelab/a.md\na\n```\n"
"```bor homelab/b.md\nb again\n```\n"
)
assert fences_in(answer) == ["homelab/b.md", "homelab/a.md"]
def test_bare_language_fence_is_an_ordinary_code_block() -> None:
"""A `` ```bor `` with no path is NOT a locator — and it must not
swallow the real fence that follows it."""
answer = "```bor\nplain code\n```\n```bor homelab/a.md\na\n```"
assert fences_in(answer) == ["homelab/a.md"]
def test_unclosed_fence_contributes_nothing() -> None:
"""Defensive: a half-streamed answer never resolves a partial locator."""
assert fences_in("intro\n```bor homelab/a.md#12-40\nhalf streamed") == []
def test_non_bor_fences_are_ignored() -> None:
answer = "```python\nx = 1\n```\nplain text, no locator"
assert fences_in(answer) == []
def test_nested_backticks_end_the_fence_first() -> None:
"""Documented limitation, the renderer's rule: the fence closes at the
FIRST closer, so text after it is only prose unless it opens a fence."""
answer = "```bor homelab/a.md\n```\nor ```bor homelab/b.md``` inline\n```"
assert fences_in(answer) == ["homelab/a.md"]
def test_indented_fence_opens() -> None:
assert fences_in(" ```bor homelab/a.md\na\n ```") == ["homelab/a.md"]
# --------------------------------------------------------------------------
# slice_lines — the one shared coordinate system
# --------------------------------------------------------------------------
_TWELVE_LINES = "\n".join(f"line {n}" for n in range(1, 13))
def test_slice_whole_document() -> None:
result = slice_lines(_TWELVE_LINES)
assert result == SliceResult(
text=_TWELVE_LINES, first_line=1, last_line=12, total_lines=12, past_end=False
)
def test_slice_offset_only_reads_to_the_end() -> None:
result = slice_lines(_TWELVE_LINES, offset=10)
assert result.text == "line 10\nline 11\nline 12"
assert (result.first_line, result.last_line, result.total_lines) == (10, 12, 12)
assert result.past_end is False
def test_slice_limit_only_starts_at_line_one() -> None:
result = slice_lines(_TWELVE_LINES, limit=2)
assert result.text == "line 1\nline 2"
assert (result.first_line, result.last_line) == (1, 2)
def test_slice_offset_and_limit() -> None:
result = slice_lines(_TWELVE_LINES, offset=12, limit=1)
assert result.text == "line 12"
assert (result.first_line, result.last_line) == (12, 12)
def test_overshooting_limit_clamps_to_the_total() -> None:
result = slice_lines(_TWELVE_LINES, offset=11, limit=100)
assert result.text == "line 11\nline 12"
assert (result.first_line, result.last_line, result.past_end) == (11, 12, False)
def test_offset_past_the_end_refuses_with_the_true_total() -> None:
result = slice_lines(_TWELVE_LINES, offset=13, limit=5)
assert result.past_end is True
assert result.text == ""
assert result.total_lines == 12
def test_empty_document_is_past_end() -> None:
result = slice_lines("")
assert result == SliceResult(
text="", first_line=1, last_line=0, total_lines=0, past_end=True
)
def test_limit_below_one_is_an_empty_past_end_slice() -> None:
"""A contract violation from the caller: refuse loudly-by-shape, never
raise (the refusal text belongs to the caller)."""
result = slice_lines(_TWELVE_LINES, offset=3, limit=0)
assert result.past_end is True
assert result.text == ""
assert result.total_lines == 12
def test_line_numbers_agree_with_splitlines() -> None:
"""The grep → read(offset) coordinate system: the line ``grep`` calls
``N`` is what ``offset=N, limit=1`` returns, byte for byte."""
content = "# header\n\ntext one\n indented\nlast"
lines = content.splitlines()
for number, expected in enumerate(lines, start=1):
result = slice_lines(content, offset=number, limit=1)
assert result.text == expected
assert (result.first_line, result.last_line) == (number, number)
# --------------------------------------------------------------------------
# cap_slice — the character budget at a LINE boundary
# --------------------------------------------------------------------------
def test_cap_under_budget_returns_the_text_untouched() -> None:
text, last, truncated = cap_slice("one\ntwo\nthree", 1, 100)
assert (text, last, truncated) == ("one\ntwo\nthree", 3, False)
def test_cap_under_budget_offsets_from_the_slices_first_line() -> None:
_text, last, truncated = cap_slice("ten\neleven", 10, 100)
assert (last, truncated) == (11, False)
def test_cap_empty_text_reports_the_first_line() -> None:
assert cap_slice("", 7, 100) == ("", 7, False)
def test_cap_counts_a_slice_that_ends_on_a_blank_line() -> None:
"""Phase 182 (task 05) defect pin: a slice whose LAST line is blank
legitimately ends with ``"\\n"`` (that empty line IS part of the text
shown), so the extent counts it. ``splitlines()`` drops the trailing
empty element — which made the caption claim one line fewer than the
block displays AND made the canonical locator disagree with the same
range resolved through any other entry point (the tool branch), so the
same block showed twice."""
text, last, truncated = cap_slice("one\ntwo\n", 4, 100)
assert (text, last, truncated) == ("one\ntwo\n", 6, False)
def test_cap_cuts_on_a_line_boundary_and_appends_the_shared_marker() -> None:
text, last, truncated = cap_slice("one\ntwo\nthree", 1, 9) # line 3 is cut
assert truncated is True
assert text == f"one\ntwo\n{TRUNCATION_MARKER}"
# The marker is not content: the range stops at the last WHOLE line.
assert last == 2
def test_cap_landing_exactly_on_a_line_end_keeps_that_line() -> None:
"""The budget's edge IS the line's terminator — the line fits."""
text, last, truncated = cap_slice("one\ntwo\nthree", 1, 7) # exactly "one\ntwo"
assert truncated is True
assert text == f"one\ntwo\n{TRUNCATION_MARKER}"
assert last == 2
def test_cap_never_claims_a_line_whose_terminator_falls_outside_the_budget() -> None:
text, last, truncated = cap_slice("1\n22\n333\n4444", 1, 7) # "1\n22\n33"
assert truncated is True
assert text == f"1\n22\n{TRUNCATION_MARKER}"
assert last == 2
def test_cap_shorter_than_the_first_line_cuts_it_mid_way() -> None:
"""The one documented sub-line cut — and it still ends with the marker."""
text, last, truncated = cap_slice("abcdefghij\nsecond", 4, 4)
assert truncated is True
assert text == f"abcd\n{TRUNCATION_MARKER}"
assert last == 4
@pytest.mark.parametrize("cap", [0, -1])
def test_cap_zero_or_negative_is_a_loud_mis_wiring(cap: int) -> None:
with pytest.raises(ValueError, match="cap_slice"):
cap_slice("anything", 1, cap)
# --------------------------------------------------------------------------
# document_by_identity — the one bare lookup
# --------------------------------------------------------------------------
def test_identity_lookup_hits() -> None:
doc = _doc()
session = _session([doc])
assert document_by_identity(_real(session), "homelab/a.md") is doc
assert session.lookups == [("homelab", "a.md")]
def test_identity_lookup_misses() -> None:
session = _session([_doc()])
assert document_by_identity(_real(session), "homelab/missing.md") is None
def test_traversal_shaped_identity_is_simply_a_row_that_does_not_exist() -> None:
session = _session([_doc()])
assert document_by_identity(_real(session), "../../etc/passwd") is None
assert session.lookups == [("..", "../etc/passwd")]
def test_first_slash_splits_identity_when_the_path_has_slashes() -> None:
doc = _doc(path="notes/deep/a.md")
session = _session([doc])
assert document_by_identity(_real(session), "homelab/notes/deep/a.md") is doc
assert session.lookups == [("homelab", "notes/deep/a.md")]
def test_identity_without_a_slash_never_touches_the_database() -> None:
"""A bare source name is never a document (the agent's contract)."""
session = _session([_doc()])
assert document_by_identity(_real(session), "homelab") is None
assert session.lookups == []
# --------------------------------------------------------------------------
# block_for — the block a locator names
# --------------------------------------------------------------------------
def test_block_whole_document() -> None:
doc = _doc(content="one\ntwo\nthree")
block = block_for(_real(_session([doc])), "homelab/a.md", cap=1000)
assert block is not None
assert (block.source, block.path, block.title) == ("homelab", "a.md", "The Title")
assert (block.start_line, block.end_line, block.total_lines) == (1, 3, 3)
assert block.text == "one\ntwo\nthree"
assert block.truncated is False
assert block.locator == "homelab/a.md"
def test_block_with_a_named_range_carries_it_in_the_locator() -> None:
doc = _doc(content="\n".join(f"line {n}" for n in range(1, 13)))
block = block_for(
_real(_session([doc])), "homelab/a.md", offset=12, limit=2, cap=1000
)
assert block is not None
assert (block.start_line, block.end_line, block.total_lines) == (12, 12, 12)
assert block.text == "line 12"
assert block.locator == "homelab/a.md#12-12"
def test_block_of_an_image_document_is_refused() -> None:
"""Image display belongs to ``show_image`` (LOCKED A6)."""
doc = _doc(is_image=True)
assert block_for(_real(_session([doc])), "homelab/a.md", cap=1000) is None
def test_block_of_an_unknown_path_is_refused() -> None:
assert block_for(_real(_session([_doc()])), "homelab/nope.md", cap=1000) is None
def test_block_of_a_past_end_range_is_refused() -> None:
doc = _doc(content="one\ntwo")
assert block_for(_real(_session([doc])), "homelab/a.md", offset=9, cap=1000) is None
def test_capped_blocks_range_describes_the_lines_actually_shown() -> None:
doc = _doc(content="1\n22\n333\n4444")
block = block_for(_real(_session([doc])), "homelab/a.md", cap=6) # cuts line 3
assert block is not None
assert block.truncated is True
assert block.text == f"1\n22\n{TRUNCATION_MARKER}"
assert (block.start_line, block.end_line, block.total_lines) == (1, 2, 4)
assert block.locator == "homelab/a.md#1-2"
def test_block_reuses_a_callers_resolved_document() -> None:
"""The ``show_text`` branch already resolved the row (with suggestions);
the block build must not look it up twice."""
db = MagicMock()
block = block_for(cast("Session", db), "homelab/a.md", cap=1000, doc=_doc())
assert block is not None
assert block.locator == "homelab/a.md"
db.scalar.assert_not_called()
# --------------------------------------------------------------------------
# resolve_answer_blocks — the done-time fusion
# --------------------------------------------------------------------------
_TEN = "\n".join(f"line {n}" for n in range(1, 11))
def _answer(*locators: str) -> str:
return "".join(f"```bor {loc}\nmodel text\n```\n" for loc in locators)
def _shown(locator: str, *, first: int = 1, last: int = 3) -> VerbatimBlock:
return VerbatimBlock(
source="homelab",
path="a.md",
title="The Title",
start_line=first,
end_line=last,
total_lines=10,
text="one\ntwo\nthree",
locator=locator,
)
def test_no_fences_and_no_shown_blocks_is_empty() -> None:
assert resolve_answer_blocks(
_real(_session([])), "a plain answer", [], _settings()
) == []
def test_a_fence_resolves_without_any_tool_call() -> None:
doc = _doc(content=_TEN)
blocks = resolve_answer_blocks(
_real(_session([doc])),
_answer("homelab/a.md#4-5"),
[],
_settings(),
)
assert len(blocks) == 1
assert blocks[0].locator == "homelab/a.md#4-5"
assert blocks[0].text == "line 4\nline 5"
assert (blocks[0].start_line, blocks[0].end_line) == (4, 5)
def test_shown_blocks_are_appended_when_the_answer_locates_nothing() -> None:
blocks = resolve_answer_blocks(
_real(_session([])),
"no fence here",
[_shown("homelab/a.md"), _shown("homelab/a.md#4-6", first=4, last=6)],
_settings(),
)
assert [b.locator for b in blocks] == ["homelab/a.md", "homelab/a.md#4-6"]
def test_fence_wins_over_the_same_locator_from_shown() -> None:
"""ONE block per canonical locator, placed at the FENCE (the fence
controls placement)."""
doc = _doc(content=_TEN)
shown = _shown("homelab/a.md#2-3", first=2, last=3)
blocks = resolve_answer_blocks(
_real(_session([doc])),
"intro\n" + _answer("homelab/a.md#2-3") + "outro",
[shown, _shown("homelab/a.md#2-3", first=2, last=3)],
_settings(),
)
assert len(blocks) == 1
assert blocks[0].text == "line 2\nline 3"
def test_capped_fence_dedupes_against_the_same_capped_shown_block() -> None:
"""Phase 182 (task 05) defect pin: a char-capped fence carries a
NARROWER locator than the one its fence wrote (the cut extent) — and
that is exactly the key the ``show_text`` tool's same-range block
carries, so the pair is ONE block, placed at the fence."""
doc = _doc(content=_TEN)
settings = _settings(show_text_max_chars=20)
fenced = resolve_answer_blocks(
_real(_session([doc])), _answer("homelab/a.md"), [], settings
)
assert len(fenced) == 1 and fenced[0].truncated
assert fenced[0].locator == f"homelab/a.md#1-{fenced[0].end_line}"
shown = _shown(fenced[0].locator, last=fenced[0].end_line)
fused = resolve_answer_blocks(
_real(_session([doc])), _answer("homelab/a.md"), [shown], settings
)
assert [b.locator for b in fused] == [fenced[0].locator]
def test_two_fences_keep_answer_order_then_the_unlocated_shown() -> None:
second = _doc(path="b.md", title="B", content="bee\nbee two")
first = _doc(path="a.md", content=_TEN)
shown_a = _shown("homelab/a.md")
shown_c = _shown("homelab/c.md")
blocks = resolve_answer_blocks(
_real(_session([first, second])),
_answer("homelab/b.md", "homelab/a.md#9-10"),
[shown_a, shown_c],
_settings(),
)
# fences first, in answer order — then the shown blocks the answer
# never located, in their original call order (the whole-document
# ``homelab/a.md`` block is a DIFFERENT canonical locator from the
# fenced ``homelab/a.md#9-10``, so it stays unlocated).
assert [b.locator for b in blocks] == [
"homelab/b.md",
"homelab/a.md#9-10",
"homelab/a.md",
"homelab/c.md",
]
def test_block_budget_truncates_the_list() -> None:
doc = _doc(content=_TEN)
blocks = resolve_answer_blocks(
_real(_session([doc])),
_answer("homelab/a.md#1-2", "homelab/a.md#4-5"),
[_shown("homelab/a.md#7-8", first=7, last=8)],
_settings(show_text_max_blocks=1),
)
assert [b.locator for b in blocks] == ["homelab/a.md#1-2"]
@pytest.mark.parametrize(
"overrides",
[
{"answer_text": False},
{"show_text_max_blocks": 0},
{"show_text_max_chars": 0},
{"answer_text": False, "show_text_max_blocks": 0},
{"answer_text": False, "show_text_max_chars": 0},
{"show_text_max_blocks": 0, "show_text_max_chars": 0},
],
)
def test_every_kill_switch_combination_is_inert(overrides: dict[str, Any]) -> None:
doc = _doc(content=_TEN)
assert (
resolve_answer_blocks(
_real(_session([doc])),
_answer("homelab/a.md#4-5"),
[_shown("homelab/a.md#7-8", first=7, last=8)],
_settings(**overrides),
)
== []
)
def test_unresolvable_fence_is_simply_absent() -> None:
blocks = resolve_answer_blocks(
_real(_session([_doc(content=_TEN)])),
_answer("elsewhere/nope.md"),
[],
_settings(),
)
assert blocks == []
def test_image_document_fence_is_absent() -> None:
blocks = resolve_answer_blocks(
_real(_session([_doc(content=_TEN, is_image=True)])),
_answer("homelab/a.md#2-3"),
[],
_settings(),
)
assert blocks == []
def test_a_duplicated_fence_appears_once() -> None:
doc = _doc(content=_TEN)
blocks = resolve_answer_blocks(
_real(_session([doc])),
_answer("homelab/a.md#2-3") + "words\n" + _answer("homelab/a.md 2-3"),
[],
_settings(),
)
assert [b.locator for b in blocks] == ["homelab/a.md#2-3"]
def test_malformed_fence_is_ignored_without_reserving_its_text() -> None:
"""A locator the grammar refuses never reaches the resolver at all."""
blocks = resolve_answer_blocks(
_real(_session([_doc(content=_TEN)])),
_answer("homelab/a.md 40-12"),
[],
_settings(),
)
assert blocks == []
def test_a_loosened_grammar_still_refuses_non_numeric_bounds(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The defensive branch: the range regex above guarantees digit bounds,
but if it is ever loosened, :func:`canonical_locator` still refuses a
non-numeric bound instead of raising out of ``int()``."""
monkeypatch.setattr(
verbatim_module,
"_TRAILING_RANGE_RE",
re.compile(r"[ :#](?P<range>[^ ]*)$"), # the separator, then ANY tail
)
assert canonical_locator("homelab/a.md:x-y") is None
def test_the_character_cap_applies_to_fenced_blocks() -> None:
doc = _doc(content=_TEN)
blocks = resolve_answer_blocks(
_real(_session([doc])),
_answer("homelab/a.md"),
[],
_settings(show_text_max_chars=20),
)
assert len(blocks) == 1
assert blocks[0].truncated is True
assert blocks[0].text.endswith(TRUNCATION_MARKER)
# 20 characters land exactly on the terminator of "line 3", so lines
# 1-3 are whole and the caption stops at 3 (line 4 is cut).
assert blocks[0].text == f"line 1\nline 2\nline 3\n{TRUNCATION_MARKER}"
assert blocks[0].end_line == 3
assert blocks[0].locator == "homelab/a.md#1-3"
def test_locator_language_constant_is_shared() -> None:
assert LOCATOR_LANG == "bor"
def test_module_never_imports_the_agent() -> None:
"""The one-way import rule, read off the AST (the docstring names the
agent on purpose to explain the rule, so a substring check would
prove nothing): ``agent.py`` imports this module's pure half; a
reverse import would cycle."""
tree = ast.parse(inspect.getsource(verbatim_module))
imports: list[str] = []
for node in ast.walk(tree):
if isinstance(node, ast.Import):
imports.extend(alias.name for alias in node.names)
elif isinstance(node, ast.ImportFrom):
imports.extend(f"{node.module or ''}.{alias.name}" for alias in node.names)
offending = [name for name in imports if name.startswith("app.rag.agent")]
assert offending == []
def test_the_fence_scan_stops_once_the_block_budget_is_spent() -> None:
"""A fence-stuffed answer costs at most ``show_text_max_blocks``
successful lookups: once the display budget is in, the scan stops —
the truncation to the same number makes every later block
unreachable, and the resolver runs on the request's event loop."""
session = _session([_doc(content=_TEN), _doc(path="b.md", title="B", content="bee\ntwo")])
blocks = resolve_answer_blocks(
_real(session),
_answer("homelab/a.md", "homelab/b.md", "homelab/a.md#2-3"),
[],
_settings(show_text_max_blocks=2),
)
assert [b.locator for b in blocks] == ["homelab/a.md", "homelab/b.md"]
assert len(session.lookups) == 2