phase: 192_verbatim_inline_midturn
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:
@@ -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"}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user