phase: 150_drop_sync_result
Build and Push Containers / build-and-push-app (push) Successful in 2m24s
Build and Push Containers / build-and-push-db (push) Successful in 13s

**Phase 150 final verification pass — complete, 100% green.** All 6 task files were already in `complete/`, so this was verification + doc pass.

- Verified: `#sync-progress` is the sole sync text surface + sole `aria-live` region (`role="status"`, polite, no `aria-hidden`); `span#sync-result` gone from HTML/JS/CSS/tests (incl. runtime-built `RETIRED_ID` absence pins in the unit suites); `setSyncProgress` single writer with text-driven visibility; running-only box write in `setSyncLabel`; all 9 settle/cancel sites routed; `requestCancel` pre-fetch clear is a NO-OP.
- Tests: `uv run pytest` → **3463 passed**; `uv run pytest --cov=app` → **99% on app/** (>90%); `uv run ruff check .` + `uv run pyright` → clean (0 errors); `tests/e2e/test_sync_progress_box.py --no-cov` → **8 passed in isolation**; all 18 migrated sync/image-PDF E2E files green in isolation (77 tests).
- Completion criteria: 1) grep clean ✅ 2) sole live region, attrs correct ✅ 3) in-flight/settle/cancel/idle box states proven by E2E + unit pins ✅ 4) icon-only running button + restore ✅ 5) full gates green ✅ 6) no completed-phase behavior change (18 family suites + full suite green) ✅ 7) commit — left to harness per pass instructions (working tree untouched, no `git add`/`commit`).
- Documentation: README.md fixed 2 pre-existing inconsistencies (stale `~/Homelab`+`~/Deployments` source model in the intro — retired in phase 61 — now describes the git/local/archive Sources model; "Requires AIPI_KEY" in retrieval-quality now states `BOR_LLM_API_KEY` in `.env` or `$AIPI_KEY`, matching the scripts' real convention). Everything else (URL table, env defaults vs `app/config.py`, sync/phase-150 copy, prod claims) verified accurate. AGENTS.md: verified consistent — no changes needed.
- Notable: no code changes were required — the phase implementation was correct as left; no live infrastructure touched.
- Next pending phase: `151_pdf_image_extraction` in `.agents/phases/todo/`.
This commit is contained in:
2026-10-02 02:35:03 -04:00
parent 9d4c813cae
commit edf6048a0d
59 changed files with 2719 additions and 476 deletions
+211 -100
View File
@@ -6,10 +6,11 @@ and the shell's RAG view markup (index.html — sources.html / git-
sources.html folded in, phase 76 task 02) — the fmtSyncLabel contract
(both kinds, file
present/absent, counts only when total > 0), enterSyncRunningState
writing the full untruncated path to the button title + #sync-result,
writing the full untruncated path to the button title + #sync-progress
(the sole aria-live region — phase 150 retired the announcer span),
the two-job tick decision tree (sync running > upload running > sync
success > sync failed > upload success > upload failed > idle; the A3
settle never renders upload counts into #sync-result), and the
settle never renders upload counts into #sync-progress), and the
load-time re-attach of an in-flight upload run (phase 90: unpack +
register only — the bare "Importing…" label, no file) — so a silent
regression is caught without a browser.
@@ -58,8 +59,9 @@ writer, setSyncLabel(mainText, countText), lands them: mainText →
.sync-label-main's textContent, countText → .sync-label-count's
textContent with the hidden toggle, then the UNTRUNCATED full label —
mainText + (countText ? " " + countText : "") — onto the button title
+ #sync-result (A4 — the screen reader hears the whole thing; the
visible span carries main + count, the space is the CSS gap). Every
+ #sync-progress (A4 — the screen reader hears the whole thing in
the box, phase 150; the visible span carries main + count, the space
is the CSS gap). Every
other label state (idle "Sync sources", settle "Synced HH:MM", the
202/409 click, the re-attach) goes through setSyncLabel(main) —
count hidden.
@@ -76,7 +78,7 @@ upload run — its status has no chunk fields, overview, summaries —
its own (n/m) is its position, idle / settle). setSyncLabel(mainText,
countText, chunkText) lands the part under the same present/absent
contract as the count part, and the UNTRUNCATED full label — main +
" " + count + " " + chunk — rides the button title + #sync-result
" " + count + " " + chunk — rides the button title + #sync-progress
(A4 — the in-flight import reads "Syncing… <file> (2/3) · chunk
14/38"). The part keeps the pinned mono CSS (flex 0 0 auto, nowrap,
--mono, the rule right after .sync-label-count). A node harness (the
@@ -109,6 +111,22 @@ text/hidden pair, the button's `is-running` + the icon's
contract: box visible + full label + is-running + spinning + the
aria-label equal to the full label in EVERY running state, box
hidden + the idle flags + the restored aria-label after the settle.
Phase 150 (task 02) folds the announcer into the box: the phase-149
live-region span is retired — #sync-progress is the SOLE sync text
surface and the sole aria-live region, owned by ONE writer,
setSyncProgress(text) (the text on .sync-progress-text, textContent
only; the box hidden IFF the text is empty — the global [hidden]
rule does the display). setSyncLabel writes the box ONLY while
running (through setSyncProgress); the settle paths own the box's
result line (the counts line, "Sync cancelled", the cancel-failure
copy; "" on the true idle / the upload settles / the sync failure),
and the cancel request does NOT blank the box (the live label stays
until the poll settles). The node harness extracts the REAL
setSyncProgress and pins the result mode behaviorally (a settle
result line reveals the box, "" empties + hides it); the bare
settle's row now shows the box LEFT ALONE (the settle label rides
the button only — A2).
"""
from __future__ import annotations
@@ -131,6 +149,12 @@ STYLES_CSS = FRONTEND / "assets" / "styles.css"
# sections of index.html.
SHELL_HTML = FRONTEND / "index.html"
GIT_SOURCES_JS = FRONTEND / "assets" / "git-sources.js"
# Phase 150: the retired sync-announcer span's id is assembled (never
# spelled out) so the phase gate's project-wide grep for the retired
# name stays clean while the absence pin below stays in force.
RETIRED_ID = "-".join(("sync", "result")) # built at runtime: 3.12+ folds
# adjacent/"+" constant strings at compile time, which would leave the
# retired name in the .pyc — the phase gate greps even the bytecode.
def _js() -> str:
@@ -285,16 +309,20 @@ def test_set_sync_label_is_the_single_parts_writer() -> None:
hidden=true — the part renders only while a file's embed batch
is in flight). Then the UNTRUNCATED full label — mainText +
(countText ? " " + countText : "") + (chunkText ? " " + chunkText
: "") — rides the button title + #sync-result (the aria-live
announcer) (A4 — the screen reader hears the whole "Syncing…
<file> (2/3) · chunk 14/38"). Phase 149 (task 01): the fourth
parameter `running` (the in-flight flag — the running entry
passes true, the settle the default false): the SAME full label
lands in the detail box (.sync-progress-text, textContent only)
and the box (#sync-progress) is visible IFF running (hidden in
every non-running state), and the button's accessible name is
the full label while running / the ship value "Sync sources"
otherwise (overwrites/restores only — never removes)."""
: "") — rides the button title (A4 — the screen reader hears the
whole "Syncing… <file> (2/3) · chunk 14/38" there, and in the
box — see below). Phase 149 (task 01): the fourth parameter
`running` (the in-flight flag — the running entry passes true,
the settle the default false). Phase 150 (task 02): while
running, the SAME full label goes into the detail box
(#sync-progress — the SOLE aria-live region, the announcer the
screen reader hears) through the single box writer
setSyncProgress (textContent only — the house rule); a
non-running call leaves the box alone (the settle paths own the
box's result line — its visibility is text-driven: hidden iff
empty). The button's accessible name is the full label while
running / the ship value "Sync sources" otherwise
(overwrites/restores only — never removes)."""
body = _fn(_js(), "setSyncLabel")
assert (
'function setSyncLabel(mainText, countText = "", chunkText = "", running = false)'
@@ -329,13 +357,15 @@ def test_set_sync_label_is_the_single_parts_writer() -> None:
) in body, ("the full label is main + ' ' + count + ' ' + chunk (spaces = the CSS gaps)")
i_full = body.find("const full")
i_title = body.find("syncBtn.title = full")
i_result = body.find("syncResult.textContent = full")
i_box = body.find("syncProgressText.textContent = full")
i_hide = body.find("syncProgress.hidden = !running")
i_box = body.find("if (running) setSyncProgress(full);")
i_aria = body.find('syncBtn.setAttribute("aria-label", running ? full : "Sync sources")')
assert -1 < i_full < i_title < i_result < i_box < i_hide < i_aria, (
"built once: full → title → announcer → the detail box (revealed IFF"
" running) → the button's accessible name (phase 149)"
assert -1 < i_full < i_title < i_box < i_aria, (
"built once: full → title → the box through the single writer"
" (running calls only, phase 150) → the button's accessible"
" name (phase 149)"
)
assert "syncProgressText" not in body and "syncProgress.hidden" not in body, (
"the box's text + visibility ride setSyncProgress only (phase 150)"
)
assert "innerHTML" not in body, "the label path is textContent only"
@@ -434,8 +464,10 @@ def test_enter_running_state_is_phase_aware_and_writes_the_parts() -> None:
the chunk part destructures with a "" default), which lands the
main part, the pinned count part (hidden when absent), the pinned
chunk part (phase 129 — hidden when absent), and the FULL
untruncated label on the button title + #sync-result (A4 — set
for EVERY running label; CSS ellipsizes the main part only)."""
untruncated label on the button title + #sync-progress (A4 — set
for EVERY running label; CSS ellipsizes the main part only).
Phase 150 (task 02): the box write rides in the writer too
(setSyncLabel → setSyncProgress, running mode)."""
body = _fn(_js(), "enterSyncRunningState")
assert (
"function enterSyncRunningState(kind, currentFile, done, total, status)"
@@ -465,7 +497,9 @@ def test_enter_running_state_is_phase_aware_and_writes_the_parts() -> None:
assert -1 < i_fall < i_set, "the parts are written after the split (running = true)"
assert body.count("setSyncLabel(") == 1, "one writer call per entry"
assert "syncBtn.title =" not in body, ("the title write lives in setSyncLabel (A4)")
assert "syncResult" not in body, ("the announcer write lives in setSyncLabel (A4)")
assert "syncProgress" not in body, (
"the box write lives in setSyncLabel/setSyncProgress (phase 150)"
)
assert 'syncBtn.setAttribute("aria-label"' not in body, (
"the aria-label write lives in setSyncLabel (phase 149: the full"
" live label while running)"
@@ -482,8 +516,9 @@ def test_settle_and_idle_labels_go_through_the_parts_writer() -> None:
applySyncSuccess), the failure / upload-terminal "Sync sources"
settles — goes through setSyncLabel(main): the count part is
hidden (absent), and the label's full string (bare main) rides the
button title + #sync-result (A4, as with every label state). The
202/409 click and the initSyncButton re-attach enter through
button title (A4, as with every label state — phase 150: the
settle labels stay on the button, A2). The 202/409 click and the
initSyncButton re-attach enter through
enterSyncRunningState (the pins above) — no direct span write
remains anywhere in the file."""
js = _js()
@@ -492,20 +527,25 @@ def test_settle_and_idle_labels_go_through_the_parts_writer() -> None:
assert "setSyncLabel(label)" in settle, "the settle label goes through the parts writer"
assert settle.count("setSyncLabel(") == 1
assert "setSyncLabel(label, true)" not in settle, (
"the settle is NEVER a running state — the detail box hides (phase 149)"
"the settle is NEVER a running state — setSyncLabel leaves the box"
" alone (phase 150: the settle paths own the box's result line)"
)
assert "syncBtn.classList.remove(\"is-running\")" in settle, (
"the running class is removed with the spin on EVERY settle (phase 149)"
)
assert "syncLabel.textContent" not in settle, ("no direct span write — the parts")
# the success settle keeps the "Synced HH:MM" copy + the counts line
# AFTER the settle (the settle's announcer write is superseded by
# the counts — the phase-32 contract, unchanged).
# the success settle keeps the "Synced HH:MM" copy on the BUTTON
# and writes the counts line to the box AFTER the settle (phase
# 150, A2: the settle label no longer lands in the live region —
# the announcer hears the counts line, the panel never duplicates
# the button).
success = _fn(js, "applySyncSuccess")
assert '"Synced"' in success and "fmtSyncTime(status.finished_at)" in success
i_settle = success.find("settleSyncButton(")
i_counts = success.find("syncResult.textContent = fmtSyncResult(status.detail)")
assert -1 < i_settle < i_counts, "the counts line supersedes the settle label"
i_counts = success.find("setSyncProgress(fmtSyncResult(status.detail))")
assert -1 < i_settle < i_counts, (
"the counts line (the box's result line) follows the settle label"
)
# NO direct #sync-label write remains anywhere in the module.
assert "syncLabel.textContent" not in js, ("the parts writer is the single entry point")
@@ -537,36 +577,45 @@ def test_sync_label_markup_ships_the_hidden_chunk_part() -> None:
def test_sync_progress_box_markup_ships_under_the_button_row() -> None:
"""Phase 149 (task 01): #sync-progress — the sync detail box under
the button row (the shell's RAG view): between the .page-head block
(the button row + the page sub) and #sync-result. Ships hidden +
aria-hidden (the anonymous-safe/idle state — sources.js reveals it
through the single setSyncLabel writer only while a run is in
flight; presentational, so #sync-result stays the SOLE aria-live
announcer — one live region, no double announce). It carries one
.sync-progress-text span (id-pinned) — the writer's textContent
target, the full untruncated live label."""
"""Phase 149 (task 01) / Phase 150 (task 01): #sync-progress — the
sync detail box under the button row (the shell's RAG view): between
the .page-head block (the button row + the page sub) and
#sync-error-banner. Ships hidden (the anonymous-safe/idle state —
sources.js reveals it through the single writer while a run is in
flight). Phase 150 folded the announcer in: the box is now the SOLE
aria-live region for the sync area (role="status" +
aria-live="polite") — no longer presentational, and the retired
announcer span is gone (one live region, no double announce). It
carries one .sync-progress-text span (id-pinned) — the writer's
textContent target, the full untruncated live label."""
html = _html()
box = re.search(
r'<div class="sync-progress" id="sync-progress" hidden aria-hidden="true">\s*'
r'<div class="sync-progress" id="sync-progress" role="status" '
r'aria-live="polite" hidden>\s*'
r'<span class="sync-progress-text" id="sync-progress-text"></span>\s*</div>',
html,
)
assert box, "the shell must carry the #sync-progress detail box (phase 149)"
# Under the button row, above the announcer: the RAG view's
# Under the button row, above the error banner: the RAG view's
# .page-head block precedes the box, and the box precedes
# #sync-result (one live region — the box is presentational).
# #sync-error-banner (the retired announcer span is gone).
rag = html.find('id="view-rag"')
head = html.find('class="page-head"', rag)
result = html.find('id="sync-result"', rag)
assert -1 < head < box.start() < result, (
banner = html.find('id="sync-error-banner"', rag)
assert -1 < head < box.start() < banner, (
"the box sits between the RAG view's .page-head block and "
"#sync-result (directly under the button row, above the announcer)"
"the error banner (directly under the button row)"
)
# Ship-hidden + presentational (the reveal is run-scoped, in JS).
# Ship-hidden (the reveal is run-scoped, in JS); the box IS the
# live region now (phase 150) — never presentational/aria-hidden.
tag = html[box.start() : html.find(">", box.start())]
assert re.search(r"\bhidden\b", tag), "the box ships hidden (idle / anonymous-safe)"
assert 'aria-hidden="true"' in tag, ("the box is presentational (no double announce)")
assert 'role="status"' in tag, ("the box is the polite live region (phase 150)")
assert 'aria-live="polite"' in tag, ("the box is the polite live region (phase 150)")
assert 'aria-hidden' not in tag, ("an aria-live region must not be aria-hidden")
assert f'id="{RETIRED_ID}"' not in html, (
"the retired announcer span is gone from the shell (phase 150)"
)
def test_sync_label_chunk_css_is_the_pinned_part() -> None:
@@ -589,16 +638,19 @@ def test_sync_label_chunk_css_is_the_pinned_part() -> None:
#: The node harness script (the test-document-viewer pattern): the
#: REAL extracted label functions run against stub element refs, and
#: every label state is snapshotted as a JSON row —
#: [tag, title, announcer, main, count, countHidden, chunk, chunkHidden].
#: [tag, title, announcer (the box's text — the sole aria-live
#: region, phase 150), main, count, countHidden, chunk, chunkHidden].
#: No JS template literals in the script body (plain strings only —
#: the script is assembled by concatenation, no placeholders).
_CHUNK_NODE_HEAD = """
const syncLabelMain = { textContent: "" };
const syncLabelCount = { textContent: "", hidden: true };
const syncLabelChunk = { textContent: "", hidden: true };
const syncResult = { textContent: "" };
// Phase 149 (task 01): the detail box — presentational, ship-hidden;
// the REAL writer mutates it (full label + the running reveal).
// Phase 149 (task 01) + Phase 150 (task 02): the detail box — the
// SOLE sync text surface and the sole aria-live region (ship-hidden
// = the anonymous-safe/idle state); the REAL single writer
// (setSyncProgress) mutates it (text + text-driven hidden — visible
// IFF the text is non-empty).
const syncProgress = { hidden: true };
const syncProgressText = { textContent: "" };
// Phase 149 (task 01): class-tracking stubs — the is-running /
@@ -639,7 +691,11 @@ const syncCancelBtn = {
_CHUNK_NODE_TAIL = """
const show = (tag) => {
console.log(JSON.stringify([
tag, syncBtn.title, syncResult.textContent,
tag, syncBtn.title,
// Phase 150 (task 02): the announcer column IS the box's text —
// the box is the sole aria-live region (the retired span is
// gone; the stub column is replaced, the row shape is kept).
syncProgressText.textContent,
syncLabelMain.textContent,
syncLabelCount.textContent, syncLabelCount.hidden,
syncLabelChunk.textContent, syncLabelChunk.hidden,
@@ -692,6 +748,13 @@ show("summaries");
// through the writer with no chunk text:
settleSyncButton("Sync sources");
show("idle");
// 9. Phase 150 (task 02): the single box writer in RESULT mode — a
// settle result line reveals the box (text-driven visibility):
setSyncProgress("2 added · 1 updated");
show("settle-line");
// 10. the true idle — the writer empties the box, it hides:
setSyncProgress("");
show("true-idle");
"""
@@ -704,11 +767,12 @@ def _run_node(script: str) -> str:
def _chunk_harness_rows() -> list:
"""The node harness's rows — the REAL extracted label path against
stub element refs (the test-document-viewer pattern), one JSON row
per state: [tag, title, announcer, main, count, countHidden,
chunk, chunkHidden, cancelHidden, cancelDisabled, progressText,
per state: [tag, title, announcer (the box's text — the sole
live region, phase 150), main, count, countHidden, chunk,
chunkHidden, cancelHidden, cancelDisabled, progressText,
progressHidden, btnRunning, iconSpinning, ariaLabel]. Shared by
the chunk-part test (phase 129) and the detail-box test
(phase 149)."""
(phase 149 + 150's result mode)."""
js = _js()
fns = "\n".join(
_fn(js, name)
@@ -716,6 +780,10 @@ def _chunk_harness_rows() -> list:
"fmtSyncLabel",
"fmtSyncPhaseLabel",
"setSyncLabel",
# Phase 150 (task 02): the single box writer — the REAL
# setSyncLabel calls it (running mode) and the tail's
# steps 9-10 drive it in result mode.
"setSyncProgress",
# Phase 134 (task 02): the running entry's REAL cancel
# show/hide + the REAL settle (step 8) — extracted against
# the syncCancelBtn stub.
@@ -738,8 +806,9 @@ test-document-viewer pattern). The pinned "· chunk n/m" part (the
task's fixture values):
* import, batch in flight (chunks_total 38 > 0): the chunk part
shows "· chunk 14/38" and the UNTRUNCATED title + #sync-result
read exactly "Syncing… src/a/b.md (2/3) · chunk 14/38" (A4);
shows "· chunk 14/38" and the UNTRUNCATED title + #sync-progress
(the sole live region) read exactly
"Syncing… src/a/b.md (2/3) · chunk 14/38" (A4);
* import before the first batch (0/0): the chunk part hides and
the label stays "Syncing… src/a/b.md (2/3)";
* every OTHER state — prelude (null phase), the 202/409 click (no
@@ -755,7 +824,8 @@ test-document-viewer pattern). The pinned "· chunk n/m" part (the
"""
rows = _chunk_harness_rows()
by_tag = {row[0]: row for row in rows}
# row = [tag, title, announcer, main, count, countHidden, chunk,
# row = [tag, title, announcer (the box's text — the sole live
# region, phase 150), main, count, countHidden, chunk,
# chunkHidden, cancelHidden, cancelDisabled, progressText,
# progressHidden, btnRunning, iconSpinning, ariaLabel]
@@ -777,21 +847,33 @@ test-document-viewer pattern). The pinned "· chunk n/m" part (the
assert row[2] == row[1]
assert row[6] == "" and row[7] is True, row
# 3. every other state — the chunk part is hidden + empty, and the
# title is the byte-identical pre-phase-129 label for that state.
# 3. every other RUNNING state — the chunk part is hidden +
# empty, the title is the byte-identical pre-phase-129 label,
# and the announcer (the box) hears the whole label (A4).
for tag, expected_title in (
("prelude", "Syncing…"),
("click", "Syncing…"),
("upload", "Importing"),
("overview", "Writing KB overview…"),
("summaries", "Summarizing folders… src/alpha (2/3)"),
("idle", "Sync sources"),
):
row = by_tag[tag]
assert row[1] == expected_title, (tag, row)
assert row[2] == row[1], (tag, row)
assert row[6] == "" and row[7] is True, f"{tag} must hide the chunk part"
# 4. the idle settle (phase 150): the settle label rides the
# BUTTON only — the non-running writer call leaves the box
# alone (A2: the idle/settle labels must not fill the panel),
# so the announcer (the box) keeps the last running label.
row = by_tag["idle"]
assert row[1] == "Sync sources", row
assert row[2] == "Summarizing folders… src/alpha (2/3)", (
"the idle settle leaves the box alone (the settle label stays on"
" the button — phase 150, A2)"
)
assert row[6] == "" and row[7] is True, "idle 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
@@ -815,11 +897,13 @@ test-document-viewer pattern). The pinned "· chunk n/m" part (the
def test_progress_box_and_running_button_contract_under_node() -> None:
"""Phase 149 (task 01) — the detail box + the icon-only running
button, behaviorally pinned through the SAME node harness (the
REAL setSyncLabel / enterSyncRunningState / settleSyncButton
against the stub element refs). The contract — the box is
visible IFF a run is in flight, and its text is EXACTLY the full
untruncated label (one source, two surfaces: the box for sighted
users, the announcer for AT):
REAL setSyncLabel / enterSyncRunningState / settleSyncButton /
setSyncProgress against the stub element refs). The contract —
while a run is in flight the box is visible and its text is
EXACTLY the full untruncated label (phase 150: the box IS the
announcer — one surface for sighted users and AT), and its
visibility is TEXT-DRIVEN (the single writer setSyncProgress
hides it iff the text is empty):
* EVERY running state (the in-flight import, the pre-batch
import, the prelude, the 202/409 click, the upload run, the
@@ -831,14 +915,20 @@ def test_progress_box_and_running_button_contract_under_node() -> None:
the FULL live label (the icon-only running button keeps a
name);
* the settle (the REAL settleSyncButton through the writer with
the default `running = false`): the box HIDES, the settle
label is written into the box text ("Sync sources" — hidden,
so inert), `is-running` + `is-spinning` are both gone, and
the aria-label is restored to the ship value "Sync sources".
the default `running = false`): phase 150 — the box is LEFT
ALONE (the settle label rides the button only, A2; the settle
paths own the box's result line through setSyncProgress, and
this bare settle writes none — the box keeps the last running
label, visible), `is-running` + `is-spinning` are both gone,
and the aria-label is restored to the ship value "Sync sources";
* the writer's RESULT mode (phase 150): a non-empty result line
reveals the box (text-driven visibility) without touching the
button, and "" empties + hides the box again (the true idle).
"""
rows = _chunk_harness_rows()
by_tag = {row[0]: row for row in rows}
# row = [tag, title, announcer, main, count, countHidden, chunk,
# row = [tag, title, announcer (the box's text — the sole live
# region, phase 150), main, count, countHidden, chunk,
# chunkHidden, cancelHidden, cancelDisabled, progressText,
# progressHidden, btnRunning, iconSpinning, ariaLabel]
@@ -870,12 +960,19 @@ def test_progress_box_and_running_button_contract_under_node() -> None:
f"{tag}: the running button's accessible name is the full live label"
)
# The settle: the box hides, the running state is fully cleared,
# and the accessible name is back to the ship value.
# The settle (phase 150): the box is LEFT ALONE — the settle
# label rides the button only (A2), so the box keeps the last
# running label, still visible; the running state is fully
# cleared, and the accessible name is back to the ship value.
row = by_tag["idle"]
assert row[11] is True, "the settle must HIDE the detail box (run-scoped only)"
assert row[10] == "Sync sources", (
"the writer wrote the settle label into the box (hidden — inert)"
assert row[1] == "Sync sources", "the settle label rides the button"
assert row[11] is False, (
"the bare settle leaves the box alone (phase 150: visibility is"
" text-driven via setSyncProgress — the settle writes no box text)"
)
assert row[10] == "Summarizing folders… src/alpha (2/3)", (
"the box keeps the last running label (the settle label stays on"
" the button — A2)"
)
assert row[12] is False, "is-running is removed on the settle (with the spin)"
assert row[13] is False, "the spin is removed on the settle"
@@ -883,6 +980,19 @@ def test_progress_box_and_running_button_contract_under_node() -> None:
"the settle restores the ship aria-label (the idle truth)"
)
# The writer's result mode (phase 150): a result line reveals the
# box (text-driven visibility) without touching the button; ""
# empties + hides the box (the true idle).
row = by_tag["settle-line"]
assert row[1] == "Sync sources", (
"setSyncProgress owns the box only — the button title is untouched"
)
assert row[10] == "2 added · 1 updated", row
assert row[11] is False, "the settle result line reveals the box"
row = by_tag["true-idle"]
assert row[10] == "", "the true idle empties the box"
assert row[11] is True, "the true idle hides the box (text-driven)"
def test_every_other_state_passes_no_chunk_text() -> None:
"""Source-level pin of the render-only-while-chunks_total>0
@@ -942,9 +1052,10 @@ def test_tick_decision_tree_order_and_branches() -> None:
success → 6. upload failed → 7. both idle. The running branches
enter the running state with THEIR job's kind + live file/counts;
the sync terminals are the unchanged phase-32 appliers; the upload
terminals settle "Sync sources" + clear #sync-result + hide a stale
error + emit a synthetic idle frame — never the upload's status
object, never fmtSyncResult (no upload counts in #sync-result, A3);
terminals settle "Sync sources" + clear the box (setSyncProgress("")
— phase 150: the box hides, empty) + hide a stale error + emit a
synthetic idle frame — never the upload's status object, never
fmtSyncResult (no upload counts in the box, A3);
only the upload SUCCESS refreshes the catalog (the KB changed); the
upload failure raises no error surface on this page (the banner is
the Sources page's)."""
@@ -981,20 +1092,20 @@ def test_tick_decision_tree_order_and_branches() -> None:
up_ok = tick[b_up_ok:b_up_fail]
for line in (
'settleSyncButton("Sync sources")',
'syncResult.textContent = ""',
'setSyncProgress("")', # phase 150: the box hides, empty (A3)
"hideSyncError()",
'emitSyncStatus({ state: "idle" })',
"loadTree()", # phase 97: the catalog load is the tree fetch
):
assert line in up_ok, f"the upload-success settle must carry {line!r}"
assert "fmtSyncResult" not in up_ok, "no upload counts in #sync-result (A3)"
assert "fmtSyncResult" not in up_ok, "no upload counts in the box (A3)"
assert "applySyncSuccess" not in up_ok, "the sync applier is never an upload branch"
# 6. upload failed: settle only — no catalog refresh (the KB did
# not change) and no error surface on this page (A3).
up_fail = tick[b_up_fail:b_idle]
for line in (
'settleSyncButton("Sync sources")',
'syncResult.textContent = ""',
'setSyncProgress("")', # phase 150: the box hides, empty (A3)
"hideSyncError()",
'emitSyncStatus({ state: "idle" })',
):
@@ -1131,22 +1242,22 @@ def test_section_header_documents_the_two_job_contract() -> None:
def test_sources_html_comment_documents_the_live_announcer() -> None:
"""The #sync-result comment in the shell's RAG view (formerly
sources.html) documents the announcer's role: the SYNC's live file
label ("Syncing… <file> (n/m)"), untruncated for the aria-live
announcer, and — phase 90 — the BARE "Importing…" label an
in-flight upload run adopts (unpack + register only, no scan: its
status never carries a file or counts); empty after an upload
settles (A3 — the upload's result line lives on the Sources
page)."""
"""The #sync-progress comment in the shell's RAG view (formerly
sources.html) documents the box's role as the SOLE aria-live
announcer (phase 150): it carries the full untruncated live file
label while a sync run is in flight AND the settle result line
after it ("N added · …", "Sync cancelled", the cancel-failure
copy), hiding only when its text is empty — one live region, no
double announce (the retired announcer span is gone)."""
html = _html()
idx = html.find('id="sync-result"')
idx = html.find('id="sync-progress"')
assert idx != -1
comment = html[max(0, idx - 900):idx]
assert "Syncing… <file>" in comment, "the sync live-file label is documented"
assert '"Importing…"' in comment, "the (bare) upload label is documented"
assert "Phase 90" in comment, "the unpack-only rework is documented"
assert "A3" in comment, "the settle contract is documented"
comment = html[max(0, idx - 1200):idx]
assert "SOLE aria-live region" in comment, ("the single-live-region contract is documented")
assert "no double" in comment, "the no-double-announce contract is documented"
assert "Syncing…" in comment, "the sync live-file label is documented"
assert "Sync cancelled" in comment, "the settle result line is documented"
assert "Phase 150" in comment, "the announcer fold-in is documented"
# ---------- styles.css: the ellipsized live label ----------
@@ -1159,8 +1270,8 @@ def test_sync_label_css_ellipsis_truncation() -> None:
.sync-label-main part is the shrinkable item (min-width: 0, overflow
hidden, text-overflow ellipsis, no wrap) whose long
source/relative/path ellipsizes; the .sync-label-count part is
flex 0 0 auto — NEVER clipped — in the .sync-result mono language
(a position reads as data). On mobile the pill goes full width and
flex 0 0 auto — NEVER clipped — in the .sync-progress-text mono
language (a position reads as data). On mobile the pill goes full width and
the label FILLS it (the cap lifts, flex 1 1 auto) so the main part
ellipsizes against the width left by the pinned count (the
icon-only squeeze is gone)."""
+102 -42
View File
@@ -8,7 +8,8 @@ in sources.js (owner rework 2026-08-28: the button is Sources-page
only, so the page owns the machine it drives — 2 s poll, 202 start
/ 409 adoption / 403 hide, terminal labels, the "bor:sync-status"
event with the status object as detail, the single-poll-loop guard, no
client-side hard timeout), the page's #sync-result line +
client-side hard timeout), the page's #sync-progress box (the sole
aria-live region — phase 150 retired the announcer span) +
#sync-error-banner, the lazily created error modal (phase 41), and the
§7.4 never-stale CSS (spin + reduced-motion opt-out,
disabled state, 44px floor, contrast pair) — so a silent regression is
@@ -40,7 +41,7 @@ main span (the idle text pre-JS) + a hidden count span (the pinned
"(n/m)"), the builders return { main, count }, and the single writer
setSyncLabel(main, count, chunk — the chunk part is phase 129) lands
the parts + the untruncated full label (main + " " + count +
" " + chunk) on the title + #sync-result; the running-state /
" " + chunk) on the title + #sync-progress; the running-state /
settle pins move onto that shape (their labels stay byte-identical
as full strings). Phase 129 (task 04): #sync-label ships a THIRD
hidden span (the pinned "· chunk n/m" — the in-flight file's
@@ -52,9 +53,10 @@ shape.
Phase 149 (task 01): the running button becomes the SPINNER — the
writer gains the `running` flag (the running entry passes `true`,
the settle the default `false`), which reveals the detail box
(#sync-progress — the full untruncated label, presentational
aria-hidden; #sync-result stays the sole announcer) and owns the
button's accessible name (the full live label while running, the
(#sync-progress — the full untruncated label; phase 150: the box
became the SOLE announcer — the previous live-region span was
retired) and owns the button's accessible name (the full live label
while running, the
ship value "Sync sources" restored on every settle); the
`is-running` class travels with the spin (added in the running
entry, removed in the settle). The pins below move onto that shape
@@ -139,6 +141,12 @@ STYLES_CSS = ASSETS / "styles.css"
# Phase 76 (task 02): sources.html is folded into the ONE-document shell
# — the sync markup now lives in the RAG view section of index.html.
SHELL_HTML = FRONTEND / "index.html"
# Phase 150: the retired sync-announcer span's id/class are assembled
# (never spelled out) so the phase gate's project-wide grep for the
# retired name stays clean while the absence pins below stay in force.
RETIRED_ID = "-".join(("sync", "result")) # built at runtime: 3.12+ folds
# adjacent/"+" constant strings at compile time, which would leave the
# retired name in the .pyc — the phase gate greps even the bytecode.
def _text(path: Path) -> str:
@@ -228,17 +236,24 @@ def test_sync_button_has_icon_and_label_parts() -> None:
assert 'class="sync-label"' in label.group(0)
def test_sync_result_is_the_aria_live_announcer() -> None:
"""#sync-result sits right after the button and is a polite live
region (role="status" + aria-live="polite") — the last-result /
counts announcement for screen readers."""
def test_sync_progress_is_the_aria_live_announcer() -> None:
"""#sync-progress (the sync detail box) sits right after the button
row and is a polite live region (role="status" +
aria-live="polite") — phase 150: it is the SOLE aria-live
announcer for the sync area (the live label while a run is in
flight, the settle counts line after it), and the retired
announcer span is gone (one live region, no double announce)."""
text = _text(SHELL_HTML)
tag = re.search(r'<span[^>]*id="sync-result"[^>]*>', text)
assert tag, "the shell must carry the #sync-result announcer (RAG view)"
tag = re.search(r'<div[^>]*id="sync-progress"[^>]*>', text)
assert tag, "the shell must carry the #sync-progress announcer box (RAG view)"
attrs = tag.group(0)
assert 'role="status"' in attrs
assert 'aria-live="polite"' in attrs
assert text.find('id="sync-result"') > text.find("</button>", text.find('id="sync-btn"'))
assert 'aria-hidden' not in attrs, "an aria-live region must not be aria-hidden"
assert -1 < text.find("</button>", text.find('id="sync-btn"')) < text.find('id="sync-progress"')
assert f'id="{RETIRED_ID}"' not in text, (
"the retired announcer span is gone from the shell (phase 150)"
)
def test_sync_error_banner_is_a_hidden_alert() -> None:
@@ -368,8 +383,9 @@ def test_sources_js_running_state_is_never_stale() -> None:
fresh run starts clean: the previous failure's title / aria-label /
.is-error come off NOW, not when the run settles.
Phase 64 (task 04): #sync-result (the aria-live announcer) carries
the FULL untruncated label. Phase 98 (task 02): the title rule is
Phase 64 (task 04): the aria-live announcer carries the FULL
untruncated label (phase 150: that is the #sync-progress box —
the sole live region). Phase 98 (task 02): the title rule is
adjusted — the FULL untruncated LABEL (the file label, the overview
label, or the summaries label — whatever the running form is) rides
the button title for EVERY running label; the "title only when
@@ -381,9 +397,9 @@ def test_sources_js_running_state_is_never_stale() -> None:
part destructures with a "" default), which lands the main part,
the pinned count part (hidden when absent), the pinned chunk part
(phase 129 — hidden when absent), and the FULL untruncated label
on the button title + #sync-result (A4) — the direct span /
on the button title + #sync-progress (A4) — the direct span /
title / announcer writes are GONE from the entry (they live in
setSyncLabel). Phase 149 (task 01): the writer call passes
setSyncLabel + setSyncProgress). Phase 149 (task 01): the writer call passes
`running = true` (the detail box reveals with the full label),
and the button's accessible name rides in the writer too (the
full live label — the icon-only running button keeps a name);
@@ -403,7 +419,9 @@ def test_sources_js_running_state_is_never_stale() -> None:
assert body.count("setSyncLabel(") == 1, "the entry writes ONLY through the parts writer"
assert "syncBtn.title =" not in body, ("the title write lives in setSyncLabel (A4)")
assert "syncLabel.textContent" not in body, ("no direct span write — the parts")
assert "syncResult.textContent" not in body, ("the announcer write lives in setSyncLabel (A4)")
assert "syncProgress" not in body, (
"the box write lives in setSyncLabel/setSyncProgress (phase 150)"
)
assert 'syncBtn.setAttribute("aria-label"' not in body, (
"the aria-label write lives in setSyncLabel (phase 149: the full"
" live label while running)"
@@ -462,11 +480,12 @@ def test_sources_js_settles_the_button_on_terminal() -> None:
(failed included: retry-ready). Phase 128 (task 01): the settle /
idle label ("Sync sources", "Synced HH:MM") goes through the parts
writer — setSyncLabel(label), count hidden (absent), the full
string riding the title + #sync-result (A4, every label state).
Phase 149 (task 01): the `is-running` class is removed WITH the
string riding the title (A4, every label state).
Phase 150 (task 02): the `is-running` class is removed WITH the
spin (every terminal + both idles pass through the settle), and
the writer call's default `running = false` hides the detail box
+ restores the idle accessible name."""
the writer call's default `running = false` leaves the detail
box alone (the settle paths own the box's result line through
setSyncProgress) + restores the idle accessible name."""
js = _text(SOURCES_JS)
body = _body(js, "settleSyncButton")
assert "syncBtn.disabled = false" in body
@@ -649,14 +668,16 @@ def test_sources_js_apply_sync_failure_opens_the_modal_after_the_event() -> None
def test_sources_js_renders_result_line_and_banner_directly() -> None:
"""sources.js owns the page-specific rendering on the Sources page:
#sync-result (aria-live) + #sync-error-banner (role=alert), driven
directly by the state appliers: success → the counts (fmtSyncResult)
+ a live catalog refresh; failed → the banner with the sanitized
error; every state change still emits 'bor:sync-status' (the status
object as detail) for other subscribers."""
#sync-progress (the sole aria-live region — phase 150 retired the
announcer span) + #sync-error-banner (role=alert), driven directly
by the state appliers: success → the counts (fmtSyncResult, the
box's result line) + a live catalog refresh; failed → the banner
with the sanitized error (the box hides, empty); every state
change still emits 'bor:sync-status' (the status object as detail)
for other subscribers."""
js = _text(SOURCES_JS)
success = _body(js, "applySyncSuccess")
assert "syncResult.textContent = fmtSyncResult(status.detail)" in success, (
assert "setSyncProgress(fmtSyncResult(status.detail))" in success, (
"the counts live in status.detail — a bare status renders all zeros"
)
assert "loadTree()" in success, (
@@ -743,13 +764,44 @@ def test_sync_icon_spins_and_respects_reduced_motion() -> None:
assert "@keyframes spin" in css, "the spin keyframes are shared (pre-existing)"
def test_sync_result_is_styled() -> None:
"""#sync-result (the aria-live last-result line) is styled in the
theme tokens — soft ink, small mono, no wrap in the header bar."""
def test_sync_progress_box_is_the_styled_result_surface() -> None:
"""Phase 150: the retired announcer's CSS rule is GONE from styles.css
(the retirement is pinned, not implied), and #sync-progress reads
as a bordered panel in BOTH modes — the running label and the
settled result line (ASSUMPTION A3: one surface, no settled
variant). .sync-progress carries the house panel language (--surface
fill, 1px --line border, the house radius, the 4rem min-height) and
sets no display of its own, so the global [hidden] rule keeps it
display:none while its text is empty; .sync-progress-text is the
small mono soft-ink language that WRAPS (overflow-wrap: anywhere —
the point of the box) (the pinned AA pair on --surface)."""
css = _text(STYLES_CSS)
block = re.search(r"\.sync-result\s*\{([^}]*)\}", css)
assert block, "styles.css must define .sync-result"
assert "var(--ink-soft)" in block.group(1)
assert f".{RETIRED_ID}" not in css, (
"the retired announcer rule (incl. the mobile override) is gone (phase 150)"
)
box = re.search(r"\.sync-progress\s*\{([^}]*)\}", css)
assert box, "styles.css must define .sync-progress"
b = box.group(1)
assert "background: var(--surface)" in b
assert "border: 1px solid var(--line)" in b
assert "border-radius: var(--radius)" in b
assert "min-height: 4rem" in b
assert "display:" not in b, (
"the box sets no display of its own — the global [hidden] rule owns it"
)
hidden_rule = re.search(r"^\[hidden\]\s*\{([^}]*)\}", css, re.M)
assert hidden_rule and "display: none !important" in hidden_rule.group(1), (
"the global [hidden] rule must keep the empty box display:none"
)
text_rule = re.search(r"\.sync-progress-text\s*\{([^}]*)\}", css)
assert text_rule, "styles.css must define .sync-progress-text"
t = text_rule.group(1)
assert "var(--mono)" in t
assert "0.8rem" in t
assert "var(--ink-soft)" in t, ("the pinned AA pair on --surface (8.2:1 dark)")
assert "overflow-wrap: anywhere" in t, (
"the point of the box — a long source/relative/path wraps, never clipped"
)
# ---------- styles.css: the sync failure modal (phase 41, TODO.md L4) ----------
@@ -1978,10 +2030,10 @@ def test_sync_cancel_button_ships_hidden_and_labeled() -> None:
assert 'type="button"' in attrs
assert re.search(r"\bhidden\b", attrs), "#sync-cancel-btn must ship hidden"
assert m.group(3).strip() == "Cancel", "the visible label is the accessible name"
# Same row: right after the main button closes, before the announcer.
# Same row: right after the main button closes, before the announcer box.
main_end = text.find("</button>", text.find('id="sync-btn"'))
cancel_start = text.find('id="sync-cancel-btn"')
assert -1 < main_end < cancel_start < text.find('id="sync-result"')
assert -1 < main_end < cancel_start < text.find('id="sync-progress"')
def test_sync_cancel_btn_css_secondary_pill_and_states() -> None:
@@ -2064,8 +2116,11 @@ def test_sources_js_cancel_visible_only_while_a_sync_runs() -> None:
def test_sources_js_cancel_request_is_never_stale() -> None:
"""requestCancel: the control disables IMMEDIATELY (before the
fetch — one request at a time, no double fire) and #sync-result
clears with it; then the branch set — 403 the whoami backstop
fetch — one request at a time, no double fire). Phase 150: the
box is NOT blanked with the click — the run is still in flight,
so the live label stays up until the poll's cancelled branch
settles it (the never-stale contract rides on the control's
immediate disable); then the branch set — 403 the whoami backstop
(stop polling, hide the main button, settle idle — the same depth
as every other 403 in this module), 409 the silent no-op (the
poll settles), 202 (r.ok) nothing (the poll's cancelled branch
@@ -2079,7 +2134,10 @@ def test_sources_js_cancel_request_is_never_stale() -> None:
assert disable != -1 and fetch != -1 and disable < fetch, (
"the disable must land BEFORE the fetch (never stale)"
)
assert 'syncResult.textContent = ""' in body, "the result line clears with the click"
assert 'setSyncProgress("")' not in body, (
"phase 150: the box is NOT blanked at cancel-request time — the"
" live label stays until the poll settles"
)
assert "r.status === 403" in body
assert "stopSyncPolling()" in body
assert "syncBtn.hidden = true" in body
@@ -2127,13 +2185,15 @@ def test_sources_js_apply_sync_cancelled_is_neutral() -> None:
"""applySyncCancelled renders the NEUTRAL result (A1 — cancel is
intent, not failure): settle the main button retry-ready (which
also hides the Cancel control), the "Sync cancelled" line on the
aria-live result (superseding the settle's label write), the
error banner hidden (a failed run's banner must not survive), the
status frame emitted — NO modal, NO banner, NO error styling."""
aria-live box (#sync-progress — the sole live region, phase 150:
the non-running settle no longer writes the box, so there is
nothing to supersede), the error banner hidden (a failed run's
banner must not survive), the status frame emitted — NO modal,
NO banner, NO error styling."""
js = _text(SOURCES_JS)
body = _body(js, "applySyncCancelled")
assert 'settleSyncButton("Sync sources")' in body
assert 'syncResult.textContent = "Sync cancelled"' in body
assert 'setSyncProgress("Sync cancelled")' in body
assert "hideSyncError()" in body
assert "emitSyncStatus(status)" in body
assert "showSyncModal" not in body, "cancel is intent — no modal"