phase: 158_thinking_timeline
Build and Push Containers / build-and-push-app (push) Successful in 8m38s
Build and Push Containers / build-and-push-db (push) Successful in 32s

All verification complete. Every gate is green and the docs check out. Final report:

## Phase 158 final verification pass — COMPLETE

**Verified (all tasks pre-completed; this pass re-ran the full gate):**
- Unit + integration: `uv run pytest --cov=app --cov-report=term-missing` → **3618 passed, 99% coverage on `app/`** (>90% gate)
- Phase E2E in isolation: `uv run pytest tests/e2e/test_thinking_timeline.py -v --no-cov` → **6/6 passed** (order pin, fold-at-rest, live-open, restore+`segments` array, shared page, no-thinking single disclosure)
- Regression E2E, each in isolation: `test_thinking_display` 5✓, `test_tool_call_compact` 4✓, `test_turn_progress_loader` 3✓ (per-run pin updated), `test_big_read_progress` 4✓, `test_agent_document_tools` 4✓, `test_share_chat` 4✓, `test_chat_persistence` 4✓, `test_smoke` 3✓
- Lint/types: `uv run ruff check . && uv run pyright` → **clean (0 errors)**

**Completion criteria:** all 9 met — timeline E2E pins ✓, restore/shared `segments` ✓, no-thinking single-disclosure + the three UNCHANGED regression suites green ✓, pre-phase byte-identical round-trip (integration pin: key absent, never null; >50 / >32 000 / empty text / unknown kind all 422) ✓, full suite + coverage + E2E + lint ✓, `git diff --stat` footprint limited to `app/schemas.py`, `frontend/assets/{app,shared}.js`, `tests/`, README, phase files (a pre-existing owner `TODO.md` edit is out of scope, not staged by the phase commit) ✓, D15→per-run supersession recorded in the updated pin ✓. (Commit + phase move are the harness's job; task dirs already in `complete/`.)

**Documentation:** README.md — Thinking section correctly describes the phase-158 timeline; config reference matches `.env.example` exactly; page table matches registered routes; no stale single-block wording. AGENTS.md — rules and quick-reference commands all accurate. **Verified consistent — no changes needed.**

**Decisions/deviations:** none. `test_tool_call_compact.py` carries a one-line docstring-only update (referenced the removed `closeToolCalls`); its test logic is byte-identical to the phase-117 contract and green.

**Next pending phase:** none — `todo/` contains only 158.
This commit is contained in:
2026-10-05 02:52:55 -04:00
parent 3f752fb77f
commit 31bd903a93
49 changed files with 4280 additions and 482 deletions
+282
View File
@@ -1543,3 +1543,285 @@ def test_shared_chat_with_failed_record_serves_public_shape(
assert body["messages"][1]["failed"] is True
assert body["messages"][1]["error"] == FAILED_BRAIN["error"]
# ---------------------------------------------------------------------------
# Phase 158 (task 01): the validated ChatMessage.segments field — the
# turn's chronological run order (D4/D6, owner direction 2026-10-04):
# the discriminated thinking/tool union (ChatSegment), capped
# phase-83-style, OMITTED when absent (the phase-50 byte-identical
# round-trip contract — ABSENT, never null), and the extra="forbid"
# boundary INTACT around the new key. The router is UNCHANGED (task
# 01's spec): POST/PUT validate through ChatMessage and store
# m.model_dump(); the admin GET and the public shared read re-validate
# the stored JSONB through ChatMessage — so every surface inherits the
# field + the omission rule from the schema alone.
# ---------------------------------------------------------------------------
#: The full segment array (task 01's spec): one ``thinking`` entry +
#: two ``tool`` entries carrying EVERY additive field (offset/limit,
#: path, truncated/chars_shown/chars_total — the phase-95/139/146
#: shapes; the tool entries are byte-for-byte the ToolCall record
#: shape, the ChatToolSegment mirror).
SEGMENTS: list[dict[str, Any]] = [
{"kind": "thinking", "text": "Step 1: list the documents to find the reference server config."},
{
"kind": "tool",
"name": "read",
"argument": "Homelab/kubernetes.md",
"truncated": True,
"chars_shown": 128_000,
"chars_total": 204_000,
"offset": 42,
"limit": 30,
"path": "search_docs/reese-notes.md",
},
{
"kind": "tool",
"name": "grep",
"argument": "reese-sentinel-42",
"truncated": False,
"chars_shown": None,
"chars_total": None,
"offset": 1,
"limit": 20,
"path": "deployments/traefik.yaml",
},
]
def _segmented_brain() -> dict[str, Any]:
"""A full ``bor.chat.v1`` brain record (phase 113 shape) PLUS the
``segments`` timeline (D4 — ``thinking`` + ``tools`` KEPT, the
runs' order ADDITIVE)."""
return {**FULL_BRAIN, "segments": SEGMENTS}
def test_create_round_trips_segments(admin_client: TestClient, db: Session) -> None:
"""A record WITH ``segments`` (both kinds, every additive tool
field) is ACCEPTED by ``POST /api/chats`` (the phase-113 lesson,
applied up front: the field exists so the ``extra="forbid"``
boundary does not 422 the done-time auto-save carrying it — NOT a
silent 422) and round-trips: the 201 body, the RAW stored JSONB
(byte-for-byte), and the admin GET all carry the array with the
entry ORDER preserved; the user record stays WITHOUT the key.
The PUT upsert carries it too (the re-Save path's
``m.model_dump()`` stores it unchanged)."""
r = admin_client.post(
"/api/chats", json={"messages": [_user(FIRST_QUESTION), _segmented_brain()]}
)
assert r.status_code == 201, f"expected 201, got {r.status_code}: {r.text}"
body = r.json()
assert body["messages"][1]["segments"] == SEGMENTS
assert "segments" not in body["messages"][0]
# The RAW stored JSONB (the DB is the durable boundary): the full
# array, byte-for-byte.
raw = db.execute(
text("SELECT messages FROM saved_chats WHERE id = :id"),
{"id": body["id"]},
).scalar_one()
assert raw[1]["segments"] == SEGMENTS
assert "segments" not in raw[0]
# The admin GET round-trips the array losslessly (order included).
got = admin_client.get(f"/api/chats/{body['id']}")
assert got.status_code == 200
assert got.json()["messages"][1]["segments"] == SEGMENTS
# The re-Save upsert keeps the array (the stored shape is
# re-validated through ChatMessage on every write).
updated = admin_client.put(
f"/api/chats/{body['id']}",
json={"messages": [_user(FIRST_QUESTION), _segmented_brain()]},
)
assert updated.status_code == 200
assert updated.json()["messages"][1]["segments"] == SEGMENTS
def test_shared_serve_includes_segments(admin_client: TestClient) -> None:
"""The shared view is faithful (the shared page's ``addSegments``
parity copy, task 03, renders from it): the PUBLIC snapshot's
brain record carries the segments (the public shape gains no new
top-level key — ``SHARED_OUT_KEYS`` is unchanged), with the entry
order preserved; the user record stays WITHOUT the key."""
created = admin_client.post(
"/api/chats",
json={"title": EXPLICIT_TITLE, "messages": [_user(FIRST_QUESTION), _segmented_brain()]},
).json()
share_url = _share(admin_client, created["id"])["share_url"]
anon = TestClient(fastapi_app) # fresh jar: truly anonymous
r = anon.get(f"/api{share_url}")
assert r.status_code == 200
body = r.json()
assert set(body) == SHARED_OUT_KEYS # no new shared-shape field
assert body["title"] == EXPLICIT_TITLE
assert body["messages"][1]["segments"] == SEGMENTS
assert "segments" not in body["messages"][0]
def test_segments_over_50_entries_rejected(admin_client: TestClient) -> None:
"""``max_length=50`` (the phase-83 voice, the ``tools`` cap): 51
entries 422 at the boundary — far past any realistic turn (the
round cap is 10 → ≤ 20 entries)."""
r = admin_client.post(
"/api/chats",
json={
"messages": [
{
"who": "brain",
"text": "ok",
"segments": [{"kind": "thinking", "text": "x"}] * 51,
}
]
},
)
assert r.status_code == 422
_assert_no_chats(admin_client)
def test_thinking_segment_text_caps_rejected(admin_client: TestClient) -> None:
"""The thinking entry's ``text`` cap (HistoryTurn's 32 000, phase
83): 32 001 chars 422; the ``min_length=1`` bound — an EMPTY text
(the zero-char run the client never stores) 422s too."""
over = admin_client.post(
"/api/chats",
json={
"messages": [
{
"who": "brain",
"text": "ok",
"segments": [{"kind": "thinking", "text": "x" * 32_001}],
}
]
},
)
assert over.status_code == 422
empty = admin_client.post(
"/api/chats",
json={
"messages": [
{
"who": "brain",
"text": "ok",
"segments": [{"kind": "thinking", "text": ""}],
}
]
},
)
assert empty.status_code == 422
_assert_no_chats(admin_client)
def test_tool_segment_caps_rejected(admin_client: TestClient) -> None:
"""The tool entry mirrors ToolCall's caps (mirrored, not
weakened): a ``name`` over 100 and an ``offset`` of 0 (the
``ge=1`` whole-line bound) both 422 — the same values that 422 on
the ``tools`` entry."""
bad_name = admin_client.post(
"/api/chats",
json={
"messages": [
{
"who": "brain",
"text": "ok",
"segments": [
{"kind": "tool", "name": "n" * 101, "argument": None}
],
}
]
},
)
assert bad_name.status_code == 422
bad_offset = admin_client.post(
"/api/chats",
json={
"messages": [
{
"who": "brain",
"text": "ok",
"segments": [{"kind": "tool", "name": "read", "offset": 0}],
}
]
},
)
assert bad_offset.status_code == 422
_assert_no_chats(admin_client)
def test_unknown_segment_kind_rejected(admin_client: TestClient) -> None:
"""The discriminator (``kind`` = ``"thinking"`` | ``"tool"``): an
unknown ``kind`` 422s at the boundary — nothing outside the union
crosses (the ``extra="forbid"`` voice, applied to the variant
pick)."""
r = admin_client.post(
"/api/chats",
json={
"messages": [
{
"who": "brain",
"text": "ok",
"segments": [{"kind": "bogus", "text": "x"}],
}
]
},
)
assert r.status_code == 422
_assert_no_chats(admin_client)
def test_stray_top_level_key_still_rejected_with_segments(
admin_client: TestClient,
) -> None:
"""The ``extra="forbid"`` boundary is INTACT around the new key:
a record that carries a well-formed ``segments`` AND a stray
top-level key still 422s — adding the field changed nothing about
which keys are accepted (the phase-83 voice)."""
bad = _segmented_brain()
bad["foo"] = 1
r = admin_client.post("/api/chats", json={"messages": [bad]})
assert r.status_code == 422
_assert_no_chats(admin_client)
def test_record_without_segments_round_trips_key_absent(
admin_client: TestClient, db: Session
) -> None:
"""The phase-50 byte-identical contract, pinned: a record WITHOUT
``segments`` (the full phase-113 brain shape — every pre-phase
chat) round-trips with the key ABSENT from every surface — the
201 body, the RAW stored JSONB, and the admin GET — NEVER
``null`` (the ``_drop_image_when_absent`` serializer's phase-158
pop). An EXPLICIT empty list is a different thing: ``[]`` is a
value, so it round-trips as ``[]`` (present, empty — the pop is
``None``-gated only)."""
r = admin_client.post(
"/api/chats", json={"messages": [_user(FIRST_QUESTION), FULL_BRAIN]}
)
assert r.status_code == 201, r.text
body = r.json()
assert body["messages"] == _expect([_user(FIRST_QUESTION), FULL_BRAIN])
assert "segments" not in body["messages"][0]
assert "segments" not in body["messages"][1]
raw = db.execute(
text("SELECT messages FROM saved_chats WHERE id = :id"),
{"id": body["id"]},
).scalar_one()
assert "segments" not in raw[0]
assert "segments" not in raw[1]
got = admin_client.get(f"/api/chats/{body['id']}")
assert got.status_code == 200
assert "segments" not in got.json()["messages"][0]
assert "segments" not in got.json()["messages"][1]
# An explicit empty list round-trips as [] (present, not popped).
r2 = admin_client.post(
"/api/chats",
json={"messages": [{"who": "brain", "text": "ok", "segments": []}]},
)
assert r2.status_code == 201, r2.text
got2 = admin_client.get(f"/api/chats/{r2.json()['id']}")
assert got2.json()["messages"][0]["segments"] == []