phase: 130_chat_answer_clusters
**Phase 130 — final verification pass: 100% green, complete.** - **Verified:** chips-only row (no chip for image docs) → images-only row (`.msg-images`) → related row → `.msg-actions` last; `getActionsRow` used by all 3 buttons + stopped/failed notes; `markLastRetryable` save-re-append hack gone; in-place chip fallback on image-load failure; CSS right-align with per-button auto margins removed — implementation matches the design in every detail. - **Completion criteria:** (1) mixed citation renders chips→images→actions-last, DOM-asserted ✅ (2) all 3 buttons in one right-aligned `.msg-actions`; non-admin sees Retry-only; restore path identical ✅ (3) 404-bytes figure → plain chip, unit-pinned ✅ (4) E2E isolated ✅ (5) no behavior change (modal affordances, related tier, ARIA names) ✅ - **Tests/lint/coverage:** `uv run pytest tests/e2e/test_chat_answer_clusters.py -v --no-cov` → 4 passed; `uv run pytest --cov=app --cov-report=term-missing` → 3061 passed, **99%** app coverage; the 4 E2E files this phase modified (test_answer_images, test_image_documents, test_pdf_support, test_responsive_polish) → 22 passed in isolation; `uv run ruff check .` clean, `uv run pyright` → 0 errors. - **Docs:** README.md — verified consistent in full (page table vs routes, env-var coverage vs `.env.example`, phase-130 cluster wording already in tree); no changes needed. AGENTS.md — fixed one pre-existing inconsistency: rule 5 still cited the retired "centered 46rem chat column"; now states the 72rem container contract (phase 100). - **Decisions:** PLAN.md §7.1 still says 46rem, but it's owner-managed (phase 100 records the supersession pending owner sign-off) and out of my editable scope — AGENTS.md now cross-references the supersession. - **Next pending phase:** `131_read_line_ranges` (todo/).
This commit is contained in:
@@ -19,16 +19,18 @@ mock LLM and the shared e2e Postgres (truncate/re-import per app):
|
||||
turn, the shape that makes the chip assertion meaningful). The
|
||||
brain bubble shows the ANSWER FIGURE (``.answer-image`` — ``<img>``
|
||||
from the bytes route, ``loading="lazy"``, below the text and above
|
||||
the chip row per A4, alt/caption settling to the summary), the
|
||||
answer TEXT is still streamed in full (the tail echo of the
|
||||
the source clusters per A4, alt/caption settling to the summary),
|
||||
the answer TEXT is still streamed in full (the tail echo of the
|
||||
``<documents>`` seed proves the description was in the prompt), the
|
||||
source-chip row carries the image doc's chip + its phase-122
|
||||
figure (the AUTO-CITE proof — the mock made zero ``read`` calls,
|
||||
asserted on the mock's request capture), and the "Nearby docs" row
|
||||
does not repeat the doc. The wire pin: the first model request
|
||||
offered FOUR tools (the three base + ``show_image``) and the turn
|
||||
executed exactly one ``show_image`` — the saved brain record
|
||||
persists the ``answer_image`` ref.
|
||||
sources block carries the image doc's citation as the compact
|
||||
figure in the images row (``.msg-meta.msg-images`` — phase 130
|
||||
task 01: NO chip for an image doc, the figure IS the citation;
|
||||
the AUTO-CITE proof — the mock made zero ``read`` calls, asserted
|
||||
on the mock's request capture), and the "Nearby docs" row does not
|
||||
repeat the doc. The wire pin: the first model request offered
|
||||
FOUR tools (the three base + ``show_image``) and the turn executed
|
||||
exactly one ``show_image`` — the saved brain record persists the
|
||||
``answer_image`` ref.
|
||||
- **Restore + share** — a second feature turn, then a page RELOAD:
|
||||
the restored brain bubble shows the figure again (the
|
||||
``answer_image`` persistence through the ``bor.chat.v1`` restore
|
||||
@@ -36,11 +38,12 @@ mock LLM and the shared e2e Postgres (truncate/re-import per app):
|
||||
fresh anonymous context) renders the figure on the shared page.
|
||||
- **Bug-fix-only flow** — the ``which diagram is in your notes``
|
||||
trigger (the mock answers straight from the seed with ZERO tool
|
||||
calls — no ``show_image``, no ``read``): the source-chip row STILL
|
||||
carries the image doc's chip + figure (the bug fix on the pure
|
||||
zero-read path — the assertion the owner's report describes), the
|
||||
answer text quotes the seeded description's tail, and NO answer
|
||||
figure renders (the model never called the tool).
|
||||
calls — no ``show_image``, no ``read``): the sources block STILL
|
||||
carries the image doc's citation figure (no chip — phase 130;
|
||||
the bug fix on the pure zero-read path — the assertion the owner's
|
||||
report describes), the answer text quotes the seeded
|
||||
description's tail, and NO answer figure renders (the model never
|
||||
called the tool).
|
||||
* **App B (default off)** — ``BOR_IMAGES=true``, ``BOR_ANSWER_IMAGES``
|
||||
pinned to the code default ``false`` (EXPLICITLY — process env ranks
|
||||
above an operator's local gitignored ``.env``, so the default-off
|
||||
@@ -51,9 +54,10 @@ mock LLM and the shared e2e Postgres (truncate/re-import per app):
|
||||
``show_image`` among the offered tools (the request capture carries
|
||||
exactly the THREE base tools), the brain bubble has NO answer
|
||||
figure, and the saved brain record carries NO ``answer_image`` key
|
||||
(the byte-identical-off contract). The bug-fix trigger STILL chips
|
||||
the image doc (the chips fix is NOT gated by ``BOR_ANSWER_IMAGES`` —
|
||||
it belongs to the ``BOR_IMAGES``-indexed docs, per the phase design).
|
||||
(the byte-identical-off contract). The bug-fix trigger STILL cites
|
||||
the image doc (the figure, no chip — phase 130; the auto-cite fix is
|
||||
NOT gated by ``BOR_ANSWER_IMAGES`` — it belongs to the
|
||||
``BOR_IMAGES``-indexed docs, per the phase design).
|
||||
|
||||
DB isolation: the suites share one Postgres; ``saved_chats`` survives
|
||||
the truncation (the house pattern), so every question text here is
|
||||
@@ -593,15 +597,18 @@ def _assert_answer_figure(
|
||||
def _assert_image_chip(
|
||||
page: Page, seeded: dict[str, str], *, last_brain: str = ".msg.brain"
|
||||
) -> None:
|
||||
"""The citation chip the AUTO-CITE earns (phase 124 task 05): the
|
||||
image doc's text chip + its phase-122 compact figure (``<img>``
|
||||
from the bytes route, alt settling to the summary — the admin
|
||||
surface; the shared page's chip is plain text and asserted
|
||||
separately)."""
|
||||
"""The AUTO-CITE (phase 124 task 05) in the phase-130 cluster
|
||||
shape (task 01, TODO L6): an image doc gets NO chip — its figure
|
||||
IS the citation — the compact figure (``<img>`` from the bytes
|
||||
route, alt settling to the summary — the admin surface; the
|
||||
shared page's plain-text chip is asserted separately) sits in the
|
||||
images row (``.msg-meta.msg-images``)."""
|
||||
chip = page.locator(f"{last_brain} .source-chip")
|
||||
expect(chip).to_have_count(1)
|
||||
expect(chip).to_have_text(f"{SOURCE_NAME}/{DOC_PATH}")
|
||||
fig = page.locator(f"{last_brain} .source-image")
|
||||
expect(chip).to_have_count(0)
|
||||
row = page.locator(f"{last_brain} .msg-meta.msg-images")
|
||||
expect(row).to_have_count(1)
|
||||
expect(row).to_have_attribute("aria-label", "Source images")
|
||||
fig = row.locator(".source-image")
|
||||
expect(fig).to_have_count(1)
|
||||
img = fig.locator(".source-image-img")
|
||||
expect(img).to_be_visible(timeout=15_000)
|
||||
@@ -612,7 +619,7 @@ def _assert_image_chip(
|
||||
def _body_child_classes(page: Page) -> list[str]:
|
||||
"""The LAST brain bubble's ``.msg-body`` direct children's class
|
||||
names, in DOM order — the A4 position pin (bubble < answer-image
|
||||
< msg-meta)."""
|
||||
< the first .msg-meta-family source row)."""
|
||||
return page.evaluate(
|
||||
"""() => {
|
||||
const body = [...document.querySelectorAll(".msg.brain")].pop()
|
||||
@@ -635,15 +642,18 @@ def test_feature_flow_answer_figure_chips_and_zero_reads(
|
||||
a ``show_image`` call (NO ``read`` — the zero-read turn). Asserts:
|
||||
(1) the brain bubble's ``.msg-body`` carries ``.answer-image`` with
|
||||
an ``<img>`` from the bytes route (200, image Content-Type) and
|
||||
``loading="lazy"``; (2) the figure sits BEFORE the ``.msg-meta``
|
||||
source-chip row (A4); (3) the answer TEXT is non-empty and quotes
|
||||
the seed's tail (the "summaries still provided" contract + the
|
||||
echo proving the seed was in the prompt); (4) the source-chip row
|
||||
carries the image doc's chip + its figure (the AUTO-CITE proof —
|
||||
the mock made ZERO ``read`` calls: only ``show_image`` — asserted
|
||||
on the wire); (5) the "Nearby docs" row does NOT repeat the doc.
|
||||
Plus the persistence pin: the saved brain record carries the
|
||||
``answer_image`` ref (the task-04 frame field, round-tripped)."""
|
||||
``loading="lazy"``; (2) the figure sits BELOW the answer text and
|
||||
ABOVE the source clusters (A4 — the image-only KB renders the
|
||||
images row, ``.msg-meta.msg-images`` — phase 130 task 01: no chip
|
||||
row); (3) the answer TEXT is non-empty and quotes the seed's tail
|
||||
(the "summaries still provided" contract + the echo proving the
|
||||
seed was in the prompt); (4) the images row carries the image
|
||||
doc's figure — no chip (phase 130: the figure IS the citation) —
|
||||
the AUTO-CITE proof (the mock made ZERO ``read`` calls: only
|
||||
``show_image`` — asserted on the wire); (5) the "Nearby docs" row
|
||||
does NOT repeat the doc. Plus the persistence pin: the saved
|
||||
brain record carries the ``answer_image`` ref (the task-04 frame
|
||||
field, round-tripped)."""
|
||||
login(page, app_url, next="/")
|
||||
expect(page.locator("#kb-banner")).to_be_hidden()
|
||||
|
||||
@@ -664,20 +674,25 @@ def test_feature_flow_answer_figure_chips_and_zero_reads(
|
||||
# summary (the fetchContentSummary pattern settles both).
|
||||
_assert_answer_figure(page, app_url, seeded, caption=seeded["description"])
|
||||
|
||||
# (2) A4 position: below the answer text, above the chip row.
|
||||
# (2) A4 position: below the answer text, above the source
|
||||
# clusters — the image-only KB renders the images row
|
||||
# (.msg-meta.msg-images, phase 130 task 01: no chip row).
|
||||
classes = _body_child_classes(page)
|
||||
assert "bubble" in classes, f"no answer bubble in .msg-body: {classes}"
|
||||
assert "answer-image" in classes, f"no .answer-image in .msg-body: {classes}"
|
||||
assert "msg-meta" in classes, f"no chip row in .msg-body: {classes}"
|
||||
meta_idx = next(
|
||||
(i for i, c in enumerate(classes) if c.startswith("msg-meta")), None
|
||||
)
|
||||
assert meta_idx is not None, f"no source row in .msg-body: {classes}"
|
||||
assert (
|
||||
classes.index("bubble")
|
||||
< classes.index("answer-image")
|
||||
< classes.index("msg-meta")
|
||||
), f"A4 order violated (bubble < answer-image < msg-meta): {classes}"
|
||||
< meta_idx
|
||||
), f"A4 order violated (bubble < answer-image < source row): {classes}"
|
||||
|
||||
# (4) The chip row carries the image doc's chip + its figure —
|
||||
# the AUTO-CITE (the model never read it; the seed IS its
|
||||
# entire content, so suggested == read for it).
|
||||
# (4) The images row carries the image doc's figure (no chip —
|
||||
# phase 130) — the AUTO-CITE (the model never read it; the
|
||||
# seed IS its entire content, so suggested == read for it).
|
||||
_assert_image_chip(page, seeded)
|
||||
|
||||
# (5) The "Nearby docs" row does NOT repeat the image doc — the
|
||||
@@ -817,8 +832,8 @@ def test_reload_restores_figure_and_shared_page_shows_it(
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. App A — the bug-fix-only flow: ZERO tool calls, the chip STILL
|
||||
# appears (the pure zero-read path — the owner's report)
|
||||
# 3. App A — the bug-fix-only flow: ZERO tool calls, the citation
|
||||
# STILL renders (the pure zero-read path — the owner's report)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -828,11 +843,12 @@ def test_bugfix_only_flow_zero_tool_calls_still_chips(
|
||||
"""The ``which diagram is in your notes`` trigger: the mock
|
||||
answers straight from the seed with ZERO tool calls (no
|
||||
``show_image`` — the clause's flow is not triggered — and no
|
||||
``read``): the source-chip row STILL carries the image doc's chip
|
||||
+ figure (the bug fix on the pure zero-read path — the assertion
|
||||
the owner's report describes), the answer text quotes the seeded
|
||||
description's tail (the description WAS in the prompt), and NO
|
||||
answer figure renders (the model never called the tool)."""
|
||||
``read``): the sources block STILL carries the image doc's
|
||||
citation figure (no chip — phase 130; the bug fix on the pure
|
||||
zero-read path — the assertion the owner's report describes), the
|
||||
answer text quotes the seeded description's tail (the description
|
||||
WAS in the prompt), and NO answer figure renders (the model never
|
||||
called the tool)."""
|
||||
login(page, app_url, next="/")
|
||||
expect(page.locator("#kb-banner")).to_be_hidden()
|
||||
|
||||
@@ -860,8 +876,9 @@ def test_bugfix_only_flow_zero_tool_calls_still_chips(
|
||||
expect(page.locator(".msg.brain .tool-calls-disclosure")).to_have_count(0)
|
||||
|
||||
# NO answer figure (the model never called show_image) — but the
|
||||
# chip row STILL carries the image doc's chip + figure (the auto-
|
||||
# cite: suggested == read for an image doc, no read performed).
|
||||
# sources block STILL carries the image doc's citation figure
|
||||
# (no chip — phase 130; the auto-cite: suggested == read for an
|
||||
# image doc, no read performed).
|
||||
expect(page.locator(".msg.brain .answer-image")).to_have_count(0)
|
||||
_assert_image_chip(page, seeded)
|
||||
|
||||
@@ -936,8 +953,8 @@ def test_toggle_off_flow_no_tool_no_answer_image_key(
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 5. App B (default off) — the bug-fix trigger STILL chips the image
|
||||
# doc (the chips fix belongs to BOR_IMAGES, not BOR_ANSWER_IMAGES)
|
||||
# 5. App B (default off) — the bug-fix trigger STILL cites the image
|
||||
# doc (the auto-cite fix belongs to BOR_IMAGES, not BOR_ANSWER_IMAGES)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -946,10 +963,11 @@ def test_toggle_off_bugfix_trigger_still_chips(
|
||||
) -> None:
|
||||
"""The bug-fix trigger on the default-OFF app (the clause is
|
||||
absent — the trigger does NOT require it): ZERO tool calls, the
|
||||
answer quotes the seeded description's tail, and the source-chip
|
||||
row STILL carries the image doc's chip + figure — the chips fix
|
||||
is NOT gated by ``BOR_ANSWER_IMAGES`` (it belongs to the
|
||||
``BOR_IMAGES``-indexed docs, per the phase design)."""
|
||||
answer quotes the seeded description's tail, and the sources
|
||||
block STILL carries the image doc's citation figure (no chip —
|
||||
phase 130) — the auto-cite fix is NOT gated by
|
||||
``BOR_ANSWER_IMAGES`` (it belongs to the ``BOR_IMAGES``-indexed
|
||||
docs, per the phase design)."""
|
||||
login(page, default_app_url, next="/")
|
||||
expect(page.locator("#kb-banner")).to_be_hidden()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user