phase: 192_verbatim_inline_midturn
Build and Push Containers / build-and-push-app (push) Successful in 1m56s
Build and Push Containers / build-and-push-db (push) Successful in 14s

Phase 192 — final verification pass complete.

- **Verified (all 4 tasks already complete):** additive `verbatim_block` SSE frame (`VerbatimBlockPiece` → `ChatVerbatimBlockEvent` → chat dispatch, after the round's `tool`/`tool_result`, before the next round); `hydrateVerbatimNow` matched-only mid-turn pass + app.js branch; `dropPlacedTwin` per-locator done/restore dedupe; the dedicated E2E suite.
- **Root cause of the rejected attempt:** my own concurrent background `pytest` run deadlocked the harness validation run on the shared Postgres (`psycopg.errors.DeadlockDetected`). No code defect — re-ran every gate **exclusively**.
- `uv run pytest -q --cov=app --cov-report=term-missing` → **exit 0**, `TOTAL 7181 16 99%` (>90%)
- `uv run pytest tests/e2e/test_verbatim_inline_midturn.py --no-cov` → **6 passed** (isolated)
- `uv run pytest tests/e2e/test_verbatim_show_text.py tests/e2e/test_chat_history.py --no-cov` → **18 passed** (`test_chat_history.py` byte-unedited; the phase-182 suite's "no eighth type" pin was tightened, not loosened, to seven + `verbatim_block` with frame-order asserts)
- `uv run ruff check .` → All checks passed (exit 0) · `uv run pyright` → **0 errors**
- **Completion criteria:** single frame per success + zero per refusal ✅ · `BOR_ANSWER_TEXT=0` wire byte-identical (integration spelled baseline + E2E App B) ✅ · fence hydrates in place / unfenced lands at the call moment, before `done` ✅ · append+fence → 1 panel, two ranges → 2 panels ✅ · restore 1 panel/locator, `done.answer_text` + record key + `verbatim_blocks=N` unchanged ✅ · suite green, coverage 99%, lint/types clean ✅ · no-show_text turn byte-identical ✅ · commit → left to the harness (no git index/history commands run).
- **Docs:** README reviewed line-by-line (2399 lines) + AGENTS.md in full; mechanically verified all 86 `BOR_*` settings, every `.env.example` var, table defaults, internal anchors (0 broken), ports 8123/8901, tree depth 1–8, pagination 25/1–100, 17 palette colors, `0029`/`0030` migrations, and every cited test file. Fixed 1 inconsistency: the verbatim "Parity" bullet claimed the live turn/reload/shared page share "the same three globals" — now names the settle-path trio and states that `hydrateVerbatimNow` is the ONE chat-only addition (a shared page has no stream).
- **Notable:** `run_agent`'s four repeated piece unions were collapsed into the ONE exported `AgentPiece` alias, so a future piece type is spelled in one place; `test_frontend_edit_message.py`'s `runTurn` byte-pin was regenerated, not weakened.
- **Next pending phase:** `193_sync_remaining_count`.
This commit is contained in:
2026-10-19 12:19:51 -04:00
parent 4f20c256d7
commit fb17d15fbe
49 changed files with 17313 additions and 102 deletions
+114
View File
@@ -1401,3 +1401,117 @@ def test_saved_chat_total_budget_counts_verbatim_block_text() -> None:
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
# ---------------------------------------------------------------------------
# Phase 192 (task 01, A1): ChatVerbatimBlockEvent — the mid-turn twin of the
# done frame's ``answer_text`` item.
# ---------------------------------------------------------------------------
def test_chat_verbatim_block_event_shape() -> None:
"""The mid-turn frame's wire shape: ``type`` FIRST (the sibling events'
convention) then the exact :class:`VerbatimBlockRef` field set, the
``verbatim_block`` default, ``truncated`` defaulting False (the block was
not capped), and no ``model_config`` — the phase-95
:class:`ChatToolResultEvent` convention mirrored: an SSE frame is
server-built, so unknown keys are ignored rather than forbidden."""
from app.schemas import ChatVerbatimBlockEvent
ev = ChatVerbatimBlockEvent(**_verbatim_block())
dumped = ev.model_dump()
assert list(dumped) == ["type", *VerbatimBlockRef.model_fields]
assert dumped == {"type": "verbatim_block", **_verbatim_block()}
minimal = ChatVerbatimBlockEvent(
source="Homelab",
path="a.md",
title="a.md",
start_line=1,
end_line=1,
total_lines=1,
text="one line",
locator="Homelab/a.md",
).model_dump()
assert minimal["truncated"] is False
assert minimal["type"] == "verbatim_block"
# The sibling convention mirrored EXACTLY: an SSE event model declares no
# ``extra="forbid"`` (the frame is server-built, so an unknown key is
# ignored rather than rejected — like ``ChatToolResultEvent``).
from app.schemas import ChatToolResultEvent
assert ChatVerbatimBlockEvent.model_config.get("extra") is None
assert ChatToolResultEvent.model_config.get("extra") is None
def test_chat_verbatim_block_event_field_set_matches_the_done_item() -> None:
"""The drift pin: the mid-turn frame carries EXACTLY the done frame's
item fields (plus ``type``) with the SAME caps — the one client
(``isVerbatimBlock`` + ``buildVerbatimPanel``) hydrates both from one
shape, so a field added to :class:`VerbatimBlockRef` without its twin
here fails this test."""
from app.schemas import ChatVerbatimBlockEvent
assert set(ChatVerbatimBlockEvent.model_fields) == set(
VerbatimBlockRef.model_fields
) | {"type"}
for name, ref_field in VerbatimBlockRef.model_fields.items():
mine = ChatVerbatimBlockEvent.model_fields[name]
assert mine.annotation == ref_field.annotation, name
assert mine.is_required() == ref_field.is_required(), name
assert mine.metadata == ref_field.metadata, name # the caps, verbatim
# The caps fire (the same limits, enforced on this model too).
with pytest.raises(ValidationError):
ChatVerbatimBlockEvent(**_verbatim_block(text="x" * 600_001))
with pytest.raises(ValidationError):
ChatVerbatimBlockEvent(**_verbatim_block(source="s" * 121))
with pytest.raises(ValidationError):
ChatVerbatimBlockEvent(**_verbatim_block(path="p" * 1001))
with pytest.raises(ValidationError):
ChatVerbatimBlockEvent(**_verbatim_block(title="t" * 501))
with pytest.raises(ValidationError):
ChatVerbatimBlockEvent(**_verbatim_block(locator="l" * 2101))
for field in ("start_line", "end_line", "total_lines"):
with pytest.raises(ValidationError):
ChatVerbatimBlockEvent(**_verbatim_block(**{field: -1}))
# ``isVerbatimBlock`` (frontend/assets/verbatim.js) needs exactly these
# three: a non-empty ``source`` string, a non-empty ``path`` string and a
# ``text`` string — so the mid-turn event is accepted as-is.
dumped = ChatVerbatimBlockEvent(**_verbatim_block()).model_dump()
assert isinstance(dumped["source"], str) and dumped["source"]
assert isinstance(dumped["path"], str) and dumped["path"]
assert isinstance(dumped["text"], str)
def test_chat_verbatim_block_event_dump_keys_are_the_client_contract() -> None:
"""Phase 192 (task 03, A4): the key-set pin the ONE panel client leans on.
``frontend/assets/verbatim.js`` has no schema of its own: the
``isVerbatimBlock`` guard is a shape check and ``buildVerbatimPanel``
reads the fields by name — so the mid-turn frame is only renderable while its
``model_dump()`` key set is EXACTLY :class:`VerbatimBlockRef`'s key set
plus the ``type`` discriminator. A field added to one model and not the
other, a rename, or an ``exclude=`` on the dump all fail here (the client
would silently lose the caption, the extent, or the whole block).
"""
from app.schemas import ChatVerbatimBlockEvent
dumped = ChatVerbatimBlockEvent(**_verbatim_block()).model_dump()
assert set(dumped) == set(VerbatimBlockRef.model_fields) | {"type"}
# ``type`` FIRST and the ref's fields in declaration order — the sibling
# SSE events' convention, unchanged by the dump.
assert list(dumped) == ["type", *VerbatimBlockRef.model_fields]
# The three keys ``isVerbatimBlock`` cannot live without, plus the keys
# ``buildVerbatimPanel`` prints (the caption's extent and its open link).
for key in ("source", "path", "text", "title", "start_line", "end_line",
"total_lines", "truncated", "locator"):
assert key in dumped, f"the client reads {key} off both frames"
# The done item round-trips the SAME keys: the mid-turn frame is a copy of
# a done item, so the restore/shared hydration needs no second reader.
assert (
VerbatimBlockRef.model_validate(
{k: v for k, v in dumped.items() if k != "type"}
).model_dump()
== {k: v for k, v in dumped.items() if k != "type"}
)