phase: 182_verbatim_show_text
**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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user