phase: 139_read_range_tool_lines
Build and Push Containers / build-and-push-app (push) Successful in 2m54s
Build and Push Containers / build-and-push-db (push) Successful in 17s

All verification is complete and green. Phase 139's three tasks (01–03) were already implemented in prior passes; this was the final verification pass, and every gate holds.

## Phase 139 — final verification report

**Verified in working tree (no code changes needed):**
- `readRangeSuffix` single-source-of-truth helper in `app.js` + byte-identical parity copy in `shared.js` (EN-dash ` (lines N–M)`, `(from line N)`, `(first N lines)`, `Number.isInteger(x) && x >= 1` guard mirroring the server's `_whole_line_number`)
- `appendToolLine(wrap, name, argument, offset = null, limit = null)` — `<span class="read-range">` sibling after `<code>`, only when suffix non-empty; `shared.js` `addToolLines` parity
- Live `tool` branch: guarded `ev.offset`/`ev.limit`, additive `toolAcc` records (non-range keeps pre-phase shape), status label carries the suffix (A2); restore path passes stored `t.offset`/`t.limit` through, phase-95 truncation note untouched
- E2E: `SAME_FILE_TRIGGER` mock flow (same file read twice, offsets 10/20 then 40/15) + 3-test Playwright suite

**Test / lint / coverage results:**
- `uv run pytest` (unit + integration): **3283 passed, 9 skipped, 0 failed**
- `uv run pytest --cov=app --cov-report=term-missing`: **TOTAL 99%** (>90% gate)
- `uv run pytest tests/e2e/test_read_range_tool_lines.py -v --no-cov`: **3 passed** in isolation
- `uv run ruff check .`: clean; `uv run pyright`: **0 errors, 0 warnings**

**Completion criteria:**
1. Range read renders `(lines N–M)` live; two same-file reads show DIFFERENT ranges — ✅ (E2E test 1)
2. Plain read byte-identical; phase-87/117 pins + emoji guard pass unedited — ✅ (E2E test 2 + full suite)
3. Suffix survives save → restore on chat page + shared page — ✅ (E2E test 3)
4. Full suite green, coverage >90%, E2E isolated green, ruff + pyright clean — ✅
5. Commit + phase move — left to the harness per executor rules (no `git add`/`commit` run)

**Notable:** the missing pytest summary line on one run was a transient output-buffering artifact, confirmed by re-run; DB was already up and healthy.

**Next pending phase:** `140_pictorial_pdf_pages`
This commit is contained in:
2026-09-30 12:55:23 -04:00
parent 1a657ce4ca
commit 9e7262a5c8
31 changed files with 2197 additions and 25 deletions
+1 -1
View File
@@ -197,7 +197,7 @@ def test_arm_has_exactly_one_live_call_site() -> None:
delta_idx = js.find('ev.type === "delta"')
branch = js[tool_idx:delta_idx]
assert "armToolLineClock(wrap)" in branch, "the live tool branch arms the clock"
append = branch.find("appendToolLine(wrap, name, argument)")
append = branch.find("appendToolLine(wrap, name, argument, frameOffset, frameLimit)")
arm = branch.find("armToolLineClock(wrap)")
assert -1 < append < arm, (
"the arm follows the line's append — the baseline resets per line"
+8 -4
View File
@@ -142,10 +142,12 @@ def test_record_without_the_new_keys_validates_with_none() -> None:
def test_pre_phase_record_round_trips_byte_identical_on_existing_keys() -> None:
"""The phase-50 contract through the new schema: a pre-phase-120
STORED record (every phase-50 key present, explicit nulls where an
optional key does not apply) validates, and re-serializes
byte-identically on EVERY pre-phase key — the only diff is the two
added keys as explicit nulls. Old saved chats and shared links
therefore render/restore exactly as before."""
optional key does not apply — tool entries in the CURRENT full
shape, the phase-95/139 additive fields as nulls for a plain read)
validates, and re-serializes byte-identically on EVERY pre-phase
key — the only diff is the two added keys as explicit nulls. Old
saved chats and shared links therefore render/restore exactly as
before."""
pre_phase: dict = {
"who": "brain",
"text": "Your k3s cluster runs on three nodes — you've got this.",
@@ -161,6 +163,8 @@ def test_pre_phase_record_round_trips_byte_identical_on_existing_keys() -> None:
"truncated": False,
"chars_shown": None,
"chars_total": None,
"offset": None,
"limit": None,
}
],
"stopped": None,
+4 -1
View File
@@ -161,7 +161,10 @@ def test_app_js_labels_resolve_the_name_at_call_time() -> None:
assert "`${brand()} is thinking`" in js
assert "`${brand()} is answering`" in js
assert "`${brand()} is still thinking (${secs}s)`" in js
assert "`${brand()} is reading ${argument}`" in js
assert (
"`${brand()} is reading ${argument}${readRangeSuffix(frameOffset, frameLimit)}`"
in js
), ("phase 139: the read status carries the requested-range suffix (A2)")
assert "`${brand()} is listing documents`" in js
# The frozen module-level literals must be gone (stale-name bug).
assert '"Brain of Reese is thinking"' not in js
+13 -5
View File
@@ -50,17 +50,19 @@ def test_tool_branch_is_a_first_class_turn_branch() -> None:
"the turn handler must branch on tool frames"
)
branch = js[tool_idx:delta_idx]
assert "toolAcc.push({ name, argument })" in branch, (
assert "toolAcc.push(toolRecord)" in branch, (
"every tool frame is recorded for persistence — the record stays"
" {name, argument}-generic, no per-tool shape (phase 68: the"
" search tool rides the same accumulator)"
" search tool rides the same accumulator; phase 139: the range"
" fields join additively — a non-range record keeps the exact"
" pre-phase shape)"
)
assert "clearTurnTimeout()" in branch, "a tool frame proves the stream is alive"
assert 'addMessage("brain", "")' in branch, "first frame creates the brain wrap"
assert "uiState === UI_STATE.thinking" in branch, (
"label updates only while the state is still thinking"
)
assert "appendToolLine(wrap, name, argument)" in branch
assert "appendToolLine(wrap, name, argument, frameOffset, frameLimit)" in branch
# No setUiState in the branch: the state stays "thinking" (never stale).
assert "setUiState" not in branch, (
"the tool branch must keep uiState=thinking (button stays disabled)"
@@ -86,7 +88,10 @@ def test_calling_tool_label_strings() -> None:
branch = js[tool_idx:delta_idx]
assert "sendLabel" not in branch, "phase 48: the button keeps its Stop label"
assert "`${brand()} is listing documents`" in branch
assert "`${brand()} is reading ${argument}`" in branch
assert (
"`${brand()} is reading ${argument}${readRangeSuffix(frameOffset, frameLimit)}`"
in branch
), ("phase 139: the read status carries the requested-range suffix (A2)")
# Phase 70: the read status — new + legacy name, locked
# name+argument gate, first in the ternary.
assert 'name === "read" || name === "read_document") && argument' in branch, (
@@ -222,7 +227,10 @@ def test_tool_lines_re_render_on_restore() -> None:
end = js.find("function restoreConversation")
body = js[fn:end]
assert "Array.isArray(m.tools)" in body
assert "appendToolLine(wrap, t.name, arg)" in body
assert "appendToolLine(wrap, t.name, arg, tOffset, tLimit)" in body, (
"phase 139: the stored range rides the SAME helper — a pre-phase"
" record (no fields) renders byte-identical"
)
thinking_restore = body.find("if (m.thinking)")
tools_restore = body.find("Array.isArray(m.tools)")
assert -1 < thinking_restore < tools_restore, (
+497
View File
@@ -0,0 +1,497 @@
"""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 gains 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."""
body = _extract(_js(), "appendToolLine")
assert (
"function appendToolLine(wrap, name, argument, offset = null, limit = null)"
in _js()
), "the signature carries the additive offset/limit (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"
# Both values pass to the SAME helper (the line + the status label).
assert "appendToolLine(wrap, name, argument, frameOffset, frameLimit)" 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 grep / ls labels are untouched."""
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 stay byte-identical (no suffix).
assert "`${brand()} is searching for ${argument}`" 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(wrap, t.name, arg, tOffset, tLimit)" in body, (
"the stored range rides the SAME helper"
)
# --------------------------------------------------------------------------
# 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 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 w1 = makeWrap();\n"
+ 'appendToolLine(w1, "read", ARG);\n'
+ 'const plain = describe(w1.querySelector(".tool-call"));\n'
+ "const w2 = makeWrap();\n"
+ 'appendToolLine(w2, "read", ARG, 10, 20);\n'
+ 'const range = describe(w2.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 = makeWrap();\n"
+ 'appendToolLine(wa, "read", ARG, 10, 20);\n'
+ 'const appRange = describe(wa.querySelector(".tool-call"));\n'
+ "const wp = makeWrap();\n"
+ 'appendToolLine(wp, "read", ARG);\n'
+ 'const appPlain = describe(wp.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"])
+2 -2
View File
@@ -163,7 +163,7 @@ def test_restore_path_renders_the_stored_marker() -> None:
assert fn != -1
end = js.find("function restoreConversation")
body = js[fn:end]
assert "appendToolLine(wrap, t.name, arg)" in body
assert "appendToolLine(wrap, t.name, arg, tOffset, tLimit)" in body
assert "t.truncated && arg" in body, (
"only an argument-bearing (Reading) record with the flag renders"
" the marker"
@@ -173,7 +173,7 @@ def test_restore_path_renders_the_stored_marker() -> None:
" Number(t.chars_total) || 0)" in body
)
# The marker append sits INSIDE the tools loop, after the line append.
line_idx = body.find("appendToolLine(wrap, t.name, arg)")
line_idx = body.find("appendToolLine(wrap, t.name, arg, tOffset, tLimit)")
note_idx = body.find("appendTruncatedNote(wrap, arg,")
assert -1 < line_idx < note_idx
+69 -3
View File
@@ -45,12 +45,16 @@ def _source_ref() -> dict:
def _tool_call() -> dict:
# Phase 95 (task 02): the truncation record rides the same entry —
# the CURRENT full shape (additive fields, defaults for a plain read).
# Phase 139 (task 02): the read's requested range joins the same
# entry (additive — None for a plain read, the phase-95 pattern).
return {
"name": "read",
"argument": "Homelab/kubernetes.md",
"truncated": False,
"chars_shown": None,
"chars_total": None,
"offset": None,
"limit": None,
}
@@ -401,12 +405,18 @@ def test_realistic_bor_chat_v1_payload_round_trips() -> None:
"tools": [
# Phase 95 (task 02): the current full shape — one
# truncated read (the marker record) + a plain ls.
# Phase 139 (task 02): the truncated read is a RANGE
# read — the requested offset/limit ride the same entry
# so a save → restore re-renders the "(lines 42–71)"
# suffix; the ls carries no range (None, as serialized).
{
"name": "read",
"argument": "Homelab/kubernetes.md",
"truncated": True,
"chars_shown": 128_000,
"chars_total": 204_000,
"offset": 42,
"limit": 30,
},
{
"name": "ls",
@@ -414,6 +424,8 @@ def test_realistic_bor_chat_v1_payload_round_trips() -> None:
"truncated": False,
"chars_shown": None,
"chars_total": None,
"offset": None,
"limit": None,
},
],
"stopped": None,
@@ -484,24 +496,78 @@ def test_realistic_payload_round_trips_through_update_model() -> None:
def test_tool_call_round_trips_with_truncation_fields() -> None:
"""The CURRENT full shape (phase 95 task 02): a truncated read's
record (``truncated: True`` + the two non-negative counts) validates
"""The CURRENT full shape (phase 95 task 02 + phase 139 task 02):
a truncated read's record (``truncated: True`` + the two non-negative
counts + the additive range fields, absent for a plain read) validates
and round-trips ``model_dump()`` unchanged — the save payload carries
it with zero other change (the UI re-renders the marker from it)."""
it with zero other change (the UI re-renders the marker — and, for a
range read, the "(lines N–M)" suffix — from it)."""
raw = {
"name": "read",
"argument": "Homelab/big.md",
"truncated": True,
"chars_shown": 128_000,
"chars_total": 204_000,
"offset": None,
"limit": None,
}
call = ToolCall.model_validate(raw)
assert call.truncated is True
assert call.chars_shown == 128_000
assert call.chars_total == 204_000
assert call.offset is None and call.limit is None
assert call.model_dump() == raw
def test_tool_call_range_fields_round_trip() -> None:
"""Phase 139 (task 02): a RANGE read's record (``offset`` +
``limit``, the phase-131 wire fields) validates and round-trips
``model_dump()`` unchanged — the saved chat's ``tools`` JSONB keeps
the range, so the chat page's restore path and the shared page
re-render the "(lines N–M)" suffix from the stored row. Only ONE of
the two is also a real shape (the model may pass a start without a
count — the UI's "(from line N)" line)."""
both = ToolCall.model_validate(
{"name": "read", "argument": "Homelab/big.md", "offset": 42, "limit": 30}
)
assert (both.offset, both.limit) == (42, 30)
assert both.model_dump() == {
"name": "read",
"argument": "Homelab/big.md",
"truncated": False,
"chars_shown": None,
"chars_total": None,
"offset": 42,
"limit": 30,
}
offset_only = ToolCall.model_validate(
{"name": "read", "argument": "Homelab/big.md", "offset": 7}
)
assert (offset_only.offset, offset_only.limit) == (7, None)
limit_only = ToolCall.model_validate(
{"name": "read", "argument": "Homelab/big.md", "limit": 20}
)
assert (limit_only.offset, limit_only.limit) == (None, 20)
def test_tool_call_range_fields_reject_zero_or_negative() -> None:
"""Phase 139 (task 02) bounds (the phase-83 philosophy, the UI's
guard mirrored): a line number is >= 1 (the server's
``_whole_line_number`` bound — 0 / negative are not real ranges, and
the UI never legitimately sends them; the bool exclusion lives on
the SSE-side guard, where frames are minted)."""
for bad_offset, bad_limit in ((0, None), (None, 0), (-3, None), (None, -1)):
with pytest.raises(ValidationError):
ToolCall.model_validate(
{
"name": "read",
"argument": "x",
"offset": bad_offset,
"limit": bad_limit,
}
)
def test_tool_call_old_shape_validates_with_defaults() -> None:
"""Backward-compat (the phase-50 rule): a saved chat written BEFORE
phase 95 — tool records without the truncation fields — validates
+2 -2
View File
@@ -136,10 +136,10 @@ def test_restore_path_folds_the_disclosure() -> None:
fn = js.find("function renderStoredMessage")
assert fn != -1
body = js[fn : js.find("function restoreConversation", fn)]
assert "appendToolLine(wrap, t.name, arg)" in body
assert "appendToolLine(wrap, t.name, arg, tOffset, tLimit)" in body
assert "closeToolCalls(wrap)" in body
assert (
body.find("appendToolLine(wrap, t.name, arg)")
body.find("appendToolLine(wrap, t.name, arg, tOffset, tLimit)")
< body.find("closeToolCalls(wrap)")
), "the fold sits AFTER the tool-line restore loop"