phase: 170_edit_bubble_sizing
Build and Push Containers / build-and-push-app (push) Successful in 1m42s
Build and Push Containers / build-and-push-db (push) Successful in 13s

All verification complete. Final report:

**Phase 170 — final verification pass (all 3 tasks were in `complete/`):**

**Verified implemented:**
- Editor renders inside the bubble: `bubble.replaceChildren(form)`, `display:none` rule deleted, `form.style.minWidth` seeded with pre-edit width, `restoreBubble()` (markdown + image re-prepend) before `closeEditor()` on Cancel/Escape, `closeEditor()` reordered before submit re-render
- Label: `editSaveBtn.textContent = "Send"` (app.js L1241)

**Gates (all 100% green):**
- `uv run pytest` → 4066 passed
- `uv run pytest --cov=app --cov-report=term-missing` → 99% on `app/` (gate >90%)
- `uv run ruff check . && uv run pyright` → clean (0 errors, 0 warnings)
- `uv run pytest tests/e2e/test_edit_bubble_sizing.py -v --no-cov` (in isolation) → 6/6 passed
- Regression set in isolation: `test_edit_and_resend.py` 7✓, `test_retry_answer.py` 4✓, `test_failed_turn_retry.py` 4✓, `test_chat_persistence.py` 4✓

**Completion criteria:** in-bubble editor + width pin ✅ · "Send" label + cancel restore + trim re-ask + "Question too long" driver case ✅ · suite/coverage/lint ✅ · E2E + regression isolation ✅ · phase dir moved to `complete/` ✅ (atomic commit is the harness's to make — changes left in the working tree per protocol)

**Documentation:** README.md reviewed in full — edit section already aligned to phase 170 (in-bubble editor + "Send"), pages table matches actual routes, all 29 `.env.example` vars documented, setup commands accurate; AGENTS.md reviewed — all referenced files/commands exist. Both verified consistent — no changes needed.

**Notable decisions:** none — verification pass only, no code changes required.

**Next pending phase:** `171_admin_settings_layout`
This commit is contained in:
2026-10-10 03:25:40 -04:00
parent 1200437bf8
commit c4f098d0c0
27 changed files with 1607 additions and 120 deletions
+8 -8
View File
@@ -26,10 +26,10 @@ The contract under test (phase 159, owner-locked 2026-10-05):
earlier bubbles are never editable.
* The Edit opens an INLINE editor in the bubble (the ``.tune-form``
family — the textarea prefilled with the question,
``maxlength`` = the 4000 cap, Save/Cancel, a ``role=alert`` error
``maxlength`` = the 4000 cap, Send/Cancel, a ``role=alert`` error
line); Cancel/``Escape`` restore the bubble and return focus to the
toggle.
* Save = REPLACE + TRUNCATE + RE-ASK (the phase-49 redo-in-place rule,
* Send = REPLACE + TRUNCATE + RE-ASK (the phase-49 redo-in-place rule,
one level up): the edited text replaces the record in place, every
record after it is dropped (DOM + ``bor.chat.v1`` + the saved chat
row), and a fresh answer streams from the edited text with
@@ -48,7 +48,7 @@ Test → contract mapping:
1. ``test_edit_last_question_reasks_in_place`` — send, grounded answer
(ONE old brain bubble), the user bubble carries Edit; the editor
prefills the EXACT question; append " in one line" + Save: exactly
prefills the EXACT question; append " in one line" + Send: exactly
ONE user bubble (the edited text), the old answer bubble is gone,
the new answer streams with the marker, ``bor.chat.v1`` holds
``[user(edited), brain]`` with NO duplicate question, and the
@@ -76,7 +76,7 @@ Test → contract mapping:
this identical settle path, its exact detail string is
real-endpoint-only, see the module docstring); Edit exists on the
question BESIDE the failed bubble's Retry; trimming (the trigger
stripped) + Save streams the grounded answer (the failed record is
stripped) + Send streams the grounded answer (the failed record is
part of the dropped tail — ``failed: true`` never survives).
6. ``test_edit_is_inert_while_the_turn_runs`` — the ``write a long
answer`` trigger (~8 s): NO ``.edit-btn`` while the turn runs
@@ -252,7 +252,7 @@ def _poll_saved_messages(
phase-55 upserts are fire-and-forget — there is no status line to
wait on; the ``#send-label`` settle only proves the LAST save
point fired, not that its PUT landed). The row's title keeps the
FIRST question on re-Save (title-keep), so the edit's row is the
FIRST question on re-Send (title-keep), so the edit's row is the
same row, polled by the original title."""
deadline = time.monotonic() + 15
last: list[dict] | None = None
@@ -338,7 +338,7 @@ def _save_editor(page: Page, new_text: str) -> None:
# --------------------------------------------------------------------------
# 1 — the flow: send → Edit (prefilled) → trim + Save → the old answer
# 1 — the flow: send → Edit (prefilled) → trim + Send → the old answer
# is gone, the new one streams, the record + the row are truncated
# --------------------------------------------------------------------------
@@ -380,7 +380,7 @@ def test_edit_last_question_reasks_in_place(
assert MOCK_ANSWER_MARKER in stored["messages"][1]["text"]
# The auto-saved row (polled — the title keeps the FIRST question
# on re-Save): the truncated shape [user(edited), brain], matching
# on re-Send): the truncated shape [user(edited), brain], matching
# the localStorage shape key-for-key (the DB is the durable boundary).
row = _poll_saved_messages(
Q, lambda msgs: len(msgs) == 2 and msgs[0].get("text") == Q_TEST1_EDITED
@@ -507,7 +507,7 @@ def test_edit_drops_a_stale_tail_and_survives_refresh(
# --------------------------------------------------------------------------
# 5 — the driver case (TODO.md L3): a failed turn's question is
# editable; trimming + Save re-asks successfully (the phase-114
# editable; trimming + Send re-asks successfully (the phase-114
# "Question too long" frame takes this identical settle path — its
# exact detail string is real-endpoint-only, see the module
# docstring)