phase: 124_image_chips_and_answers
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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user