phase: 153_document_viewer_context
Build and Push Containers / build-and-push-app (push) Successful in 2m45s
Build and Push Containers / build-and-push-db (push) Successful in 19s

That last `--co` (collect-only) run was my probe mistake — it shows 34% because no tests execute under collect-only. The valid coverage run was the real gate command executed earlier: `uv run pytest --cov=app --cov-report=term-missing` → **TOTAL 99%** (exit 0, all 3544 tests executed). All gates are verified.

## Phase 153 — final verification pass report

**Verified (all tasks 01–04 complete in `complete/`):**
- Modal `fullPageUrl(source, path, back = null)` + `openDocumentModal(…, back = null)` with module-level `currentBack`; 5 chat call sites pass `"/"`, sources.js call site unchanged (byte-identical Sources path); `documentUrl` comment now true
- Copy-path control (shared `renderDocument` core, icon+text, text = accessible name) on both surfaces; per-page clipboard helpers with the fallback-field + status-line contract; `#doc-copy-announcer` live region; styles + 44px touch floor
- Unit pins: `&back=%2F` / no-`back` byte-identity, `currentBack` single-writer guard, copy strings byte-pinned, phase-13 regression pins untouched

**Test / lint / coverage results:**
- `uv run pytest` → **3544 passed**, 0 failed (full unit + integration)
- `uv run pytest --cov=app --cov-report=term-missing` → **TOTAL 99%** (>90% gate)
- `uv run pytest tests/e2e/test_document_viewer_context.py -v --no-cov` → **8 passed** in 47.33s, in isolation (DB up, mock LLM)
- `uv run ruff check . && uv run pyright` → clean (0 errors/warnings)

**Completion criteria:** all met — (1) chat→modal→Full page: `href="/"` + "Chat"; Sources→modal→Full page: `href="/sources.html"` + "Sources" with `back=` asserted absent; (2) no-`back` URLs byte-identical to pre-phase; (3) Copy path: "Path copied." on success (clipboard readback), prefilled select-on-focus fallback field + "Path ready — copy it from the field." on rejected clipboard, both surfaces parameterized; (4) copied string exactly `docs/homelab/kubernetes.md`; (5) full suite green, E2E isolated green, lint clean, coverage 99%; (6) commit by harness.

**Documentation:** README.md reviewed in full — phase-153 feature already named in the Chat section; routes, env reference, workflows, deployment all consistent — no changes needed. AGENTS.md reviewed in full — rules, gates, and quick-reference commands verified against the repo (incl. `test_smoke.py`, `.env.example`, gitignore entries) — consistent, no changes needed.

**Notable:** pytest 9.1.1 quirk — a doubled `-q` (pyproject `addopts` + explicit `-q`) swallows the final "N passed" summary line while still exiting 0; the plain `uv run pytest` gate command prints it. No defects found in prior work.

**Next pending phase:** `154_sync_status_freshness`.
This commit is contained in:
2026-10-04 09:23:54 -04:00
parent 12fa69b7ea
commit 966c4b0540
59 changed files with 4304 additions and 34 deletions
@@ -0,0 +1,69 @@
# Task 01 — the validated `ChatMessage.segments` field (server)
**Phase:** `158_thinking_timeline` · **Story:** n/a
## Objective
Saved-chat writes accept the new optional `segments` array (a discriminated
`thinking`/`tool` union with phase-83-style caps), omit it when absent so
pre-phase records round-trip byte-identically, and reject malformed values at
the `extra="forbid"` boundary.
## Work
1. `app/schemas.py`:
- `ChatThinkingSegment` (a `BaseModel`, beside `ToolCall`): `kind:
Literal["thinking"]`, `text: str = Field(min_length=1, max_length=32_000)`
(`HistoryTurn`'s cap, phase 83 — docstring cross-reference).
- `ChatToolSegment` (a `BaseModel`): `kind: Literal["tool"]` + the EXACT
`ToolCall` field set (name / argument / offset / limit / path / truncated /
chars_shown / chars_total with their existing types and caps — mirror, do
not weaken; docstring cross-references `ToolCall` as the shape source).
- `ChatSegment = Annotated[Union[ChatThinkingSegment, ChatToolSegment],
Field(discriminator="kind")]`.
- `ChatMessage.segments: list[ChatSegment] | None = Field(default=None,
max_length=50)` — with the phase-83 voice comment (the cap sizing: far
past any realistic turn — the round cap is 10 → ≤ 20 entries; 50 matches
the `tools` cap).
- The EXISTING wrap serializer (`_drop_image_when_absent`) gains the
omission rule: after the two image pops, `if self.segments is None:
data.pop("segments", None)` — ABSENT, never `null` (the phase-50
round-trip contract; the phase-122 `SourceRef.image_url` omission
precedent, the phase-113 lesson applied up front: the field EXISTS so
the boundary does NOT 422 the done-time auto-save carrying it).
- The `ChatMessage` docstring gains the phase-158 paragraph (the shape,
the omission rule, the `extra="forbid"` boundary) in the established
voice; `HistoryTurn` and everything in `app/rag/` is UNTOUCHED (the
history wire stays `{who, text, thinking}`).
2. NO migration, NO router change (`app/api/chats.py` validates through
`ChatMessage` — `m.model_dump()` + `ChatMessage.model_validate(m)` already
carry the new field end to end, including the public shared read
`SharedChatOut.messages`).
## Testing & Quality
- Integration (extend `tests/integration/test_chats_api.py` — the file already
pins the brain-record contract, phase 83/113/120/123/124):
- a record WITH `segments` (one `thinking` entry + two `tool` entries
carrying every additive field: `offset/limit`, `path`,
`truncated/chars_shown/chars_total`) is accepted by `POST /api/chats` and
round-trips through `GET` (the shape preserved, entry order preserved);
- `segments` with 51 entries → 422; a thinking-segment `text` of 32 001
chars → 422; a segment with `kind: "bogus"` → 422; a record with a stray
top-level key (e.g. `"foo": 1`) still → 422 (the `extra="forbid"`
boundary intact);
- a record WITHOUT `segments` (a pre-phase brain record, the full phase-113
shape) round-trips with the key ABSENT from the served JSON (never
`null` — the byte-identity pin, the phase-50 contract);
- the shared read path (the public read endpoint's `messages`) carries the
segments of a saved record with them (the public shape, the phase-51
contract).
- Coverage: **>90%** on this task's new/modified code (the schema code is
exercised by the integration tests).
## Completion Criteria
- [ ] the three models + the field + the serializer rule land exactly as
designed (a source check)
- [ ] the new integration tests are green; the EXISTING
`test_chats_api.py` tests stay green unchanged
- [ ] `uv run pytest tests/integration -q` green; `uv run ruff check . && uv
run pyright` clean
- [ ] no change in `app/rag/`, no migration, no router change (a `git diff
--stat` scoped check)