Files
brain-of-reese/tests/unit/test_read_range_tool_lines.py
T
ducoterra 31bd903a93
Build and Push Containers / build-and-push-app (push) Successful in 8m38s
Build and Push Containers / build-and-push-db (push) Successful in 32s
phase: 158_thinking_timeline
All verification complete. Every gate is green and the docs check out. Final report:

## Phase 158 final verification pass — COMPLETE

**Verified (all tasks pre-completed; this pass re-ran the full gate):**
- Unit + integration: `uv run pytest --cov=app --cov-report=term-missing` → **3618 passed, 99% coverage on `app/`** (>90% gate)
- Phase E2E in isolation: `uv run pytest tests/e2e/test_thinking_timeline.py -v --no-cov` → **6/6 passed** (order pin, fold-at-rest, live-open, restore+`segments` array, shared page, no-thinking single disclosure)
- Regression E2E, each in isolation: `test_thinking_display` 5✓, `test_tool_call_compact` 4✓, `test_turn_progress_loader` 3✓ (per-run pin updated), `test_big_read_progress` 4✓, `test_agent_document_tools` 4✓, `test_share_chat` 4✓, `test_chat_persistence` 4✓, `test_smoke` 3✓
- Lint/types: `uv run ruff check . && uv run pyright` → **clean (0 errors)**

