phase: 115_doc_draft_discard
Build and Push Containers / build-and-push-app (push) Successful in 2m12s
Build and Push Containers / build-and-push-db (push) Successful in 14s

All green — this was the final verification pass; everything from the four completed tasks was already in the working tree and verified.

**Phase 115 — Doc drafts: Discard + DELETE route + title fix — verification report**

- Verified all 4 task deliverables present: DELETE route (`app/api/doc_drafts.py`), Discard UI (`doc-edit.html` + `doc-edit.js` + `.discard-draft` CSS), title fix (`defaultDocTitle(wrap)` pairing + `saveAsDoc` call site), and all test pins (integration, frontend unit, E2E). No code changes needed.
- **Completion criteria:**
  1. ✅ Orphaned draft discardable from edit screen; row gone — `test_delete_removes_row_and_invalidates_token` (204 → GET 404), unknown-token 404, admin-gate 403 on all routes, E2E `test_discard_draft_from_edit_screen` all pass.
  2. ✅ Title after retry redo = redone answer's own question — E2E `test_save_title_is_the_redo_question_after_retry` passes.
  3. ✅ Push flow byte-identical — `git diff` shows only the new DELETE route + module docstring; all 7 existing push tests green.
  4. ✅ `uv run pytest --cov=app` → **2457 passed**, app coverage **99%** (>90%); `uv run pytest tests/e2e/test_save_doc_session.py -v --no-cov` → **4 passed**; `uv run ruff check .` → clean; `uv run pyright` → 0 errors.
  5. ⏳ Commit + phase-dir move left to the harness (per executor rules, no `git` run; all changes left in the working tree).
- No defects found; no deviations.
- Next pending phase: none in `todo/` other than this one (`115_doc_draft_discard` is the last).
This commit is contained in:
2026-09-15 05:35:05 -04:00
parent 3846f26a58
commit 990c8adf13
29 changed files with 1384 additions and 30 deletions
+48 -10
View File
@@ -202,28 +202,66 @@ def test_app_js_call_sites_pass_the_raw_markdown() -> None:
# ---------------------------------------------------------------------------
# app.js — the click: payload, slug rule, navigation, failure copy
# app.js — the title: the paired user question (phase 115) + the click
# ---------------------------------------------------------------------------
def test_app_js_default_title_is_the_last_user_question() -> None:
"""The default title: the LAST user question's text,
whitespace-collapsed, ≤120 chars (the phase-50 auto-title
convention — the chat auto-title targets the FIRST question, the
docs default the LAST). Defensive "Note" with no user record."""
def test_app_js_default_title_is_the_paired_user_question() -> None:
"""Phase 115 (task 03): defaultDocTitle(wrap) — the title is the
text of the user bubble PAIRED with the saved brain bubble: the
NEAREST preceding .msg.user in the DOM conversation flow (the
redo-in-place reorders the DOM, and the structural pair IS the
answer's question by construction — the last conversation record
can be an unrelated trailing question after a retry). The text is
read from the bubble's .bubble (the meta rows carry button labels
— never the whole wrap). No wrap given, or no paired user bubble
found (first-turn edge / DOM mismatch) → the pre-phase-115
fallback: the LAST user record in `conversation` (iterate
backwards). The DOC_TITLE_MAX slice, the whitespace collapse, and
the defensive "Note" are unchanged."""
js = _text(APP_JS)
assert "const DOC_TITLE_MAX = 120;" in js
fn_idx = js.find("function defaultDocTitle() {")
assert fn_idx != -1, "defaultDocTitle missing"
fn_idx = js.find("function defaultDocTitle(wrap) {")
assert fn_idx != -1, "defaultDocTitle(wrap) missing"
fn_body = js[fn_idx : js.find("function docSlug", fn_idx)]
assert "conversation.length - 1" in fn_body, (
"the LAST user record wins (iterate backwards)"
# The paired-bubble walk: the wrap's previousElementSibling chain,
# the first .msg.user wins (nearest preceding user bubble) …
assert "wrap.previousElementSibling" in fn_body, (
"the pairing must walk the DOM conversation flow backwards"
)
assert 'el.classList.contains("msg")' in fn_body
assert 'el.classList.contains("user")' in fn_body
# …with the text read from that bubble's .bubble (not the wrap —
# the meta rows carry button labels).
assert 'el.querySelector(".bubble")' in fn_body
assert "textContent" in fn_body
# The fallback: the LAST user record in `conversation` (kept for
# the no-wrap / no-pair edges).
assert "conversation.length - 1" in fn_body
assert 'conversation[i].who === "user"' in fn_body
# The unchanged title rule: collapse, 120-cap, "Note".
assert 'question.replace(/\\s+/g, " ").trim().slice(0, DOC_TITLE_MAX)' in fn_body
assert '|| "Note"' in fn_body
def test_app_js_save_as_doc_passes_the_bubble_ancestor() -> None:
"""Phase 115 (task 03): the saveAsDoc call site passes the
bubble's ancestor — the .save-as-doc-btn's own .msg.brain wrap
(the button lives in the bubble's .msg-meta row, so closest climbs
button → meta → body → wrap) — the title pairs the answer with ITS
question. No call site may stay on the no-wrap fallback: the
button always knows its own bubble."""
js = _text(APP_JS)
fn_idx = js.find("async function saveAsDoc(btn) {")
assert fn_idx != -1, "saveAsDoc missing"
fn_body = js[fn_idx : fn_idx + 3000]
assert 'defaultDocTitle(btn.closest(".msg.brain"))' in fn_body
assert "defaultDocTitle()" not in js, (
"no caller may stay on the no-wrap fallback once the button "
"knows its own bubble"
)
def test_app_js_slug_rule() -> None:
"""The default in-repo path slug: lowercase → runs of
non-alphanumerics → "-" → trimmed → ≤60 chars → empty → "note"