phase: 134_sync_cancel
**Phase 134 final verification pass — all green** (all 3 task files already in `complete/`; no implementation was missing) - Verified: 5-state `SyncStatus` + `cancelled` terminal (error None, final counts kept), `POST /api/sync/cancel` (202/409/403), cooperative `_check_cancelled` boundaries, shutdown `CancelledError` contract preserved, `#sync-cancel-btn` show/hide + neutral "Sync cancelled" settle, 8-branch poll tree, re-attach — code, unit, integration, and E2E layers all present - `uv run pytest --cov=app --cov-report=term-missing` → **3150 passed**, `app/` coverage **99%** (gate: >90%) - `uv run pytest tests/e2e/test_sync_cancel.py -v --no-cov` (isolated) → **3 passed** (47.6 s) - `uv run ruff check .` → clean; `uv run pyright` → 0 errors, 0 warnings **Completion criteria:** ① cancel endpoint 202/409/403 ✅ (unit + E2E) ② `cancelled` terminal reported by status, error null, counts kept ✅ (integration `test_cancel_mid_run_lands_in_cancelled_then_resyncs_clean`) ③ unrequested-cancel pin ✅ (`test_unrequested_cancel_is_never_swallowed_and_never_converts`) ④ UI lifecycle + clean re-sync (sentinel row, full 40-file import) ✅ (E2E) ⑤ suite/coverage/E2E gates ✅ ⑥ uncancelled runs byte-identical ✅ (all 3150 pre-existing tests green untouched) - Docs: README.md reviewed in full against code (pages, auth endpoints, every config default vs `app/config.py`, palette 9+8, fail-loud, cancel section) — consistent, no changes needed. AGENTS.md: fixed one pre-existing inaccuracy (rule 2 "move its file" → "move its directory" — phases are directories); everything else verified accurate - No live infrastructure touched; no deviations **Next pending phase:** `135_git_source_branch`
This commit is contained in:
@@ -82,6 +82,16 @@ contract as the count part, and the UNTRUNCATED full label — main +
|
||||
--mono, the rule right after .sync-label-count). A node harness (the
|
||||
test-document-viewer pattern) behaviorally pins the rendered parts +
|
||||
the full title across every label state.
|
||||
|
||||
Phase 134 (task 02) extends the node harness to the Cancel control:
|
||||
the extracted functions gain the REAL show/hide helpers + the REAL
|
||||
settleSyncButton (step 8 now runs the settle itself — it goes through
|
||||
the writer as before, plus it hides the Cancel control), and every
|
||||
row snapshots the control's hidden/disabled pair — the behavioral
|
||||
contract: shown + enabled in EVERY sync-running state (the entry
|
||||
shows it for kind "sync"), hidden + disabled for the upload run
|
||||
(the upload job is NOT cancellable in this phase) and after the
|
||||
settle (settleSyncButton always hides it).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -514,6 +524,16 @@ const syncBtn = {
|
||||
classList: { add() {}, remove() {} },
|
||||
};
|
||||
const syncIcon = { classList: { add() {}, remove() {} } };
|
||||
// Phase 134 (task 02): the Cancel control — ships hidden + disabled
|
||||
// (the markup's state); the extracted REAL show/hide helpers mutate
|
||||
// it (show also sets the aria-label — the attrs pocket records it).
|
||||
const syncCancelBtn = {
|
||||
hidden: true,
|
||||
disabled: true,
|
||||
attrs: {},
|
||||
setAttribute(k, v) { this.attrs[k] = v; },
|
||||
removeAttribute(k) { delete this.attrs[k]; },
|
||||
};
|
||||
"""
|
||||
|
||||
_CHUNK_NODE_TAIL = """
|
||||
@@ -523,6 +543,7 @@ const show = (tag) => {
|
||||
syncLabelMain.textContent,
|
||||
syncLabelCount.textContent, syncLabelCount.hidden,
|
||||
syncLabelChunk.textContent, syncLabelChunk.hidden,
|
||||
syncCancelBtn.hidden, syncCancelBtn.disabled,
|
||||
]));
|
||||
};
|
||||
// 1. import, batch in flight (the task's fixture values, 14/38):
|
||||
@@ -561,8 +582,10 @@ enterSyncRunningState("sync", null, 4, 4, {
|
||||
summaries_done: 2, summaries_total: 3, current_chunk: 0, chunks_total: 0,
|
||||
});
|
||||
show("summaries");
|
||||
// 8. the idle settle goes through the writer with no chunk text:
|
||||
setSyncLabel("Sync sources");
|
||||
// 8. the idle settle goes through the REAL settle (phase 134 task
|
||||
// 02: it hides the Cancel control too) — the settle itself goes
|
||||
// through the writer with no chunk text:
|
||||
settleSyncButton("Sync sources");
|
||||
show("idle");
|
||||
"""
|
||||
|
||||
@@ -590,16 +613,34 @@ test-document-viewer pattern). The pinned "· chunk n/m" part (the
|
||||
status), the upload run (no status, D2), overview, summaries (its
|
||||
own (n/m) is its position), the idle settle — leaves the chunk
|
||||
part hidden + empty.
|
||||
|
||||
Phase 134 (task 02): every row also snapshots the Cancel
|
||||
control's hidden/disabled pair — the behavioral contract:
|
||||
shown + enabled in EVERY sync-running state, hidden + disabled
|
||||
for the upload run (the upload job is NOT cancellable in this
|
||||
phase) and after the (real) settle.
|
||||
"""
|
||||
js = _js()
|
||||
fns = "\n".join(
|
||||
_fn(js, name)
|
||||
for name in ("fmtSyncLabel", "fmtSyncPhaseLabel", "setSyncLabel", "enterSyncRunningState")
|
||||
for name in (
|
||||
"fmtSyncLabel",
|
||||
"fmtSyncPhaseLabel",
|
||||
"setSyncLabel",
|
||||
# Phase 134 (task 02): the running entry's REAL cancel
|
||||
# show/hide + the REAL settle (step 8) — extracted against
|
||||
# the syncCancelBtn stub.
|
||||
"showSyncCancelBtn",
|
||||
"hideSyncCancelBtn",
|
||||
"enterSyncRunningState",
|
||||
"settleSyncButton",
|
||||
)
|
||||
)
|
||||
script = _CHUNK_NODE_HEAD + fns + _CHUNK_NODE_TAIL
|
||||
rows = [json.loads(line) for line in _run_node(script).splitlines() if line]
|
||||
by_tag = {row[0]: row for row in rows}
|
||||
# row = [tag, title, announcer, main, count, countHidden, chunk, chunkHidden]
|
||||
# row = [tag, title, announcer, main, count, countHidden, chunk,
|
||||
# chunkHidden, cancelHidden, cancelDisabled]
|
||||
|
||||
# 1. the in-flight import — the chunk part renders, the full
|
||||
# untruncated label rides the title + the announcer (A4).
|
||||
@@ -634,6 +675,24 @@ test-document-viewer pattern). The pinned "· chunk n/m" part (the
|
||||
assert row[2] == row[1], (tag, row)
|
||||
assert row[6] == "" and row[7] is True, f"{tag} must hide the chunk part"
|
||||
|
||||
# Phase 134 (task 02): the Cancel control's behavioral contract —
|
||||
# shown + enabled in EVERY sync-running state (the entry shows it
|
||||
# for kind "sync"), hidden + disabled for the upload run (the
|
||||
# upload job is NOT cancellable in this phase) and hidden +
|
||||
# disabled after the real settle (settleSyncButton always hides
|
||||
# it — every terminal + idle passes through it).
|
||||
for tag in ("import-inflight", "import-prebatch", "prelude", "click", "overview", "summaries"):
|
||||
row = by_tag[tag]
|
||||
assert row[8] is False and row[9] is False, (
|
||||
f"{tag}: the Cancel control must be shown + enabled while a sync run is in flight"
|
||||
)
|
||||
for tag in ("upload", "idle"):
|
||||
row = by_tag[tag]
|
||||
assert row[8] is True and row[9] is True, (
|
||||
f"{tag}: the Cancel control must be hidden + disabled "
|
||||
"(the upload job is not cancellable / the settle hides it)"
|
||||
)
|
||||
|
||||
|
||||
def test_every_other_state_passes_no_chunk_text() -> None:
|
||||
"""Source-level pin of the render-only-while-chunks_total>0
|
||||
|
||||
Reference in New Issue
Block a user