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:
@@ -2302,3 +2302,117 @@ def test_record_without_segments_round_trips_key_absent(
|
||||
assert r2.status_code == 201, r2.text
|
||||
got2 = admin_client.get(f"/api/chats/{r2.json()['id']}")
|
||||
assert got2.json()["messages"][0]["segments"] == []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 182 (task 05): the answer's VERBATIM display blocks on the BRAIN
|
||||
# record — the persisted twin of the done frame's ``answer_text`` (the
|
||||
# show_text tool's blocks and the ones a ```bor locator fence resolved).
|
||||
# Same phase-113 lesson as ``related`` / ``answer_image``: the key must be
|
||||
# ACCEPTED (``extra="forbid"`` otherwise 422s the done-time auto-save and
|
||||
# the quiet failure leaves the turn unsaved), round-trip byte-for-byte so
|
||||
# the restore path and the anonymous shared page re-render the panels from
|
||||
# the record's own bytes (LOCKED A4 — no new route), and stay ABSENT for
|
||||
# every record without blocks (A9, the phase-50 round-trip contract).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
#: One resolved block — the done frame's ``answer_text`` item shape
|
||||
#: (the SERVER's text + its 1-based inclusive extent + the canonical
|
||||
#: locator the client matches its placeholder against).
|
||||
VERBATIM_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\n[Service]\nExecStart=/usr/bin/gufo",
|
||||
"truncated": False,
|
||||
"locator": "Homelab/quadlet/gufo.container#3-9",
|
||||
}
|
||||
|
||||
|
||||
def test_create_round_trips_the_brain_answer_text_blocks(
|
||||
admin_client: TestClient, db: Session
|
||||
) -> None:
|
||||
"""A brain record carrying ``answer_text`` → 201 (NOT the silent 422)
|
||||
and the blocks survive byte-for-byte in the stored JSONB, the create
|
||||
response and the admin detail — the panels restore from the record."""
|
||||
brain = {**FULL_BRAIN, "answer_text": [VERBATIM_BLOCK]}
|
||||
r = admin_client.post(
|
||||
"/api/chats",
|
||||
json={"messages": [_user(FIRST_QUESTION), brain]},
|
||||
)
|
||||
assert r.status_code == 201, f"expected 201, got {r.status_code}: {r.text}"
|
||||
body = r.json()
|
||||
assert body["messages"][1]["answer_text"] == [VERBATIM_BLOCK]
|
||||
assert "answer_text" not in body["messages"][0]
|
||||
|
||||
raw = db.execute(
|
||||
text("SELECT messages FROM saved_chats WHERE id = :id"),
|
||||
{"id": body["id"]},
|
||||
).scalar_one()
|
||||
assert raw[1]["answer_text"] == [VERBATIM_BLOCK]
|
||||
|
||||
got = admin_client.get(f"/api/chats/{body['id']}")
|
||||
assert got.status_code == 200
|
||||
assert got.json()["messages"][1]["answer_text"] == [VERBATIM_BLOCK]
|
||||
|
||||
|
||||
def test_create_without_answer_text_round_trips_unchanged(
|
||||
admin_client: TestClient,
|
||||
) -> None:
|
||||
"""A record WITHOUT the key (every pre-phase-182 chat, every turn
|
||||
with no blocks) round-trips UNCHANGED — no ``answer_text`` key
|
||||
anywhere, ABSENT rather than ``null``/``[]`` on every surface (A9)."""
|
||||
brain = {**FULL_BRAIN}
|
||||
r = admin_client.post(
|
||||
"/api/chats",
|
||||
json={"messages": [_user(FIRST_QUESTION), brain]},
|
||||
)
|
||||
assert r.status_code == 201, r.text
|
||||
body = r.json()
|
||||
assert body["messages"] == _expect([_user(FIRST_QUESTION), brain])
|
||||
assert "answer_text" not in body["messages"][0]
|
||||
assert "answer_text" not in body["messages"][1]
|
||||
|
||||
got = admin_client.get(f"/api/chats/{body['id']}")
|
||||
assert got.status_code == 200
|
||||
assert "answer_text" not in got.json()["messages"][1]
|
||||
|
||||
|
||||
def test_empty_answer_text_list_is_dropped_not_stored(
|
||||
admin_client: TestClient,
|
||||
) -> None:
|
||||
"""An empty ``answer_text`` (the client saving a turn whose resolver
|
||||
returned nothing) serializes AWAY — never ``null``, never ``[]`` —
|
||||
so the stored record is byte-identical to the no-key shape (A9)."""
|
||||
brain = {**FULL_BRAIN, "answer_text": []}
|
||||
r = admin_client.post(
|
||||
"/api/chats",
|
||||
json={"messages": [_user(FIRST_QUESTION), brain]},
|
||||
)
|
||||
assert r.status_code == 201, r.text
|
||||
assert "answer_text" not in r.json()["messages"][1]
|
||||
|
||||
|
||||
def test_shared_serve_includes_the_brain_answer_text_blocks(
|
||||
admin_client: TestClient,
|
||||
) -> None:
|
||||
"""The anonymous shared page renders the SAME panels from the share's
|
||||
own record (A4 — no new bytes route): the public snapshot's brain
|
||||
record carries the blocks verbatim, the user record does not."""
|
||||
brain = {**FULL_BRAIN, "answer_text": [VERBATIM_BLOCK]}
|
||||
r = admin_client.post(
|
||||
"/api/chats",
|
||||
json={"messages": [_user(FIRST_QUESTION), brain]},
|
||||
)
|
||||
assert r.status_code == 201, r.text
|
||||
share_url = _share(admin_client, r.json()["id"])["share_url"]
|
||||
|
||||
anon = TestClient(fastapi_app) # fresh jar: truly anonymous
|
||||
got = anon.get(f"/api{share_url}")
|
||||
assert got.status_code == 200
|
||||
messages = got.json()["messages"]
|
||||
assert messages[1]["answer_text"] == [VERBATIM_BLOCK]
|
||||
assert "answer_text" not in messages[0]
|
||||
|
||||
Reference in New Issue
Block a user