phase: 98_sync_summary_visibility
All verification complete. Final report: **Phase 98 — Sync summary visibility: final verification pass** (all 5 tasks already complete; implementation verified against the design, no defects found, no code changes needed) - **Implementation checked:** `SyncStatus` phase machine (4 new keys, terminal-keep counts), `on_progress` hook in `generate_folder_summaries`, `summary_pending` on `KbTreeSource`/`KbTreeFolder` + D3 rule in `build_kb_tree`, phase-aware sync labels + pending UI in `sources.js`, `.kb-summary-pending` CSS — all match decisions D1–D5. - **Unit + integration:** `uv run pytest` → 2184 tests, 0 failed/errors (exit 0) - **Coverage:** `uv run pytest --cov=app --cov-report=term-missing` → **99%** on `app/` (criterion >90% ✓; `app/api/sync.py` and `app/rag/folder_summaries.py` at 100%) - **Lint/types:** `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors, 0 warnings - **Phase E2E (isolation):** `uv run pytest tests/e2e/test_sync_summary_visibility.py -v --no-cov` → **3 passed** (phase machine, live label, pending markers + gap-fill self-heal) - **Regression suites (each isolated, `--no-cov`):** test_kb_tree ✓, test_ls_tree_drilldown 3 ✓, test_sync_button 3 ✓, test_sync_upload_progress 4 ✓, test_oneshot_llm_retry 2 ✓, test_local_directory_sources 3 ✓ - **Completion criteria:** all 7 verified green — status phase fields + terminal semantics; `Writing KB overview…`/`Summarizing folders… (n/m)` labels (title + aria-live); pending set == `missing_folder_summaries` (integration cross-check pinned at `test_docs_api.py:428`); CLI/`ls` byte-identity (no changes to those paths, pins green); suite/coverage/lint gates; dedicated + regression E2E. Commit left to the harness per protocol (no `git add`/`commit` run). - **Decisions/deviations:** none — no fixes were required this pass. - **Next pending phase:** `99_kb_tree_table_and_back_nav`.
This commit is contained in:
@@ -34,6 +34,18 @@ settled result line is
|
||||
off the status's {"message": "uploaded"} detail; the nameless variant
|
||||
after a reload / on the 409 re-attach), and the success announce
|
||||
names the next step.
|
||||
|
||||
Phase 98 (task 02) pins the SYNC job's phase-aware label (D2):
|
||||
fmtSyncPhaseLabel — "overview" → exactly "Writing KB overview…",
|
||||
"summaries" → "Summarizing folders… <current_summary> (n/m)" (the
|
||||
folder part omitted while current_summary is null → the bare
|
||||
"Summarizing folders… (n/m)"), anything else → the byte-identical
|
||||
phase-64 fmtSyncLabel("sync", …) fall-through — plus the status-object
|
||||
threading (the tick's branch 1 + the initSyncButton running re-attach
|
||||
pass the WHOLE status; the upload job and the 202/409 click stay on
|
||||
the bare label — the upload status has no phase) and the A4 title-rule
|
||||
adjustment (the full untruncated label rides the button title for
|
||||
EVERY running label — the file-only rule is gone).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -182,30 +194,86 @@ def test_fmt_sync_label_signature_and_prefixes() -> None:
|
||||
# ---------- enterSyncRunningState: full path to title + announcer ----------
|
||||
|
||||
|
||||
def test_enter_running_state_writes_full_path_to_title_and_announcer() -> None:
|
||||
def test_fmt_sync_phase_label_is_the_exact_d2_contract() -> None:
|
||||
"""fmtSyncPhaseLabel(status) — the SYNC job's phase-aware label
|
||||
(phase 98 task 02, D2): "overview" → exactly "Writing KB
|
||||
overview…" (no folder, no counts); "summaries" →
|
||||
"Summarizing folders… <current_summary> (n/m)" — the folder part
|
||||
(one leading space + current_summary) is omitted while
|
||||
current_summary is null (the phase's first poll → the bare
|
||||
"Summarizing folders… (n/m)"); anything else (the null prelude,
|
||||
"import") → the phase-64 fmtSyncLabel fall-through, byte-identical
|
||||
(same builder, same fields, same order). The UPLOAD job never
|
||||
enters here — its status has no phase (D2)."""
|
||||
body = _fn(_js(), "fmtSyncPhaseLabel")
|
||||
assert "function fmtSyncPhaseLabel(status)" in body
|
||||
# "overview": the exact copy, no folder, no counts.
|
||||
i_ov = body.find('status.phase === "overview"')
|
||||
i_sum = body.find('status.phase === "summaries"')
|
||||
assert -1 < i_ov < i_sum
|
||||
ov = body[i_ov:i_sum]
|
||||
assert 'return "Writing KB overview…";' in ov
|
||||
assert "current_summary" not in ov and "summaries_done" not in ov, (
|
||||
"the overview label carries no folder and no counts"
|
||||
)
|
||||
# "summaries": the exact template — optional folder, counts always.
|
||||
i_fall = body.find("return fmtSyncLabel")
|
||||
assert i_sum < i_fall, "the summaries branch precedes the fall-through"
|
||||
sum_ = body[i_sum:i_fall]
|
||||
assert 'status.current_summary ? ` ${status.current_summary}` : ""' in sum_, (
|
||||
"the folder part — one leading space + the folder, or nothing"
|
||||
)
|
||||
assert (
|
||||
"return `Summarizing folders…${folder} "
|
||||
"(${status.summaries_done}/${status.summaries_total})`;"
|
||||
) in sum_, "the exact D2 copy (with and without the folder)"
|
||||
# anything else (null prelude, "import"): byte-identical to today's
|
||||
# sync label — same builder, same fields, same order.
|
||||
assert (
|
||||
'return fmtSyncLabel("sync", status.current_file, '
|
||||
"status.files_done, status.files_total);"
|
||||
) in body[i_fall:]
|
||||
|
||||
|
||||
def test_enter_running_state_is_phase_aware_and_announces_full_label() -> None:
|
||||
"""The running-state entry keeps the §7.4 never-stale mechanics
|
||||
(disabled, aria-busy, spinning icon, the stale .is-error removed)
|
||||
and — phase 64 — writes the FULL untruncated current file to the
|
||||
button title (removed when null: no file yet) and the full
|
||||
untruncated fmtSyncLabel text to BOTH the label span and
|
||||
#sync-result (the aria-live announcer reads the full live path;
|
||||
CSS only ellipsizes the button's span)."""
|
||||
and — phase 98 (task 02, D2) — is phase-aware for the SYNC job:
|
||||
a sync-kind call carrying the status object takes the
|
||||
fmtSyncPhaseLabel form, while the upload job (no status — its
|
||||
endpoint has no phase) and the 202/409 click keep the bare
|
||||
fmtSyncLabel label, byte-identical. The FULL untruncated label —
|
||||
whatever its form — rides the button title (set for EVERY running
|
||||
label: the phase-64 "title only when there is a current file"
|
||||
rule is adjusted away) and #sync-result (the aria-live announcer);
|
||||
CSS ellipsizes only the label span."""
|
||||
body = _fn(_js(), "enterSyncRunningState")
|
||||
assert (
|
||||
"function enterSyncRunningState(kind, currentFile, done, total, status)"
|
||||
in body
|
||||
)
|
||||
assert "syncBtn.disabled = true" in body
|
||||
assert 'syncBtn.setAttribute("aria-busy", "true")' in body
|
||||
assert "syncIcon.classList.add(\"is-spinning\")" in body
|
||||
assert "syncBtn.classList.remove(\"is-error\")" in body
|
||||
assert "syncBtn.title = currentFile" in body, "the full path on hover"
|
||||
assert 'syncBtn.removeAttribute("title")' in body, "removed when no file yet"
|
||||
i_set = body.find("if (currentFile) syncBtn.title = currentFile")
|
||||
i_remove = body.find('syncBtn.removeAttribute("title")')
|
||||
assert -1 < i_set < i_remove, "title is set (not removed) only when a file exists"
|
||||
assert "fmtSyncLabel(kind, currentFile, done, total)" in body
|
||||
i_label = body.find("fmtSyncLabel(kind, currentFile, done, total)")
|
||||
# the phase-aware split: sync + status → the D2 builder; the
|
||||
# fall-through (upload / the click, no status) is the byte-
|
||||
# identical phase-64 builder.
|
||||
i_phase = body.find("fmtSyncPhaseLabel(status)")
|
||||
i_fall = body.find("fmtSyncLabel(kind, currentFile, done, total)")
|
||||
assert 'kind === "sync" && status' in body
|
||||
assert -1 < i_phase < i_fall, "the phase label precedes the fall-through"
|
||||
# A4: the untruncated label rides the title for EVERY running label
|
||||
# (the remove-when-no-file rule is gone) and #sync-result.
|
||||
assert "syncBtn.title = label" in body, "the full label on hover, always"
|
||||
assert 'syncBtn.removeAttribute("title")' not in body, (
|
||||
"the title is set for every running label (D2: not just file labels)"
|
||||
)
|
||||
i_title = body.find("syncBtn.title = label")
|
||||
i_span = body.find("syncLabel.textContent = label")
|
||||
i_result = body.find("syncResult.textContent = label")
|
||||
assert -1 < i_label < i_span < i_result, (
|
||||
"one label: built once, written to the span AND the announcer"
|
||||
assert -1 < i_title < i_span < i_result, (
|
||||
"one label: built once, written to title, span AND the announcer"
|
||||
)
|
||||
|
||||
|
||||
@@ -327,14 +395,16 @@ def test_click_branch_enters_sync_running_without_a_file() -> None:
|
||||
|
||||
|
||||
def test_reattach_adopts_a_running_upload_only() -> None:
|
||||
"""initSyncButton: the sync branches are unchanged (running
|
||||
re-enters with the live file; the terminals render the last
|
||||
result). With the sync IDLE it fetches the upload status: a RUNNING
|
||||
upload run re-attaches (running state, upload kind — phase 90: no
|
||||
live file, the label stays bare "Importing…" — the synthetic
|
||||
running frame, the poll starts); a terminal upload is a no-op —
|
||||
the fall-through is the plain idle settle (the boot-time loadTree()
|
||||
already shows the current catalog)."""
|
||||
"""initSyncButton: the sync terminals render the last result; the
|
||||
RUNNING sync re-enters with the live file AND the whole status
|
||||
(phase 98 task 02: the label is phase-aware — a mid-summaries
|
||||
reload re-enters with the summaries label). With the sync IDLE it
|
||||
fetches the upload status: a RUNNING upload run re-attaches
|
||||
(running state, upload kind — phase 90: no live file, the label
|
||||
stays bare "Importing…" — the synthetic running frame, the poll
|
||||
starts); a terminal upload is a no-op — the fall-through is the
|
||||
plain idle settle (the boot-time loadTree() already shows the
|
||||
current catalog)."""
|
||||
body = _fn(_js(), "initSyncButton")
|
||||
assert "await fetchIsAdmin()" in body, "admin-only (no extra fetch)"
|
||||
assert 'fetch("/api/git-sources/upload/status")' in body
|
||||
@@ -356,6 +426,61 @@ def test_reattach_adopts_a_running_upload_only() -> None:
|
||||
assert body.rstrip().removesuffix("}").rstrip().endswith("applySyncIdle(status);")
|
||||
|
||||
|
||||
def test_tick_sync_running_passes_the_status_for_the_phase_label() -> None:
|
||||
"""Branch 1 of the two-job tick (phase 98 task 02): the sync
|
||||
running state passes the WHOLE syncStatus object as the fifth
|
||||
argument — the phase-aware label (overview / summaries) reads its
|
||||
phase fields. The upload running branch stays on the BARE four-arg
|
||||
label (its status has no phase — D2): no fifth argument, no status
|
||||
object anywhere in the branch."""
|
||||
tick = _tick(_js())
|
||||
b_sync_run = tick.find('syncStatus.state === "running"')
|
||||
b_up_run = tick.find('uploadStatus && uploadStatus.state === "running"')
|
||||
b_sync_ok = tick.find('syncStatus.state === "success"')
|
||||
assert -1 < b_sync_run < b_up_run < b_sync_ok
|
||||
branch1 = tick[b_sync_run:b_up_run]
|
||||
i_call = branch1.find("enterSyncRunningState(")
|
||||
call = branch1[i_call:branch1.find(");", i_call)]
|
||||
assert re.search(
|
||||
r'"sync",\s*syncStatus\.current_file,\s*syncStatus\.files_done,\s*'
|
||||
r"syncStatus\.files_total,\s*syncStatus\s*$",
|
||||
call,
|
||||
), "branch 1 passes the whole status (fifth arg) for the phase-aware label"
|
||||
branch2 = tick[b_up_run:b_sync_ok]
|
||||
i_call2 = branch2.find("enterSyncRunningState(")
|
||||
call2 = branch2[i_call2:branch2.find(");", i_call2)]
|
||||
assert re.fullmatch(
|
||||
r'enterSyncRunningState\(\s*"upload",\s*uploadStatus\.current_file,\s*'
|
||||
r"uploadStatus\.files_done,\s*uploadStatus\.files_total\s*$",
|
||||
call2,
|
||||
), "the upload job keeps the bare four-arg label — no status, no phase (D2)"
|
||||
assert "phase" not in call2 and "summaries" not in call2
|
||||
|
||||
|
||||
def test_reattach_sync_running_passes_the_status_for_the_phase_label() -> None:
|
||||
"""initSyncButton (phase 98 task 02): the RUNNING sync re-attach
|
||||
passes the WHOLE status object — a reload mid-summaries re-enters
|
||||
the running state with the summaries label (the never-stale
|
||||
contract); the RUNNING upload re-attach stays on the bare four-arg
|
||||
label (its status has no phase — D2)."""
|
||||
body = _fn(_js(), "initSyncButton")
|
||||
i_sync = body.find('"sync", status.current_file')
|
||||
assert i_sync != -1
|
||||
call = body[i_sync:body.find(");", i_sync)]
|
||||
assert re.search(
|
||||
r'"sync",\s*status\.current_file,\s*status\.files_done,\s*'
|
||||
r"status\.files_total,\s*status\s*$",
|
||||
call,
|
||||
), "the RUNNING sync re-attach passes the status object (fifth arg)"
|
||||
i_up = body.find('"upload", upload.current_file')
|
||||
assert i_up != -1
|
||||
call2 = body[i_up:body.find(");", i_up)]
|
||||
assert re.fullmatch(
|
||||
r'"upload",\s*upload\.current_file,\s*upload\.files_done,\s*upload\.files_total\s*$',
|
||||
call2,
|
||||
), "the RUNNING upload re-attach keeps the bare four-arg label (D2)"
|
||||
|
||||
|
||||
# ---------- the section header + the page comment ----------
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user