phase: 139_read_range_tool_lines
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user