phase: 115_doc_draft_discard
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:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user