**Completion criteria:** all 9 met — timeline E2E pins ✓, restore/shared `segments` ✓, no-thinking single-disclosure + the three UNCHANGED regression suites green ✓, pre-phase byte-identical round-trip (integration pin: key absent, never null; >50 / >32 000 / empty text / unknown kind all 422) ✓, full suite + coverage + E2E + lint ✓, `git diff --stat` footprint limited to `app/schemas.py`, `frontend/assets/{app,shared}.js`, `tests/`, README, phase files (a pre-existing owner `TODO.md` edit is out of scope, not staged by the phase commit) ✓, D15→per-run supersession recorded in the updated pin ✓. (Commit + phase move are the harness's job; task dirs already in `complete/`.)

**Documentation:** README.md — Thinking section correctly describes the phase-158 timeline; config reference matches `.env.example` exactly; page table matches registered routes; no stale single-block wording. AGENTS.md — rules and quick-reference commands all accurate. **Verified consistent — no changes needed.**

**Decisions/deviations:** none. `test_tool_call_compact.py` carries a one-line docstring-only update (referenced the removed `closeToolCalls`); its test logic is byte-identical to the phase-117 contract and green.

**Next pending phase:** none — `todo/` contains only 158.
2026-10-05 02:52:55 -04:00

529 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Unit: the phase-139 (task 03) "the Reading line shows which lines"
frontend contract.
No new Python app logic exists for this task — the behavior lives in
``frontend/assets/app.js`` (the ``readRangeSuffix`` helper + the
``appendToolLine`` suffix span + the live ``tool`` branch's
``ev.offset``/``ev.limit`` read + the ``toolAcc`` persistence stamp +
the restore pass-through) and ``frontend/assets/shared.js`` (the shared
page's parity copy of the suffix + ``addToolLines``). Like the other
frontend-adjacent unit files (``test_frontend_tool_states.py`` is the
phase-37 precedent, ``test_read_truncation_frontend.py`` the phase-95
one), this module pins the JS markers the story depends on so a silent
regression in the suffix wording, the well-formedness guard, the
persistence shape, or the shared-page parity is caught without a
browser. A small node-driven behavioral check (skipped when node is
unavailable, like the phase-10 renderer check in
``test_document_viewer.py``) proves the three suffix shapes, the
plain-read line's byte-identity to pre-phase, and the shared-page
parity. The E2E gate is the phase's story suite
(``tests/e2e/test_read_range_tool_lines.py``).
"""
from __future__ import annotations
import json
import shutil
import subprocess
from pathlib import Path
import pytest
FRONTEND = Path(__file__).resolve().parents[2] / "frontend"
APP_JS = FRONTEND / "assets" / "app.js"
SHARED_JS = FRONTEND / "assets" / "shared.js"
HAVE_NODE = shutil.which("node") is not None
def _js() -> str:
return APP_JS.read_text(encoding="utf-8")
def _shared_js() -> str:
return SHARED_JS.read_text(encoding="utf-8")
def _extract(js: str, name: str) -> str:
"""One top-level function by name — from its ``function NAME`` to
its column-0 closing ``}`` (inner braces are indented, so the first
``\\n}\\n`` is the function's own end — the
``test_frontend_tool_states`` extraction convention)."""
fn = js.find(f"function {name}")
assert fn != -1, f"{name} not found"
end = js.find("\n}\n", fn)
assert end != -1, f"could not find the end of {name}"
return js[fn : end + 2]
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
# --------------------------------------------------------------------------
# app.js — readRangeSuffix: the single source of truth for the wording
# --------------------------------------------------------------------------
def test_read_range_suffix_exists_with_the_pinned_literals() -> None:
"""readRangeSuffix (app.js): the well-formedness guard mirrors the
server's ``_whole_line_number`` (``Number.isInteger(x) && x >= 1``
— bools are not integers in JS), and the three suffix shapes carry
the EXACT pinned copy — both present → `` (lines {offset}–{offset +
limit - 1})`` (EN-DASH, ASSUMPTION A1: the REQUESTED extent),
offset only → `` (from line {offset})``, limit only →
`` (first {limit} lines)``, otherwise `` ""``."""
body = _extract(_js(), "readRangeSuffix")
assert "Number.isInteger(offset) && offset >= 1" in body, (
"the offset guard mirrors the server's _whole_line_number"
)
assert "Number.isInteger(limit) && limit >= 1" in body, (
"the limit guard mirrors the server's _whole_line_number"
)
assert "` (lines ${offset}–${offset + limit - 1})`" in body, (
"the both-present shape — the REQUESTED extent, en-dash per the"
" pinned copy (ASSUMPTION A1)"
)
assert "` (from line ${offset})`" in body, "the offset-only shape"
assert "` (first ${limit} lines)`" in body, "the limit-only shape"
assert 'return ""' in body, "neither field well-formed → no suffix"
def test_read_range_suffix_shape_order() -> None:
"""The both-present shape wins (it is checked first), then
offset-only, then limit-only, then the empty fallback — so a
well-formed pair always renders the range, never a one-sided
shape."""
body = _extract(_js(), "readRangeSuffix")
both = body.find("if (offOk && limOk)")
off_only = body.find("if (offOk)")
lim_only = body.find("if (limOk)")
empty = body.find('return ""')
assert -1 < both < off_only < lim_only < empty, (
"both → offset-only → limit-only → empty, in order"
)
# --------------------------------------------------------------------------
# app.js — appendToolLine: the suffix span on the Reading line
# --------------------------------------------------------------------------
def test_append_tool_line_signature_takes_optional_range() -> None:
"""appendToolLine takes the CONTAINER as its first param (phase
158, D1 — the caller passes the run's disclosure; the
single-container derivation is gone) + the additive
``offset``/``limit`` params (default ``null`` — a pre-phase
3-arg call renders byte-identical), and the read arm renders the
suffix through the SAME helper. Phase 146 (task 02) extends the
signature one more additive param (``scope = null`` — the scoped
grep's document) — the 5-arg pre-phase-146 call shape renders
byte-identically."""
body = _extract(_js(), "appendToolLine")
assert (
"function appendToolLine(container, name, argument, offset = null, "
"limit = null, scope = null)" in _js()
), ("the signature carries the container + the additive "
"offset/limit/scope (default null)")
assert "readRangeSuffix(offset, limit)" in body, (
"the read arm renders the suffix through the SAME helper"
)
assert 'range.className = "read-range"' in body, (
"the suffix rides a .read-range span"
)
assert "range.textContent = rangeSuffix" in body, (
"the range is data — textContent, never innerHTML"
)
assert "if (rangeSuffix)" in body, (
"the span is appended ONLY when the suffix is non-empty — a"
" plain read's line stays byte-identical"
)
def test_append_tool_line_suffix_is_a_sibling_after_the_code() -> None:
"""The .read-range span is a SIBLING appended AFTER the ``<code>``
child (the phase-95 truncation-note precedent — append a span,
never rewrite the pinned template text), so the line's pinned
``📄 Reading `` + ``<code>`` bytes are untouched."""
body = _extract(_js(), "appendToolLine")
code_append = body.find("line.appendChild(code)")
range_append = body.find("line.appendChild(range)")
assert -1 < code_append < range_append, (
"the range span lands AFTER the <code> child"
)
# The read arm's pinned template text is untouched (phase-87/117).
assert 'line.textContent = "📄 Reading "' in body
assert "innerHTML" not in body, (
"no HTML injection surface on tool lines — textContent only"
)
# --------------------------------------------------------------------------
# app.js — the live `tool` branch: ev.offset/ev.limit + the toolAcc stamp
# --------------------------------------------------------------------------
def test_live_tool_branch_reads_the_range_with_the_guard() -> None:
"""The live ``tool`` branch reads ``ev.offset`` / ``ev.limit`` with
the SAME well-formedness guard (``Number.isInteger(x) && x >= 1``
→ ``null`` when absent / malformed), so a non-range frame keeps the
exact pre-phase ``{name, argument}`` record shape (the additive
``bor.chat.v1`` JSONB — old records restore byte-identically, no
migration)."""
js = _js()
tool_idx = js.find('ev.type === "tool"')
delta_idx = js.find('ev.type === "delta"')
branch = js[tool_idx:delta_idx]
assert "Number.isInteger(ev.offset) && ev.offset >= 1 ? ev.offset : null" in branch, (
"ev.offset reads through the house guard → null when absent"
)
assert "Number.isInteger(ev.limit) && ev.limit >= 1 ? ev.limit : null" in branch, (
"ev.limit reads through the house guard → null when absent"
)
# The record carries the fields ONLY when non-null.
assert "const toolRecord = { name, argument }" in branch, (
"the base record stays {name, argument}-generic"
)
assert "if (frameOffset !== null) toolRecord.offset = frameOffset" in branch, (
"offset joins the record ONLY when non-null"
)
assert "if (frameLimit !== null) toolRecord.limit = frameLimit" in branch, (
"limit joins the record ONLY when non-null"
)
assert "toolAcc.push(toolRecord)" in branch, "every tool frame is recorded"
# Phase 146 (task 02): the scope joins the SAME two pass-throughs.
assert (
"const line = appendToolLine("
in branch and "armToolLineClock(wrap, line)" in branch
)
def test_live_read_status_carries_the_range_suffix() -> None:
"""ASSUMPTION A2: the read status label carries the SAME
requested-range suffix as the line (``readRangeSuffix`` — the single
source of truth); a non-range read renders byte-identical (the
helper returns ``""``). The ls labels are untouched; the grep label
gains the phase-146 additive scope (`` in <path>`` ONLY when the
frame carried one — a bare grep renders the exact pre-phase label).
"""
js = _js()
tool_idx = js.find('ev.type === "tool"')
delta_idx = js.find('ev.type === "delta"')
branch = js[tool_idx:delta_idx]
assert (
"`${brand()} is reading ${argument}${readRangeSuffix(frameOffset, frameLimit)}`"
in branch
), "the read status carries the suffix (A2)"
# The other labels: the grep scope extension (phase 146) + the
# ls labels untouched.
assert (
"`${brand()} is searching for ${argument}"
"${framePath ? ` in ${framePath}` : \"\"}`" in branch
)
assert "`${brand()} is listing documents in ${argument}`" in branch
assert "`${brand()} is listing documents`" in branch
# --------------------------------------------------------------------------
# app.js — the restore path: the stored range re-renders the suffix
# --------------------------------------------------------------------------
def test_restore_path_passes_the_stored_range_through() -> None:
"""The phase-14 restore path (renderStoredMessage): a stored tool
record with ``offset``/``limit`` (the live ``toolAcc`` stamp —
additive on ``bor.chat.v1``) re-renders the SAME suffix through the
SAME helper; a record saved before the phase (no fields) renders
byte-identically (the guard → null → no span)."""
js = _js()
fn = js.find("function renderStoredMessage")
assert fn != -1
end = js.find("function restoreConversation")
body = js[fn:end]
assert "Number.isInteger(t.offset) && t.offset >= 1 ? t.offset : null" in body, (
"the stored offset reads through the house guard"
)
assert "Number.isInteger(t.limit) && t.limit >= 1 ? t.limit : null" in body, (
"the stored limit reads through the house guard"
)
assert "appendToolLine(restoreDisc, t.name, arg, tOffset, tLimit, tPath)" in body, (
"the stored range + the phase-146 stored scope ride the SAME helper "
"(the caller passes the run's disclosure — phase 158)"
)
# --------------------------------------------------------------------------
# shared.js — the parity copy: identical literals + the addToolLines span
# --------------------------------------------------------------------------
def test_shared_page_suffix_is_byte_identical_to_app_js() -> None:
"""shared.js's ``readRangeSuffix`` is the parity copy — IDENTICAL
literals and IDENTICAL guard (the phase-50 restore contract: the
shared page re-renders saved records pixel-identically to the chat
page). The three suffix shapes + the guard must match app.js
EXACTLY."""
shared = _extract(_shared_js(), "readRangeSuffix")
app = _extract(_js(), "readRangeSuffix")
for literal in (
"Number.isInteger(offset) && offset >= 1",
"Number.isInteger(limit) && limit >= 1",
"` (lines ${offset}–${offset + limit - 1})`",
"` (from line ${offset})`",
"` (first ${limit} lines)`",
'return ""',
):
assert literal in shared, f"shared.js lost the parity literal {literal!r}"
assert literal in app, f"app.js lost the parity literal {literal!r}"
def test_shared_page_add_tool_lines_renders_the_range_span() -> None:
"""shared.js's ``addToolLines``: the SAME .read-range span as the
chat page's ``appendToolLine`` — a sibling after the ``<code>``
child, textContent only, appended ONLY when the suffix is non-empty
(a record without the fields renders exactly as today)."""
body = _extract(_shared_js(), "addToolLines")
assert "readRangeSuffix(t.offset, t.limit)" in body, (
"the shared page renders the suffix from the stored record"
)
assert 'range.className = "read-range"' in body
assert "range.textContent = rangeSuffix" in body
assert "if (rangeSuffix)" in body
code_append = body.find("line.appendChild(code)")
range_append = body.find("line.appendChild(range)")
assert -1 < code_append < range_append, (
"the shared range span lands AFTER the <code> child (parity)"
)
assert "innerHTML" not in body, (
"no HTML injection surface on shared tool lines — textContent only"
)
def test_no_cdn_added() -> None:
"""AGENTS.md rule 6: the range suffix adds no external
script/link."""
index = (FRONTEND / "index.html").read_text(encoding="utf-8")
assert 'src="http' not in index and 'href="http' not in index
# --------------------------------------------------------------------------
# behavior (node): the three suffix shapes, the plain line's
# byte-identity, and the shared-page parity
# --------------------------------------------------------------------------
#: A minimal DOM stub — just enough surface for appendToolLine /
#: addToolLines (createElement / querySelector / appendChild /
#: insertBefore / textContent / className / setAttribute). Plain strings
#: only; every node is snapshotted as a JSON row (the
#: test-document-viewer / test-chat-image-questions pattern).
_DOM_STUB = """
class El {
constructor(tag) {
this.tag = tag;
this.children = [];
this.attrs = {};
this.className = "";
this._text = "";
this.open = false;
}
setAttribute(k, v) { this.attrs[k] = String(v); }
getAttribute(k) { return k in this.attrs ? this.attrs[k] : null; }
appendChild(c) { this.children.push(c); return c; }
insertBefore(c, ref) {
if (!ref) { this.children.push(c); return c; }
const i = this.children.indexOf(ref);
if (i === -1) this.children.push(c); else this.children.splice(i, 0, c);
return c;
}
_match(el, sel) {
if (sel.charAt(0) === ".")
return (el.className || "").split(/\\s+/).indexOf(sel.slice(1)) !== -1;
if (sel.charAt(0) === "#") return el.id === sel.slice(1);
return el.tag === sel;
}
querySelector(sel) {
for (const c of this.children) {
if (this._match(c, sel)) return c;
const r = c.querySelector(sel);
if (r) return r;
}
return null;
}
querySelectorAll(sel) {
const out = [];
for (const c of this.children) {
if (this._match(c, sel)) out.push(c);
out.push(...c.querySelectorAll(sel));
}
return out;
}
get textContent() {
let t = this._text;
for (const c of this.children) t += c.textContent;
return t;
}
set textContent(v) { this._text = String(v); this.children = []; }
}
const document = { createElement: (t) => new El(t) };
function makeWrap() {
const wrap = document.createElement("div");
const body = document.createElement("div");
body.className = "msg-body";
const bubble = document.createElement("div");
bubble.className = "bubble";
body.appendChild(bubble);
wrap.appendChild(body);
return wrap;
}
function makeDisclosure() {
const container = document.createElement("details");
container.className = "tool-calls-disclosure";
const summary = document.createElement("summary");
summary.className = "tool-calls-summary";
container.appendChild(summary);
const listEl = document.createElement("div");
listEl.className = "tool-calls";
container.appendChild(listEl);
const wrap = makeWrap();
wrap.querySelector(".msg-body").appendChild(container);
return { wrap, container };
}
function describe(el) {
return {
text: el.textContent,
children: el.children.map(
(c) => ({ tag: c.tag, cls: c.className, text: c.textContent })
),
};
}
"""
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_suffix_shapes_under_node() -> None:
"""The REAL app.js ``readRangeSuffix`` under node — the three suffix
shapes + the malformed fallbacks (the guard: bools / strings / 0 are
not whole line numbers ≥ 1; a one-sided well-formed pair renders its
one-sided shape; a well-formed pair renders the range)."""
fn = _extract(_js(), "readRangeSuffix")
cases = [
[42, 30, " (lines 42–71)"],
[42, None, " (from line 42)"],
[None, 5, " (first 5 lines)"],
[None, None, ""],
["x", 5, " (first 5 lines)"], # malformed offset, valid limit
[42, "y", " (from line 42)"], # valid offset, malformed limit
[0, 5, " (first 5 lines)"], # offset 0 is not ≥ 1
[42, 0, " (from line 42)"], # limit 0 is not ≥ 1
[True, 5, " (first 5 lines)"], # a bool is not an integer
[1, 1, " (lines 1–1)"], # the smallest well-formed range
]
driver = (
fn
+ "\nconst cases = "
+ json.dumps([[c[0], c[1]] for c in cases])
+ ";\nconsole.log(JSON.stringify(cases.map((c) => readRangeSuffix(c[0], c[1]))));"
)
out = json.loads(_run_node(driver))
assert out == [c[2] for c in cases], out
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_plain_line_byte_identical_and_range_line_carries_the_suffix() -> None:
"""The REAL app.js ``appendToolLine`` under node (against the DOM
stub): a PLAIN read (no offset/limit) renders the pre-phase line
byte-identical — ``📄 Reading <code>path</code>``, a single ``code``
child, NO ``.read-range`` span — while a RANGE read appends the
``.read-range`` sibling after the ``code`` with the exact suffix."""
suffix = _extract(_js(), "readRangeSuffix")
append = _extract(_js(), "appendToolLine")
driver = (
_DOM_STUB
+ "\n"
+ suffix
+ "\n"
+ append
+ '\nconst ARG = "samedoc/twice.md";\n'
+ "const a1 = makeDisclosure();\n"
+ 'appendToolLine(a1.container, "read", ARG);\n'
+ 'const plain = describe(a1.wrap.querySelector(".tool-call"));\n'
+ "const a2 = makeDisclosure();\n"
+ 'appendToolLine(a2.container, "read", ARG, 10, 20);\n'
+ 'const range = describe(a2.wrap.querySelector(".tool-call"));\n'
+ "console.log(JSON.stringify({ plain, range }));"
)
out = json.loads(_run_node(driver))
plain, range = out["plain"], out["range"]
# The plain line: byte-identical to pre-phase — ONE <code> child, no
# .read-range span, the exact pinned text.
assert plain["text"] == "📄 Reading samedoc/twice.md", plain
assert len(plain["children"]) == 1, plain
assert plain["children"][0] == {"tag": "code", "cls": "", "text": "samedoc/twice.md"}, plain
assert all(c["cls"] != "read-range" for c in plain["children"]), (
"a plain read must carry NO .read-range span"
)
# The range line: the <code> child PLUS the .read-range sibling with
# the exact suffix (10 + 20 - 1 = 29, en-dash).
assert range["text"] == "📄 Reading samedoc/twice.md (lines 10–29)", range
assert range["children"][0] == {"tag": "code", "cls": "", "text": "samedoc/twice.md"}, range
assert range["children"][1] == {
"tag": "span",
"cls": "read-range",
"text": " (lines 10–29)",
}, range
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_shared_page_line_parity_under_node() -> None:
"""The REAL shared.js ``addToolLines`` under node renders the SAME
line as app.js's ``appendToolLine`` for the same record (the
parity contract — the shared page re-renders saved records
pixel-identically to the chat page): identical text and identical
children (the ``code`` + the ``.read-range`` span with the same
suffix). A record WITHOUT the fields renders the plain line on both
pages."""
shared_suffix = _extract(_shared_js(), "readRangeSuffix")
add_lines = _extract(_shared_js(), "addToolLines")
app_suffix = _extract(_js(), "readRangeSuffix")
append = _extract(_js(), "appendToolLine")
driver = (
_DOM_STUB
+ "\n"
+ app_suffix
+ "\n"
+ append
+ '\nconst ARG = "samedoc/twice.md";\n'
# app.js: a range read + a plain read
+ "const wa = makeDisclosure();\n"
+ 'appendToolLine(wa.container, "read", ARG, 10, 20);\n'
+ 'const appRange = describe(wa.wrap.querySelector(".tool-call"));\n'
+ "const wp = makeDisclosure();\n"
+ 'appendToolLine(wp.container, "read", ARG);\n'
+ 'const appPlain = describe(wp.wrap.querySelector(".tool-call"));\n'
# shared.js: the SAME record (range + no-fields) through addToolLines
+ shared_suffix.replace("function readRangeSuffix", "function sharedSuffix")
+ "\n"
+ add_lines.replace(
"readRangeSuffix(t.offset, t.limit)", "sharedSuffix(t.offset, t.limit)"
)
+ "const ws = makeWrap();\n"
+ 'addToolLines(ws, [{ name: "read", argument: ARG, offset: 10, limit: 20 }]);\n'
+ 'const sharedRange = describe(ws.querySelector(".tool-call"));\n'
+ "const wn = makeWrap();\n"
+ 'addToolLines(wn, [{ name: "read", argument: ARG }]);\n'
+ 'const sharedPlain = describe(wn.querySelector(".tool-call"));\n'
+ "console.log(JSON.stringify({ appRange, appPlain, sharedRange, sharedPlain }));"
)
out = json.loads(_run_node(driver))
# The range line is pixel-identical across the two pages.
assert out["sharedRange"] == out["appRange"], (
f"parity broken (range): {out['sharedRange']} vs {out['appRange']}"
)
# A record without the fields renders the plain line on both pages.
assert out["sharedPlain"] == out["appPlain"], (
f"parity broken (plain): {out['sharedPlain']} vs {out['appPlain']}"
)
# And the plain line carries no .read-range span (byte-identical).
assert all(c["cls"] != "read-range" for c in out["sharedPlain"]["children"])