phase: 124_image_chips_and_answers
Build and Push Containers / build-and-push-app (push) Successful in 2m12s
Build and Push Containers / build-and-push-db (push) Failing after 1m39s

All green. Final report:

**Phase 124 final verification pass — all criteria verified, no defects found (nothing to fix)**
- Verified the implemented phase (all 7 tasks done, changes in working tree): `BOR_ANSWER_IMAGES` toggle, `show_image` tool + refusals + `holder.shown_images`, toggle-gated prompt clause, `answer_image` frame field (None-omitting serializer) + `ChatMessage` persistence, `merge_cited_docs` auto-cite bug fix, answer-figure UI (live/restore/shared/CSS), mock triggers + E2E suite. Also confirmed the bytes route → public change matches the phase-123 shared-chat precedent (task 06 design).
- `uv run pytest --cov=app --cov-report=term-missing` → **2848 passed**, TOTAL **99%** (>90% gate)
- `bash .agents/validate.sh` → **validation OK** (tests + coverage + ruff + pyright)
- `uv run ruff check . && uv run pyright` → clean, 0 errors
- `uv run pytest tests/e2e/test_answer_images.py -v --no-cov` (isolated) → **5/5 passed**
- Mock-inert spot checks: `tests/e2e/test_image_documents.py` 4/4, `tests/e2e/test_agent_document_tools.py` 4/4

**Completion criteria:** ① zero-read suggested-tier image doc chips with figure — verified (unit matrix, integration `test_zero_read_grounded_turn_auto_cites_suggested_image_doc`, E2E bug-fix flows); ② show_image → answer figure below text/above chips, second call refused, persists + restore + shared — verified (unit handler matrix, integration round-trip, E2E feature flow); ③ default-off byte-identical (3 tools, no clause, no key) — verified (unit + integration + E2E toggle-off); ④ full suite green / >90% / lint clean — verified above; ⑤ commit + phase move — harness responsibility (changes left uncommitted in working tree, no live-infrastructure changes made).

**Next pending phase:** none — `todo/` holds only this phase; the pipeline is clear once the harness moves it.
This commit is contained in:
2026-09-26 17:06:09 -04:00
parent 7bd6335929
commit 62e4f6b572
65 changed files with 4909 additions and 46 deletions
+106
View File
@@ -899,6 +899,112 @@ def test_shared_serve_includes_the_user_image_path(
assert "image" not in messages[2]
# ---------------------------------------------------------------------------
# Phase 124 (task 04): the brain answer's first-class image — the
# shown document's ref (the LLM's ``show_image`` pick) rides the BRAIN
# record's ``answer_image`` key (distinct from the user record's
# phase-123 ``image`` attachment path — the attachment belongs to the
# question, the answer image to the brain's reply). The phase-113
# lesson applied up front: the done-time auto-save (``rememberBrainTurn``)
# carries the key, and the ``extra="forbid"`` boundary silently 422'd
# any key the model was missing (the A2 quiet-failure path swallowed
# it — grounded turns silently unsaved). These round-trips are that
# regression pin.
# ---------------------------------------------------------------------------
#: The shown image doc's full ref — the done frame's ``answer_image``
#: shape (source/path/title + the ``image_url`` bytes route).
ANSWER_IMAGE_REF: dict[str, Any] = {
"source": "Homelab",
"path": "kubernetes-cluster.png",
"title": "Kubernetes Cluster Diagram",
"image_url": "/api/documents/0e8a6e5d-7b6c-4f2a-9c1d-3e5f7a9b1c2d/image",
}
def test_create_round_trips_the_brain_answer_image_ref(
admin_client: TestClient, db: Session
) -> None:
"""The phase-113 regression pin (the A2 quiet-failure path): a
brain record carrying ``answer_image`` → ``POST /api/chats`` is
201 (NOT a silent 422) and the served chat round-trips the ref
BYTE-FOR-BYTE (the restore path re-renders the answer figure from
it). The user record carries NO ``answer_image`` key, and the
user's phase-123 ``image`` attachment key stays distinct."""
brain = {**FULL_BRAIN, "answer_image": ANSWER_IMAGE_REF}
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_image"] == ANSWER_IMAGE_REF
assert "answer_image" not in body["messages"][0]
assert "image" not in body["messages"][1] # the brain's image is a REF
# The RAW stored JSONB (the DB is the durable boundary): the full
# ref, byte-for-byte.
raw = db.execute(
text("SELECT messages FROM saved_chats WHERE id = :id"),
{"id": body["id"]},
).scalar_one()
assert raw[1]["answer_image"] == ANSWER_IMAGE_REF
assert "answer_image" not in raw[0]
# The admin GET round-trips the ref losslessly.
got = admin_client.get(f"/api/chats/{body['id']}")
assert got.status_code == 200
assert got.json()["messages"][1]["answer_image"] == ANSWER_IMAGE_REF
def test_create_without_answer_image_round_trips_unchanged(
admin_client: TestClient,
) -> None:
"""A record WITHOUT the key (every pre-phase-124 chat, every
image-less turn) round-trips UNCHANGED: the response carries no
``answer_image`` key at all — ABSENT, never ``null`` (the
byte-identical-off contract; the phase-50 round-trip contract).
``_expect`` (the stored shape) is unchanged by phase 124 — the
omission serializer inherits it on every surface."""
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_image" not in body["messages"][0]
assert "answer_image" not in body["messages"][1]
got = admin_client.get(f"/api/chats/{body['id']}")
assert got.status_code == 200
assert "answer_image" not in got.json()["messages"][1]
def test_shared_serve_includes_the_brain_answer_image_ref(
admin_client: TestClient,
) -> None:
"""The shared view is faithful (task 06's render depends on it):
the PUBLIC snapshot's brain record carries the answer ref (the
bytes route is public — a shared chat is faithful, the phase-123
user-image precedent), and the user record stays WITHOUT the key."""
brain = {**FULL_BRAIN, "answer_image": ANSWER_IMAGE_REF}
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_image"] == ANSWER_IMAGE_REF
assert "answer_image" not in messages[0]
def test_public_read_wrong_and_revoked_tokens_404_with_one_detail(
admin_client: TestClient,
) -> None: