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
+2 -2
View File
@@ -355,7 +355,7 @@ def _upload_zip(page: Page, app_url: str, archive: Path) -> None:
def _sync_via_ui(page: Page, app_url: str, expected_result: str) -> None:
"""The RAG page's "Sync sources" button (the scan — phase 90 A3):
click → "Syncing…" → the terminal "Synced HH:MM" label + the fresh
counts in #sync-result (the never-stale contract)."""
counts in #sync-progress (the never-stale contract)."""
page.goto(app_url + "/sources.html")
btn = page.locator("#sync-btn")
expect(btn).to_be_visible(timeout=30_000)
@@ -367,7 +367,7 @@ def _sync_via_ui(page: Page, app_url: str, expected_result: str) -> None:
expect(page.locator("#sync-label")).to_have_text("Syncing…")
expect(page.locator("#sync-label")).to_have_text(SYNCED_LABEL, timeout=SYNC_TIMEOUT_MS)
expect(btn).to_be_enabled()
expect(page.locator("#sync-result")).to_have_text(expected_result)
expect(page.locator("#sync-progress-text")).to_have_text(expected_result)
def _seed_image_doc(
+10 -7
View File
@@ -50,8 +50,9 @@ Test → contract mapping (two tests):
holds ``doc.md``) → the force push (V2) → run 2 (``state ==
"success"`` — NOT ``failed``; the UI settles to "Synced HH:MM"
with the error banner HIDDEN and no error modal — a reset is a
LOGGED event, not a user-facing failure; ``#sync-result`` shows
"0 added · 1 updated" and the document modal serves the V2
LOGGED event, not a user-facing failure; the ``#sync-progress``
box shows "0 added · 1 updated" (phase 150 — the single sync text
surface) and the document modal serves the V2
sentinel — the reset's content flowed through the import; the
counts are the secondary signal, the content is primary) → run 3
(idempotent: "0 added · 1 unchanged" — the checkout is stably at
@@ -480,8 +481,9 @@ def test_sync_survives_a_force_push(
expect(page.locator("#sync-label")).to_have_text("Syncing…")
body = _wait_sync_state(page, app_url, clicked_at=clicked_at)
assert body["state"] == "success", body
# The fixture doc (one A9-format file) is the only change.
expect(page.locator("#sync-result")).to_have_text("1 added")
# The fixture doc (one A9-format file) is the only change — the
# counts line rides the box (phase 150).
expect(page.locator("#sync-progress-text")).to_have_text("1 added")
# The REAL clone was imported: the fixture path is in the Sources
# table (the drill-down tree, the test_sync_button pattern).
_expect_fixture_row(page)
@@ -508,8 +510,8 @@ def test_sync_survives_a_force_push(
expect(page.locator(".sync-modal-backdrop")).to_have_count(0)
# …and the digest delta re-indexed the file as updated (the counts
# are the secondary signal — the content assertion below is
# primary).
expect(page.locator("#sync-result")).to_have_text("0 added · 1 updated")
# primary). The counts line rides the box (phase 150).
expect(page.locator("#sync-progress-text")).to_have_text("0 added · 1 updated")
# The KB holds the NEW head: the document modal serves the V2
# sentinel — the reset's content flowed through the import.
_expect_fixture_row(page)
@@ -530,7 +532,8 @@ def test_sync_survives_a_force_push(
assert body["state"] == "success", body
# "1 unchanged" means the content hash did not move — drift back
# to V1 would have re-reported as "1 updated" (the no-drift pin).
expect(page.locator("#sync-result")).to_have_text("0 added · 1 unchanged")
# The counts line rides the box (phase 150).
expect(page.locator("#sync-progress-text")).to_have_text("0 added · 1 unchanged")
# ---------------------------------------------------------------------------
+11 -7
View File
@@ -513,8 +513,9 @@ def test_add_with_branch_syncs_that_branch(
body = sync_and_wait(page, app_url)
assert body["state"] == "success", body
expect(page.locator("#sync-label")).to_have_text(SYNCED_LABEL, timeout=15_000)
# Both branch docs are fresh after the per-test truncate.
expect(page.locator("#sync-result")).to_have_text("2 added")
# Both branch docs are fresh after the per-test truncate — the
# counts line rides the box (phase 150).
expect(page.locator("#sync-progress-text")).to_have_text("2 added")
# --- the branch's content is what got indexed ----------------------
expect(catalog_has_doc(page, FIXTURE_NAME, "release-only.md")).to_have_count(1)
@@ -537,8 +538,8 @@ def test_add_with_branch_syncs_that_branch(
assert body["state"] == "success", body
expect(page.locator("#sync-label")).to_have_text(SYNCED_LABEL, timeout=15_000)
# Nothing re-embeds (sha256 delta) — the no-op run announces the
# unchanged count (release's two files).
expect(page.locator("#sync-result")).to_have_text("0 added · 2 unchanged")
# unchanged count (release's two files) on the box (phase 150).
expect(page.locator("#sync-progress-text")).to_have_text("0 added · 2 unchanged")
# The branch's doc survived the prune re-import.
expect(catalog_has_doc(page, FIXTURE_NAME, "release-only.md")).to_have_count(1)
@@ -574,7 +575,9 @@ def test_default_branch_source_unaffected(
body = sync_and_wait(page, app_url)
assert body["state"] == "success", body
expect(page.locator("#sync-label")).to_have_text(SYNCED_LABEL, timeout=15_000)
expect(page.locator("#sync-result")).to_have_text("2 added")
# The default branch's two docs are fresh — the counts line rides
# the box (phase 150).
expect(page.locator("#sync-progress-text")).to_have_text("2 added")
# The default branch (main) is what landed — the pre-phase
# byte-identical guarantee, observable through the UI…
@@ -679,8 +682,9 @@ def test_row_editor_branch_change_fails_loud(
assert body["state"] == "success", body
expect(page.locator("#sync-label")).to_have_text(SYNCED_LABEL, timeout=15_000)
# The KB is still empty (the failed sync aborted before the
# import) — the restored branch's two docs are fresh additions.
expect(page.locator("#sync-result")).to_have_text("2 added")
# import) — the restored branch's two docs are fresh additions;
# the counts line rides the box (phase 150).
expect(page.locator("#sync-progress-text")).to_have_text("2 added")
# And the branch's doc is what is indexed again.
expect(catalog_has_doc(page, FIXTURE_NAME, "release-only.md")).to_have_count(1)
+3 -2
View File
@@ -328,8 +328,9 @@ def _sync_in_app(page: Page, app_url: str) -> None:
expect(page.locator("#sync-label")).to_have_text("Syncing…")
expect(page.locator("#sync-label")).to_have_text(SYNCED_LABEL, timeout=SYNC_TIMEOUT_MS)
expect(btn).to_be_enabled() # never stale — re-enabled at the terminal state
# Both fixture docs are fresh after the per-test truncate.
expect(page.locator("#sync-result")).to_have_text("2 added")
# Both fixture docs are fresh after the per-test truncate — the
# counts line rides the box (phase 150).
expect(page.locator("#sync-progress-text")).to_have_text("2 added")
def _source_node(tree_json: dict[str, Any]) -> dict[str, Any]:
+2 -2
View File
@@ -316,7 +316,7 @@ def _upload_zip(page: Page, app_url: str, archive: Path, source: str) -> None:
def _sync_via_ui(page: Page, app_url: str, expected_result: str) -> None:
"""The RAG page's "Sync sources" button (the scan — phase 90 A3):
click → "Syncing…" → the terminal "Synced HH:MM" label + the fresh
counts in #sync-result (the never-stale contract)."""
counts in #sync-progress (the never-stale contract)."""
page.goto(app_url + "/sources.html")
btn = page.locator("#sync-btn")
expect(btn).to_be_visible(timeout=30_000)
@@ -328,7 +328,7 @@ def _sync_via_ui(page: Page, app_url: str, expected_result: str) -> None:
expect(page.locator("#sync-label")).to_have_text("Syncing…")
expect(page.locator("#sync-label")).to_have_text(SYNCED_LABEL, timeout=SYNC_TIMEOUT_MS)
expect(btn).to_be_enabled()
expect(page.locator("#sync-result")).to_have_text(expected_result)
expect(page.locator("#sync-progress-text")).to_have_text(expected_result)
def _captured_systems(mock_llm: int) -> list[str]:
+2 -2
View File
@@ -253,7 +253,7 @@ def _upload_zip(page: Page, app_url: str, archive: Path) -> None:
def _sync_via_ui(page: Page, app_url: str, expected_result: str) -> None:
"""The RAG page's "Sync sources" button (the scan — phase 90 A3):
click → "Syncing…" → the terminal "Synced HH:MM" label + the fresh
counts in #sync-result (the never-stale contract, test_git_source_
counts in #sync-progress (the never-stale contract, test_git_source_
dates' lifecycle wait)."""
page.goto(app_url + "/sources.html")
btn = page.locator("#sync-btn")
@@ -266,7 +266,7 @@ def _sync_via_ui(page: Page, app_url: str, expected_result: str) -> None:
expect(page.locator("#sync-label")).to_have_text("Syncing…")
expect(page.locator("#sync-label")).to_have_text(SYNCED_LABEL, timeout=SYNC_TIMEOUT_MS)
expect(btn).to_be_enabled()
expect(page.locator("#sync-result")).to_have_text(expected_result)
expect(page.locator("#sync-progress-text")).to_have_text(expected_result)
def _drill_to_source(page: Page, name: str) -> None:
+2 -2
View File
@@ -328,7 +328,7 @@ def _upload_zip(page: Page, app_url: str, archive: Path, source: str) -> None:
def _sync_via_ui(page: Page, app_url: str, expected_result: str) -> None:
"""The RAG page's "Sync sources" button (the scan): click →
"Syncing…" → the terminal "Synced HH:MM" label + the fresh counts
in #sync-result (the never-stale contract)."""
in #sync-progress (the never-stale contract)."""
page.goto(app_url + "/sources.html")
btn = page.locator("#sync-btn")
expect(btn).to_be_visible(timeout=30_000)
@@ -340,7 +340,7 @@ def _sync_via_ui(page: Page, app_url: str, expected_result: str) -> None:
expect(page.locator("#sync-label")).to_have_text("Syncing…")
expect(page.locator("#sync-label")).to_have_text(SYNCED_LABEL, timeout=SYNC_TIMEOUT_MS)
expect(btn).to_be_enabled()
expect(page.locator("#sync-result")).to_have_text(expected_result)
expect(page.locator("#sync-progress-text")).to_have_text(expected_result)
def _ask(page: Page, app_url: str, question: str) -> None:
+2 -2
View File
@@ -286,7 +286,7 @@ def _upload_zip(page: Page, app_url: str, archive: Path) -> None:
def _sync_via_ui(page: Page, app_url: str, expected_result: str) -> None:
"""The RAG page's "Sync sources" button (the scan): click →
"Syncing…" → the terminal "Synced HH:MM" label + the fresh counts
in #sync-result (the never-stale contract)."""
in #sync-progress (the never-stale contract)."""
page.goto(app_url + "/sources.html")
btn = page.locator("#sync-btn")
expect(btn).to_be_visible(timeout=30_000)
@@ -298,7 +298,7 @@ def _sync_via_ui(page: Page, app_url: str, expected_result: str) -> None:
expect(page.locator("#sync-label")).to_have_text("Syncing…")
expect(page.locator("#sync-label")).to_have_text(SYNCED_LABEL, timeout=SYNC_TIMEOUT_MS)
expect(btn).to_be_enabled()
expect(page.locator("#sync-result")).to_have_text(expected_result)
expect(page.locator("#sync-progress-text")).to_have_text(expected_result)
def _drill_to_source(page: Page, name: str) -> None:
+2 -2
View File
@@ -447,7 +447,7 @@ def _upload_tar(page: Page, app_url: str, archive: Path) -> None:
def _sync_via_ui(page: Page, app_url: str, expected_result: str) -> None:
"""The RAG page's "Sync sources" button (the scan): click →
"Syncing…" → the terminal "Synced HH:MM" label + the fresh counts
in #sync-result (the never-stale contract)."""
in #sync-progress (the never-stale contract)."""
page.goto(app_url + "/sources.html")
btn = page.locator("#sync-btn")
expect(btn).to_be_visible(timeout=30_000)
@@ -459,7 +459,7 @@ def _sync_via_ui(page: Page, app_url: str, expected_result: str) -> None:
expect(page.locator("#sync-label")).to_have_text("Syncing…")
expect(page.locator("#sync-label")).to_have_text(SYNCED_LABEL, timeout=SYNC_TIMEOUT_MS)
expect(btn).to_be_enabled()
expect(page.locator("#sync-result")).to_have_text(expected_result)
expect(page.locator("#sync-progress-text")).to_have_text(expected_result)
def _drill_to_source(page: Page, name: str) -> None:
+7 -4
View File
@@ -270,8 +270,10 @@ def test_admin_sync_lifecycle(page: Page, app_url: str, db_ready: None) -> None:
expect(page.locator("#sync-progress")).to_have_text("Syncing…")
expect(page.locator("#sync-label")).to_have_text(SYNCED_LABEL, timeout=SYNC_TIMEOUT_MS)
expect(btn).to_be_enabled() # never stale — re-enabled at the terminal state
# The fixture doc (one A9-format file) is the only change.
expect(page.locator("#sync-result")).to_have_text("1 added")
# The fixture doc (one A9-format file) is the only change — the
# settle's counts line rides the box (phase 150: the box is the
# sole sync text surface).
expect(page.locator("#sync-progress-text")).to_have_text("1 added")
# The REAL clone was imported: the fixture path is in the Sources
# table (the sentinel lives inside it) — via the drill-down tree
@@ -293,8 +295,9 @@ def test_admin_sync_lifecycle(page: Page, app_url: str, db_ready: None) -> None:
expect(page.locator("#sync-label")).to_have_text(SYNCED_LABEL, timeout=SYNC_TIMEOUT_MS)
expect(btn).to_be_enabled()
# Nothing re-embedded (sha256 delta) — the no-op run announces the
# unchanged count instead of an empty live region.
expect(page.locator("#sync-result")).to_have_text("0 added · 1 unchanged")
# unchanged count instead of an empty live region (the box keeps
# its result line — phase 150).
expect(page.locator("#sync-progress-text")).to_have_text("0 added · 1 unchanged")
# The doc survived the prune re-import (its file is still in the
# repo) — via the drill-down tree (phase 97).
_expect_fixture_row(page)
+4 -2
View File
@@ -544,7 +544,9 @@ def test_admin_cancel_lifecycle(page: Page, app_url: str, db_ready: None) -> Non
expect(btn).to_be_enabled() # retry-ready
expect(btn).not_to_have_attribute("aria-busy")
expect(cancel).to_be_hidden()
expect(page.locator("#sync-result")).to_have_text(
# Phase 150: the neutral result line rides the box (the sole sync
# text surface — the retired announcer span is gone).
expect(page.locator("#sync-progress-text")).to_have_text(
"Sync cancelled", timeout=SYNC_SETTLE_TIMEOUT_MS
)
# A1: the cancel is never rendered as a failure — no error banner,
@@ -568,7 +570,7 @@ def test_admin_cancel_lifecycle(page: Page, app_url: str, db_ready: None) -> Non
# content-hash delta (already-indexed files re-import unchanged,
# the rest are added; the interrupted file's uncommitted
# transaction rolled back).
line = page.locator("#sync-result").inner_text()
line = page.locator("#sync-progress-text").inner_text()
m = RESULT_LINE.fullmatch(line)
assert m, f"unexpected result line: {line!r}"
added = int(m.group(1))
+8 -5
View File
@@ -19,7 +19,8 @@ bump ``indexed_at``); a source change syncs ONLY the changed part (new
file → ``1 added``; modified file → ``1 updated`` with only its own
row moving). NO PDF here — the unit matrix (task 02) carries the
vision-skip proof; this suite pins the WIRE (the owner's visible
``#sync-result`` line) + the UI settle + the row-level proof, fast and
``#sync-progress`` result line — phase 150: the box is the single sync
text surface) + the UI settle + the row-level proof, fast and
deterministic.
Per-module app env (the E2E conftest pattern, module-scoped): this
@@ -259,11 +260,13 @@ def _click_sync_and_wait_settle(page: Page) -> None:
def _expect_settle_line(page: Page, expected: str) -> None:
"""The owner's surface: the VISIBLE ``#sync-result`` text in its
house count shape (sources.js's fmtSyncResult — the zero
"""The owner's surface: the VISIBLE ``#sync-progress`` box text in
its house count shape (sources.js's fmtSyncResult — the zero
"updated" part is omitted: ``0 added · N unchanged``,
``1 added · N unchanged``, ``0 added · 1 updated · N unchanged``)."""
expect(page.locator("#sync-result")).to_have_text(
``1 added · N unchanged``, ``0 added · 1 updated · N unchanged``).
Phase 150: the box IS the result line's surface (the retired
announcer span is gone)."""
expect(page.locator("#sync-progress-text")).to_have_text(
expected, timeout=SYNC_TIMEOUT_MS
)
+27 -21
View File
@@ -100,8 +100,9 @@ Test → observable mapping (Playwright Mapping Rule):
``.sync-label-main`` == ``Syncing… chunkprog/big.md``,
``.sync-label-count`` == ``(1/2)``, and ``.sync-label-chunk``
VISIBLE matching ``· chunk \\d+/\\d+`` with the total == the
expected chunk count — and the button ``title`` (and
``#sync-result``, the aria-live announcer) carries the UNTRUNCATED
expected chunk count — and the button ``title`` (and the
``#sync-progress`` box — the aria-live announcer, phase 150: the
box is the single sync text surface) carries the UNTRUNCATED
concatenation main + " " + count + " " + chunk (A4 — asserted
atomically in the same in-browser evaluation, so the parts + title
can never straddle a poll tick); after the file's embed span the
@@ -110,8 +111,8 @@ Test → observable mapping (Playwright Mapping Rule):
hidden + empty while the label itself keeps rendering in sync
(the title tracks the rendered parts, A4); on settle the button
reads the terminal ``Synced HH:MM`` (the phase-32 contract) with
the count + chunk parts hidden and ``#sync-result`` carrying the
counts line.
the count + chunk parts hidden and the ``#sync-progress`` box
carrying the counts line (phase 150).
"""
from __future__ import annotations
@@ -314,7 +315,8 @@ def _dedup_consecutive(values: list[int]) -> list[int]:
#: untruncated A4 text — the big file is the first of the two, so the
#: count part is "(1/2)"), the pinned chunk part is visible matching
#: "· chunk n/m" with m == the expected chunk count (the arg), and the
#: button title + #sync-result carry the UNTRUNCATED concatenation
#: button title + the #sync-progress box (the aria-live announcer,
#: phase 150) carry the UNTRUNCATED concatenation
#: (main + " " + count + " " + chunk — A4: the announcer hears the
#: whole "Syncing… <file> (1/2) · chunk n/m").
_JS_IMPORT_CHUNK_SNAPSHOT = """
@@ -332,14 +334,14 @@ _JS_IMPORT_CHUNK_SNAPSHOT = """
if (!m || Number(m[2]) !== n) return null;
const full = main.textContent + ' ' + count.textContent + ' ' + chunk.textContent;
if (btn.getAttribute('title') !== full) return null;
const result = document.querySelector('#sync-result');
if (!result || result.textContent !== full) return null;
const boxText = document.querySelector('#sync-progress-text');
if (!boxText || boxText.textContent !== full) return null;
return {
mainText: main.textContent,
countText: count.textContent,
chunkText: chunk.textContent,
title: btn.getAttribute('title'),
resultText: result.textContent,
boxText: boxText.textContent,
};
}
"""
@@ -874,15 +876,16 @@ def test_label_shows_the_chunk_part(
and the VISIBLE chunk part matching ``· chunk n/m`` with the total
== the expected chunk count (the big file's span is ~45 s under
the slow proxy — the poll cannot miss it) — with the button
``title`` + ``#sync-result`` carrying the UNTRUNCATED
concatenation (A4, asserted atomically in the same in-browser
evaluation). After the file's embed span, once the label has left
the import phase (overview / summaries / terminal), the chunk part
is CLEARED — hidden + empty — while the label keeps rendering in
sync (the title tracks the rendered parts); on settle the button
reads the terminal ``Synced HH:MM`` (the phase-32 contract) with
the count + chunk parts hidden and ``#sync-result`` carrying the
counts line."""
``title`` + the ``#sync-progress`` box (the aria-live announcer,
phase 150) carrying the UNTRUNCATED concatenation (A4, asserted
atomically in the same in-browser evaluation). After the file's
embed span, once the label has left the import phase (overview /
summaries / terminal), the chunk part is CLEARED — hidden + empty
— while the label keeps rendering in sync (the title tracks the
rendered parts); on settle the button reads the terminal
``Synced HH:MM`` (the phase-32 contract) with the count + chunk
parts hidden and the ``#sync-progress`` box carrying the counts
line (phase 150)."""
page.set_default_timeout(30_000)
big_content = (chunkprog_dir / "big.md").read_text(encoding="utf-8")
n_chunks = len(_expected_big_chunks(big_content))
@@ -893,8 +896,8 @@ def test_label_shows_the_chunk_part(
# Step 1 — the import span with the chunk part (the page's own 2 s
# poll; the big file's ~45 s span outlives the poll ~20×). The
# in-browser snapshot verifies the parts + the title +
# #sync-result TOGETHER (one JS task per poll tick — the atomic
# check can never straddle one) and returns the texts.
# #sync-progress-text TOGETHER (one JS task per poll tick — the
# atomic check can never straddle one) and returns the texts.
snap = page.wait_for_function(
_JS_IMPORT_CHUNK_SNAPSHOT, arg=n_chunks, timeout=SETTLE_TIMEOUT_MS
).json_value()
@@ -908,7 +911,9 @@ def test_label_shows_the_chunk_part(
# aria-live announcer (the "· chunk" part included).
full = f"Syncing… {SOURCE}/big.md (1/2) {snap['chunkText']}"
assert snap["title"] == full and "· chunk" in snap["title"], snap
assert snap["resultText"] == full, snap
# The box IS the announcer (phase 150) — it carries the same full
# string the title does.
assert snap["boxText"] == full, snap
# Step 2 — after the file's embed span: the chunk part is CLEARED
# (hidden + empty) once the label has left the import phase — the
@@ -942,5 +947,6 @@ def test_label_shows_the_chunk_part(
expect(page.locator(".sync-label-count")).to_have_text("")
expect(page.locator(".sync-label-chunk")).to_be_hidden()
expect(page.locator(".sync-label-chunk")).to_have_text("")
expect(page.locator("#sync-result")).to_have_text(f"{N_FILES} added")
# The counts line rides the box (phase 150 — the sole sync surface).
expect(page.locator("#sync-progress-text")).to_have_text(f"{N_FILES} added")
expect(page.locator("#sync-error-banner")).to_be_hidden()
+14 -7
View File
@@ -21,8 +21,10 @@ in the pill even on desktop, and the fix moves the running detail
out of the pill entirely — while a run is in flight the button is
the spinner (the ``is-running`` class; task 02's CSS hides the label
parts), and a detail box under the button row (``#sync-progress``)
carries the SAME full untruncated label — wrapping, never clipped,
``#sync-result`` staying the sole aria-live announcer. Tests 1–3
carries the SAME full untruncated label — wrapping, never clipped.
Phase 150 folded the announcer into the box: it is now the SOLE
sync text surface AND the sole aria-live region (the retired
announcer span is gone). Tests 1–3
therefore pin the BOX: visible while the run is in flight, its text
the pre-phase byte-identical full string ("Syncing… <full
source/relative/path> (n/4)" / "Summarizing folders… <folder>
@@ -30,8 +32,10 @@ source/relative/path> (n/4)" / "Summarizing folders… <folder>
inside the viewport at 390 px and 1280 px, the button in its
running state, and the ``title`` carrying the same full string
(A4 — one source, two surfaces). The SETTLED pins (the phase-32
"Synced HH:MM" label, the count span hidden + empty, the box hidden)
are UNCHANGED. Test 4 (the pre-phase-128 rules' clip reproduction)
"Synced HH:MM" label, the count span hidden + empty) are UNCHANGED;
phase 150 re-pointed the box pin — the box STAYS visible at settle
to carry the counts line (the fmtSyncResult string the retired
announcer spoke, byte-identical). Test 4 (the pre-phase-128 rules' clip reproduction)
pins the old markup/CSS counterfactually — the label parts still
receive their text under any CSS (the writer is unchanged), so it
holds on the DOM text + injected rules regardless of the phase-149
@@ -679,9 +683,12 @@ def _settle(page: Page, app_url: str) -> None:
count = page.locator(".sync-label-count")
expect(count).to_be_hidden()
expect(count).to_have_text("")
# Phase 149 (task 01): the detail box is run-scoped — the settle
# (the writer's default running = false) hides it again.
expect(page.locator("#sync-progress")).to_be_hidden()
# Phase 150: the box is NOT run-scoped — it STAYS visible at
# settle to carry the counts line (the fmtSyncResult shape the
# retired announcer spoke, byte-identical: the fresh 4-file
# fixture's "4 added"), hiding only when its text is empty.
expect(page.locator("#sync-progress")).to_be_visible()
expect(page.locator("#sync-progress-text")).to_have_text(f"{N_FILES} added")
expect(page.locator("#sync-error-banner")).to_be_hidden()
+3 -2
View File
@@ -453,7 +453,8 @@ def test_healthy_sync_still_succeeds(
expect(page.locator("#sync-label")).to_have_text("Syncing…")
expect(page.locator("#sync-label")).to_have_text(SYNCED_LABEL, timeout=SYNC_TIMEOUT_MS)
expect(btn).to_be_enabled() # never stale — re-enabled at the terminal state
expect(page.locator("#sync-result")).to_have_text("1 added")
# The counts line rides the box (phase 150 — the sole sync surface).
expect(page.locator("#sync-progress-text")).to_have_text("1 added")
# The REAL clone was imported: the fixture path is in the Sources
# table (the sentinel lives inside it) — via the drill-down tree
@@ -474,7 +475,7 @@ def test_healthy_sync_still_succeeds(
expect(page.locator("#sync-label")).to_have_text("Syncing…")
expect(page.locator("#sync-label")).to_have_text(SYNCED_LABEL, timeout=SYNC_TIMEOUT_MS)
expect(btn).to_be_enabled()
expect(page.locator("#sync-result")).to_have_text("0 added · 1 unchanged")
expect(page.locator("#sync-progress-text")).to_have_text("0 added · 1 unchanged")
# The doc survived the prune re-import — via the drill-down tree
# (phase 97).
_expect_fixture_row(page)
+419 -101
View File
@@ -1,21 +1,26 @@
"""Phase 149 task 03 E2E (Playwright, mock-only): the owner's ask, pinned
end to end — while a sync run is in flight, the **button is the loading
spinner** (``is-running`` hides the label parts; the spinning icon is the
whole control) and a NEW bordered text box directly under the button row
(``#sync-progress``) carries the **EXACT full untruncated live label** —
``Syncing… <source/relative/path> (n/m) · chunk n/m`` — wrapping, never
clipping (the long ``source/relative/path`` that overflows the pill is the
fixture's whole reason to exist). At settle the box hides, the pill is
restored byte-identical to pre-phase (``Synced HH:MM`` + the ``fmtSyncResult``
counts line on ``#sync-result``), and ``#sync-result`` stayed the SOLE
aria-live announcer the whole time (the box is presentational,
``aria-hidden``).
"""Phase 149 task 03 / phase 150 task 05 E2E (Playwright, mock-only):
the owner's ask, pinned end to end — while a sync run is in flight,
the **button is the loading spinner** (``is-running`` hides the label
parts; the spinning icon is the whole control) and the bordered text
box directly under the button row (``#sync-progress``) carries the
**EXACT full untruncated live label** — ``Syncing…
<source/relative/path> (n/m) · chunk n/m`` — wrapping, never clipping
(the long ``source/relative/path`` that overflows the pill is the
fixture's whole reason to exist). Phase 150 folded the announcer into
the box: it is the **single sync text surface AND the sole aria-live
region** (``role="status" aria-live="polite"``, no ``aria-hidden`` —
the retired announcer span is gone from the DOM), and
at settle it **stays visible** to carry the result line (the
``fmtSyncResult`` counts line, "Sync cancelled" — byte-identical to
the pre-phase announcer's strings), hiding only when its text is empty
(idle, upload settles, failures — the banner + modal carry the error).
The dedicated story suite for ``149_sync_progress_box`` (A16 — one
Playwright file per phase, run in isolation). Tasks 01/02 built the box +
its wiring + its CSS; this suite is the behavioral proof through the REAL
sync path: a real ``git clone`` of a local ``file://`` fixture repo, a real
import, the slowed mock LLM so the in-flight state is observable.
The dedicated story suite for ``149_sync_progress_box`` / phase 150
(A16 — one Playwright file per phase, run in isolation). Phase 149's
tasks 01/02 built the box + its wiring + its CSS; this suite is the
behavioral proof through the REAL sync path: a real ``git clone`` of a
local ``file://`` fixture repo, a real import, the slowed mock LLM so
the in-flight state is observable.
Run in isolation (DB must be up: ``podman compose up -d db``; git on PATH —
a documented environment prerequisite, phase 28):
@@ -73,34 +78,46 @@ the first clone).
Test → observable mapping (Playwright Mapping Rule — the rendered DOM is
the observable; every in-flight snapshot is ONE in-browser evaluation that
verifies the awaited label state and reads the numbers together, so they
can never straddle a 2 s poll tick — the single ``setSyncLabel`` writer
updates the box + the button's aria-label/title + ``#sync-result`` in one
JS task):
can never straddle a 2 s poll tick — the single ``setSyncProgress`` /
``setSyncLabel`` writers update the box + the button's aria-label/title
in one JS task):
1. ``test_running_button_is_spinner_only`` — click **Sync sources**; poll
1. ``test_idle_page_one_live_region_box_hidden`` — a page load with NO
run in flight (this test runs FIRST — the module app has never run a
sync, so the boot re-attach settles idle): the template pins hold —
the retired announcer span is ABSENT from the DOM (the retirement
is pinned, not implied), the sync view (``#view-rag``) carries
EXACTLY ONE
explicit ``[aria-live]`` element and it is ``#sync-progress``
(``role="status"``, ``aria-live="polite"``, NO ``aria-hidden`` —
the two are mutually exclusive by intent), and the box is hidden +
empty (the anonymous-safe idle state).
2. ``test_running_button_is_spinner_only`` — click **Sync sources**; poll
until ``#sync-progress`` is visible (a run is in flight): ``#sync-btn``
carries ``is-running`` AND the icon ``is-spinning`` (the spinner IS the
button), ``#sync-label`` is NOT visible (the label parts are hidden —
the button is the icon), the button's ``aria-label`` EQUALS the box's
current text (written by the same call — never stale), the box is
``aria-hidden`` (presentational — ``#sync-result`` is the sole live
region), the button is disabled + ``aria-busy`` (the pre-phase running
mechanics), and the Cancel button is visible (the phase-134 contract
unchanged).
2. ``test_box_carries_the_full_untruncated_label`` — during the
current text (written by the same call — never stale), the box IS the
live region (``role="status"``, ``aria-live="polite"``, NO
``aria-hidden`` — phase 150) and the ONLY explicit ``[aria-live]``
element in the sync view, the button is disabled + ``aria-busy`` (the
pre-phase running mechanics), and the Cancel button is visible (the
phase-134 contract unchanged).
3. ``test_box_carries_the_full_untruncated_label`` — during the
in-flight window of the LONG-path file (poll until the box text is
``Syncing… <full long path> (1/3)`` — the path string IN FULL, the
pre-phase ellipsis would have truncated it): the box's
``scrollWidth <= clientWidth`` AND the text's ``scrollWidth <=
clientWidth`` (the wrap engaged — nothing is clipped), ``#sync-result``
(the announcer) carries the SAME full string (A4 — one source, two
surfaces), the button's ``aria-label`` equals it too, and the box ink
on the panel background clears WCAG 2.1 AA (≥ 4.5:1). Then a retry loop
over the slow embed window pins the phase-129 chunk part IN THE BOX
(``Syncing… <full long path> (1/3) · chunk n/m`` — the part is only
present while an embed batch is in flight; the proxy's 1.5 s delay
makes the ~15 s window ≫ the 2 s poll).
3. ``test_mobile_box_full_width`` — a fresh page at the house mobile
clientWidth`` (the wrap engaged — nothing is clipped), the box IS the
announcer (``role="status"`` / ``aria-live="polite"`` / no
``aria-hidden`` — phase 150), the button's ``aria-label`` equals the
box's text too, and the box ink on the panel background clears WCAG
2.1 AA (≥ 4.5:1). Then a retry loop over the slow embed window pins
the phase-129 chunk part IN THE BOX (``Syncing… <full long path>
(1/3) · chunk n/m`` — the part is only present while an embed batch
is in flight; the proxy's 1.5 s delay makes the ~15 s window ≫ the
2 s poll).
4. ``test_mobile_box_full_width`` — a fresh page at the house mobile
viewport (390×844 — the ``test_sync_label_mobile.py`` constant): the
same in-flight assertions — the box is FULL WIDTH (its width ≈ the
container's content width minus the container's padding — no override
@@ -108,19 +125,36 @@ JS task):
(``scrollWidth <= clientWidth``), the icon-only button keeps the
mobile full-width rule, the label wraps (the full long path is a
substring — the old mobile-stuffing ellipsis is gone).
4. ``test_settled_state_restores_the_pill`` — in flight first (the box is
visible), then let the run settle (the house settle idiom): the box is
HIDDEN (run-scoped only), the button shows the settle label
5. ``test_settled_state_restores_the_pill`` — in flight first (the box is
visible), then let the run settle (the house settle idiom): the box
STAYS VISIBLE and carries the counts line (the pre-phase
``fmtSyncResult`` shape — ``N added`` — byte-identical to what the
retired announcer spoke), the button shows the settle label
``Synced HH:MM`` (the pre-phase byte-identical settle copy) with the
count + chunk parts hidden and empty, ``is-running`` / ``is-spinning``
are gone, the ``aria-label`` is back to ``"Sync sources"``, and the
counts line on ``#sync-result`` matches the pre-phase ``fmtSyncResult``
shape (``N added``).
5. ``test_reattach_shows_the_box`` — in flight (the box visible), RELOAD
are gone, and the ``aria-label`` is back to ``"Sync sources"``
(ASSUMPTION A2: the settle label stays on the button — the box
carries the counts line, not a duplicate of the button).
6. ``test_reattach_shows_the_box`` — in flight (the box visible), RELOAD
the page (the admin session cookie persists): the load-time re-attach
(``initSyncButton``) finds the RUNNING sync → the box is VISIBLE with
the current label and the button is icon-only again (the never-stale
contract — a page load mid-run shows the truth, not an idle pill).
7. ``test_settled_cancel_shows_the_result_line`` — in flight, then
CANCELLED (the phase-134 control): the poll's ``cancelled`` branch
settles the box with the neutral result line "Sync cancelled"
(VISIBLE — the phase-150 result-line contract, byte-identical to the
pre-phase announcer's string), the button settles retry-ready
("Sync sources", no busy, no running classes), and the cancel is
never rendered as a failure (no error banner, no modal in the DOM).
8. ``test_settled_failure_hides_the_box`` — a BROKEN git source (a
``file://`` URL with no repository behind it, added as a DB row —
which outranks the env fallback) makes the sync's clone step fail:
the settled failure is the box HIDDEN + empty (the failed terminal
announces nothing in the live region), the error banner AND the
failure modal carry the (sanitized) error (the pre-phase contract,
unchanged), and the button settles retry-ready with the error
styling (the accessible name carries the error).
"""
from __future__ import annotations
@@ -225,11 +259,26 @@ IDLE_OR_SETTLED = re.compile(r"^(Sync sources|Synced \d{1,2}:\d{2})$")
SETTLE_TIMEOUT_MS = 120_000
#: The module's wall budget: five full 3-file syncs against the slowed
#: mock (≈ 30–35 s each) must stay well under this — a breach is a
#: regression canary for the sync pipeline's speed, not a flake
#: tolerance.
#: mock (≈ 30–35 s each) + one cancelled run + one fast clone-failure
#: run (phase 150's result-line tests 7/8) must stay well under this —
#: a breach is a regression canary for the sync pipeline's speed, not a
#: flake tolerance.
WALL_BUDGET_S = 600.0
#: The settle's counts line (sources.js's fmtSyncResult — the first
#: part is always "N added"; the rest are conditional, " · "-joined —
#: the shape the pre-phase announcer spoke, byte-identical).
RESULT_LINE = re.compile(r"^\d+ added( · \d+ (updated|unchanged|pruned))*$")
#: A broken git source — the failure trigger (test 8): a ``file://``
#: URL with no repository behind it (the URL shape passes add-time
#: validation — it is the CLONE that fails). Added as a DB row it
#: OUTRANKS the env fallback (effective_sources: any DB row makes
#: ``BOR_GIT_SOURCES`` ignored entirely), so the sync's only source is
#: the unclonable one — the run fails in the clone step, before any
#: import (no partial KB; only the probe's LLM calls run).
BROKEN_REPO_URL = "file:///nonexistent/bor-progressbox-fail-repo"
# ---------------------------------------------------------------------------
# The in-browser snapshot functions (Playwright Mapping Rule — the
# rendered DOM is the observable).
@@ -243,12 +292,47 @@ WALL_BUDGET_S = 600.0
# two surfaces).
# ---------------------------------------------------------------------------
#: Tests 1 + 5 — ANY in-flight state (a run is in flight): the box is
#: Test 1 — the template pin + the idle re-attach (a page load with NO
#: run in flight): the sync view (``#view-rag``) carries EXACTLY ONE
#: explicit ``[aria-live]`` element and it is ``#sync-progress``
#: (``role="status"``, ``aria-live="polite"``, NO ``aria-hidden`` —
#: an aria-live region that is aria-hidden announces nothing), the box
#: is hidden + empty (the anonymous-safe idle state), and the retired
#: announcer span is ABSENT from the DOM.
# 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 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.
_JS_LIVE_REGION_SNAPSHOT = """
() => {
const box = document.querySelector('#sync-progress');
const textEl = document.querySelector('#sync-progress-text');
if (!box || !textEl) return null;
const liveEls = document.querySelectorAll('#view-rag [aria-live]');
return {
role: box.getAttribute('role'),
ariaLive: box.getAttribute('aria-live'),
ariaHidden: box.getAttribute('aria-hidden'),
boxHidden: box.hidden,
boxText: textEl.textContent,
resultCount: document.querySelector('#' + ['sync', 'result'].join('-')) ? 1 : 0,
liveCount: liveEls.length,
liveIds: Array.from(liveEls).map(el => el.id),
};
}
"""
#: Tests 2, 5, 6, 7 — ANY in-flight state (a run is in flight): the box is
#: VISIBLE with non-empty text, and the button is in its running state
#: (is-running + the spinning icon + the hidden label parts + the full
#: live label as aria-label + disabled + aria-busy) with the Cancel
#: control visible (the phase-134 contract). The box's aria-hidden
#: attribute is read too (presentational — the one-live-region pin).
#: control visible (the phase-134 contract). The box's live-region
#: attributes are read too (phase 150 — the box IS the announcer:
#: role="status", aria-live="polite", NO aria-hidden) + the sync view's
#: [aria-live] inventory (the one-live-region pin).
_JS_RUNNING_SNAPSHOT = """
() => {
const box = document.querySelector('#sync-progress');
@@ -260,9 +344,14 @@ _JS_RUNNING_SNAPSHOT = """
if (!box || !textEl || !btn || !label || !icon || !cancel) return null;
if (box.hidden || !textEl.textContent) return null;
const cs = (el) => getComputedStyle(el);
const liveEls = document.querySelectorAll('#view-rag [aria-live]');
return {
boxText: textEl.textContent,
role: box.getAttribute('role'),
ariaLive: box.getAttribute('aria-live'),
ariaHidden: box.getAttribute('aria-hidden'),
liveCount: liveEls.length,
liveIds: Array.from(liveEls).map(el => el.id),
ariaLabel: btn.getAttribute('aria-label'),
isRunning: btn.classList.contains('is-running'),
isSpinning: icon.classList.contains('is-spinning'),
@@ -274,22 +363,22 @@ _JS_RUNNING_SNAPSHOT = """
}
"""
#: Test 2, wait A — the in-flight window of the LONG-path file: the box
#: Test 3, wait A — the in-flight window of the LONG-path file: the box
#: is VISIBLE and its text is the FULL label for the long file —
#: "Syncing… <full long path> (1/3)" (+ the optional phase-129
#: " · chunk n/m" part while an embed batch is in flight — the
#: pre-first-batch frame has none) — the complete
# source/relative/path IN the text (the pre-phase ellipsis would have
#: truncated it). Returns the text + the wrap proof (scrollWidth vs
#: clientWidth of box and text) + the announcer + the aria-label + the
#: clientWidth of box and text) + the box's live-region attributes
#: (phase 150 — the box IS the announcer) + the aria-label + the
#: computed ink/panel colors (the AA pin).
_JS_LONG_FILE_SNAPSHOT = """
(longPath) => {
const box = document.querySelector('#sync-progress');
const textEl = document.querySelector('#sync-progress-text');
const btn = document.querySelector('#sync-btn');
const result = document.querySelector('#sync-result');
if (!box || !textEl || !btn || !result || box.hidden) return null;
if (!box || !textEl || !btn || box.hidden) return null;
const t = textEl.textContent;
const prefix = 'Syncing… ' + longPath + ' (';
if (!t.startsWith(prefix)) return null;
@@ -298,7 +387,9 @@ _JS_LONG_FILE_SNAPSHOT = """
const cs = (el) => getComputedStyle(el);
return {
boxText: t,
resultText: result.textContent,
role: box.getAttribute('role'),
ariaLive: box.getAttribute('aria-live'),
ariaHidden: box.getAttribute('aria-hidden'),
ariaLabel: btn.getAttribute('aria-label'),
boxScrollWidth: box.scrollWidth,
boxClientWidth: box.clientWidth,
@@ -310,7 +401,7 @@ _JS_LONG_FILE_SNAPSHOT = """
}
"""
#: Test 2, wait B — the phase-129 chunk part IN THE BOX during the slow
#: Test 3, wait B — the phase-129 chunk part IN THE BOX during the slow
#: embed window: the box text is "Syncing… <full long path> (1/3) ·
#: chunk n/m" (the part is only present while an in-flight file's embed
#: batch is in flight — the proxy's 1.5 s delay makes the ~15 s
@@ -337,7 +428,7 @@ _JS_LONG_FILE_CHUNK_SNAPSHOT = """
}
"""
#: Test 3 — the mobile (390×844) in-flight window of the long file: the
#: Test 4 — the mobile (390×844) in-flight window of the long file: the
#: box text is the full label (the long path IN the text), the box is
#: FULL WIDTH (its bounding-box width ≈ the container's content width —
#: the block rides the 72rem container, no override), no horizontal
@@ -676,20 +767,47 @@ def _start_sync(page: Page, app_url: str) -> None:
then click **Sync sources** and pin the click's immediate 202
state: the button IS the spinner (disabled, aria-busy,
``is-running``, the spinning icon) and the box is revealed with
the bare label (phase 149 — the running detail rides the box)."""
the bare label (phase 149 — the running detail rides the box).
The module app is SHARED across tests: a prior test's terminal
state survives in its memory, and the boot re-attach renders it
(asynchronously). A prior FAILED terminal re-opens the failure
modal — its backdrop intercepts clicks until closed — so the
deterministic layer reads the state from the status endpoint first
(the test_sync_cancel pattern) and closes the modal before the
click (the owner's next step — never stale)."""
login(page, app_url) # lands on /sources.html (the button's home)
# The deterministic layer: the app's in-memory run state — a prior
# test's terminal, never "running" (the autouse cleanup waits a
# leaked run out before every test).
with httpx.Client(base_url=app_url, timeout=30.0) as client:
r = client.post("/api/login", json={"password": ADMIN_PASSWORD})
assert r.status_code == 204, r.text
r = client.get("/api/sync/status")
assert r.status_code == 200, r.text
state = r.json()["state"]
assert state != "running", f"a sync leaked across a test boundary: {state}"
btn = page.locator("#sync-btn")
expect(btn).to_be_visible(timeout=30_000)
expect(page.locator("#sync-label")).to_have_text(IDLE_OR_SETTLED)
expect(btn).to_be_enabled()
# A boot re-attach of a prior FAILED terminal opens the failure
# modal (the backdrop intercepts clicks until closed) — close it
# before the click.
if state == "failed":
expect(page.locator(".sync-modal-backdrop")).to_be_visible(timeout=15_000)
page.locator(".sync-modal-close").click()
expect(page.locator(".sync-modal-backdrop")).to_be_hidden()
btn.click()
# The click's immediate state (the 202 moment — the bare prefix,
# the phase-64 contract): the button is the loading spinner and
# the box carries the bare label (phase 149 — one source, two
# surfaces).
# the box carries the bare label (phase 149/150 — the running
# detail rides the single box surface).
expect(btn).to_be_disabled()
expect(btn).to_have_attribute("aria-busy", "true")
expect(btn).to_have_class(re.compile(r"\bis-running\b"))
@@ -720,12 +838,36 @@ def _wait_terminal(app_url: str, timeout_s: float = 120.0) -> dict[str, Any]:
raise AssertionError(f"the run never reached a terminal state: {body}")
def _wait_state(
app_url: str, states: set[str], timeout_s: float = 120.0
) -> dict[str, Any]:
"""Poll the status endpoint (own admin session) until the run's
state is in *states* — the deterministic layer for the non-success
terminals (the cancelled / failed settles, tests 7/8). Returns the
terminal body for the test to pin; a timeout fails with the last
body (never an opaque wait)."""
with httpx.Client(base_url=app_url, timeout=30.0) as client:
r = client.post("/api/login", json={"password": ADMIN_PASSWORD})
assert r.status_code == 204, r.text
deadline = time.monotonic() + timeout_s
body: dict[str, Any] = {}
while time.monotonic() < deadline:
r = client.get("/api/sync/status")
assert r.status_code == 200, r.text
body = r.json()
if body["state"] in states:
return body
time.sleep(0.2)
raise AssertionError(f"the run never reached {sorted(states)}: {body}")
def _settle(page: Page, app_url: str) -> None:
"""The phase-32 success settle (the pre-phase byte-identical
terminal shape, preserved by phase 149): the run reaches a SUCCESS
terminal at the endpoint, then the button shows "Synced HH:MM"
(re-enabled, the running state fully cleared), the box is HIDDEN
(run-scoped), and no error."""
"""The phase-32 success settle, phase-150 shape: the run reaches a
SUCCESS terminal at the endpoint, then the button shows "Synced
HH:MM" (re-enabled, the running state fully cleared) and the box
STAYS VISIBLE carrying the counts line (the fmtSyncResult shape
the pre-phase announcer spoke — phase 150 A1: the box hides only
when its text is empty), with no error."""
_wait_terminal(app_url)
expect(page.locator("#sync-label")).to_have_text(SYNCED_LABEL, timeout=30_000)
btn = page.locator("#sync-btn")
@@ -733,12 +875,54 @@ def _settle(page: Page, app_url: str) -> None:
expect(btn).not_to_have_attribute("aria-busy")
expect(btn).not_to_have_class(re.compile(r"\bis-running\b"))
expect(btn.locator(".sync-icon")).not_to_have_class(re.compile(r"\bis-spinning\b"))
expect(page.locator("#sync-progress")).to_be_hidden()
expect(page.locator("#sync-progress")).to_be_visible()
expect(page.locator("#sync-progress-text")).to_have_text(RESULT_LINE)
expect(page.locator("#sync-error-banner")).to_be_hidden()
# ---------------------------------------------------------------------------
# 1. In flight: the button is the spinner only (no visible label text),
# 1. Idle: the template pins — one live region (the box), the retired
# announcer absent, the box hidden + empty (a page load with no run)
# ---------------------------------------------------------------------------
def test_idle_page_one_live_region_box_hidden(
page: Page, app_url: str, db_ready: None
) -> None:
"""A page load with NO run in flight (the idle re-attach — this
test runs FIRST, so the module app has never started a sync and
the boot re-attach settles the true idle): the phase-150 template
pins hold end to end — the retired announcer span is ABSENT from
the DOM (the retirement is pinned, not implied), the sync
view (``#view-rag``) carries EXACTLY ONE explicit ``[aria-live]``
element and it is ``#sync-progress`` (``role="status"``,
``aria-live="polite"``, NO ``aria-hidden`` — the two attributes are
mutually exclusive by intent), and the box is hidden + empty (the
anonymous-safe idle state — the box hides only when its text is
empty)."""
login(page, app_url) # lands on /sources.html (the box's home)
snap = page.evaluate(_JS_LIVE_REGION_SNAPSHOT)
assert snap is not None, "the box is missing from the template"
# The retirement: the pre-150 announcer span is gone from the DOM.
assert snap["resultCount"] == 0, snap
assert page.locator("#" + RETIRED_ID).count() == 0
# ONE live region in the sync area — and it is the box.
assert snap["liveCount"] == 1, snap
assert snap["liveIds"] == ["sync-progress"], snap
assert snap["role"] == "status", snap
assert snap["ariaLive"] == "polite", snap
assert snap["ariaHidden"] is None, snap
# Idle: the box is hidden + empty, the button is the idle pill.
expect(page.locator("#sync-progress")).to_be_hidden()
expect(page.locator("#sync-progress-text")).to_have_text("")
expect(page.locator("#sync-label")).to_have_text("Sync sources")
expect(page.locator("#sync-btn")).to_be_enabled()
expect(page.locator("#sync-error-banner")).to_be_hidden()
# ---------------------------------------------------------------------------
# 2. In flight: the button is the spinner only (no visible label text),
# the aria-label carries the full live label, and Cancel rides along
# ---------------------------------------------------------------------------
@@ -754,8 +938,10 @@ def test_running_button_is_spinner_only(
``#sync-label`` is NOT visible (the label parts are hidden — the
button is the icon, at any viewport), the button's ``aria-label``
EQUALS the box's current text (written by the same single-writer
call — never stale), the box is ``aria-hidden`` (presentational —
``#sync-result`` remains the sole aria-live region), the running
call — never stale), the box IS the live region (``role="status"``,
``aria-live="polite"``, NO ``aria-hidden`` — phase 150 folded the
retired announcer in) and the ONLY explicit ``[aria-live]`` element
in the sync view (one live region, no double announce), the running
mechanics (disabled + ``aria-busy``) are preserved, and the Cancel
button is visible (the phase-134 contract unchanged)."""
_start_sync(page, app_url)
@@ -778,8 +964,15 @@ def test_running_button_is_spinner_only(
# label, equal to the box's text (the same writer call wrote both).
assert snap["boxText"], snap
assert snap["ariaLabel"] == snap["boxText"], snap
# The box is presentational — #sync-result is the SOLE live region.
assert snap["ariaHidden"] == "true", snap
# Phase 150: the box IS the SOLE live region — role="status",
# aria-live="polite", NO aria-hidden (an aria-live region that is
# aria-hidden announces nothing), and the only [aria-live] element
# in the sync view.
assert snap["role"] == "status", snap
assert snap["ariaLive"] == "polite", snap
assert snap["ariaHidden"] is None, snap
assert snap["liveCount"] == 1, snap
assert snap["liveIds"] == ["sync-progress"], snap
# The pre-phase running mechanics are preserved.
assert snap["btnDisabled"] is True, snap
assert snap["ariaBusy"] == "true", snap
@@ -790,10 +983,9 @@ def test_running_button_is_spinner_only(
# ---------------------------------------------------------------------------
# 2. The box carries the EXACT full untruncated label (long path
# included — wrapped, never clipped), the announcer carries the same
# string, and the chunk part appears in the box during the embed
# window
# 3. The box carries the EXACT full untruncated label (long path
# included — wrapped, never clipped), the box IS the announcer,
# and the chunk part appears in the box during the embed window
# ---------------------------------------------------------------------------
@@ -807,13 +999,14 @@ def test_box_carries_the_full_untruncated_label(
substring (the pre-phase ellipsis would have truncated it — the
wrap means nothing is dropped), the box did NOT overflow
horizontally (``scrollWidth <= clientWidth`` — the wrap engaged)
neither as a panel nor in its text, ``#sync-result`` (the
announcer) carries the SAME full string (A4 — one source, two
surfaces), the button's ``aria-label`` equals it, and the box ink
on the panel clears WCAG 2.1 AA (≥ 4.5:1). Then a retry loop over
the slow embed window pins the phase-129 chunk part IN THE BOX
(``· chunk n/m`` — the part is only present while an embed batch is
in flight; the proxy's 1.5 s delay makes the window wide)."""
neither as a panel nor in its text, the box IS the announcer
(``role="status"`` / ``aria-live="polite"`` / no ``aria-hidden``
— phase 150), the button's ``aria-label`` equals the box's text,
and the box ink on the panel clears WCAG 2.1 AA (≥ 4.5:1). Then a
retry loop over the slow embed window pins the phase-129 chunk part
IN THE BOX (``· chunk n/m`` — the part is only present while an
embed batch is in flight; the proxy's 1.5 s delay makes the window
wide)."""
_start_sync(page, app_url)
# Wait A — the in-flight window of the long file (any frame of it —
@@ -831,9 +1024,13 @@ def test_box_carries_the_full_untruncated_label(
# box: the path WRAPS, it is never clipped).
assert snap["boxScrollWidth"] <= snap["boxClientWidth"], snap
assert snap["textScrollWidth"] <= snap["textClientWidth"], snap
# A4: the announcer carries the SAME full string (one source, two
# surfaces) — and the icon-only button's accessible name does too.
assert snap["resultText"] == snap["boxText"], snap
# Phase 150: the box IS the announcer — role="status",
# aria-live="polite", NO aria-hidden (the retired span is gone) —
# and the icon-only button's accessible name carries the same full
# string.
assert snap["role"] == "status", snap
assert snap["ariaLive"] == "polite", snap
assert snap["ariaHidden"] is None, snap
assert snap["ariaLabel"] == snap["boxText"], snap
# WCAG 2.1 AA: the box's mono ink on its panel background ≥ 4.5:1
# (the theme's pinned identity pair — observed, not assumed).
@@ -857,7 +1054,7 @@ def test_box_carries_the_full_untruncated_label(
# ---------------------------------------------------------------------------
# 3. Mobile (390×844): the box is full-width, the button is icon-only
# 4. Mobile (390×844): the box is full-width, the button is icon-only
# (the mobile full-width rule), and the label wraps
# ---------------------------------------------------------------------------
@@ -906,8 +1103,9 @@ def test_mobile_box_full_width(page: Page, app_url: str, db_ready: None) -> None
# ---------------------------------------------------------------------------
# 4. Settle: the box hides, the "Synced HH:MM" pill is restored
# byte-identical, and the counts line is unchanged
# 5. Settle: the box STAYS visible with the counts line (phase 150),
# the "Synced HH:MM" pill is restored byte-identical, and the counts
# line is unchanged
# ---------------------------------------------------------------------------
@@ -916,21 +1114,23 @@ def test_settled_state_restores_the_pill(
) -> None:
"""A sync is triggered and observed IN FLIGHT (the box is visible —
so the settle below is a real transition, not a no-op), then let to
settle (the house settle idiom): the box is HIDDEN (run-scoped
only), the button shows the settle label ``Synced HH:MM`` (the
pre-phase byte-identical settle copy) with the label parts
restored — the count + chunk parts hidden and empty —,
``is-running`` / ``is-spinning`` are gone, the ``aria-label`` is
back to ``"Sync sources"`` (the ship value), and the counts line
on ``#sync-result`` matches the pre-phase ``fmtSyncResult`` shape
(``N added`` — the 3 fixture files)."""
settle (the house settle idiom): the box STAYS VISIBLE and carries
the counts line — the pre-phase ``fmtSyncResult`` shape
(``N added`` — the 3 fixture files), byte-identical to what the
retired announcer spoke (phase 150 A1: the box holds the result
line; it hides only when its text is empty) —, the button shows
the settle label ``Synced HH:MM`` (the pre-phase byte-identical
settle copy, ASSUMPTION A2: it stays on the button, not the box)
with the label parts restored — the count + chunk parts hidden and
empty —, ``is-running`` / ``is-spinning`` are gone, and the
``aria-label`` is back to ``"Sync sources"`` (the ship value)."""
_start_sync(page, app_url)
# In flight first (the box is visible + the button is the spinner).
page.wait_for_function(_JS_RUNNING_SNAPSHOT, timeout=SETTLE_TIMEOUT_MS).json_value()
# The settle (the house idiom): the terminal lands at the endpoint,
# then the UI's 2 s poll renders the phase-32 terminal shape.
# then the UI's 2 s poll renders the phase-32/150 terminal shape.
_wait_terminal(app_url)
expect(page.locator("#sync-label")).to_have_text(SYNCED_LABEL, timeout=30_000)
@@ -942,8 +1142,11 @@ def test_settled_state_restores_the_pill(
expect(btn).to_have_attribute("aria-label", "Sync sources")
expect(btn).not_to_have_class(re.compile(r"\bis-running\b"))
expect(btn.locator(".sync-icon")).not_to_have_class(re.compile(r"\bis-spinning\b"))
# The box is run-scoped: hidden at settle.
expect(page.locator("#sync-progress")).to_be_hidden()
# Phase 150: the box STAYS visible at settle — it carries the
# counts line (the fmtSyncResult shape the pre-phase announcer
# spoke, byte-identical).
expect(page.locator("#sync-progress")).to_be_visible()
expect(page.locator("#sync-progress-text")).to_have_text(f"{N_FILES} added")
# The label parts are restored — the settle label carries no count
# or chunk part.
count = page.locator(".sync-label-count")
@@ -952,13 +1155,11 @@ def test_settled_state_restores_the_pill(
chunk = page.locator(".sync-label-chunk")
expect(chunk).to_be_hidden()
expect(chunk).to_have_text("")
# The counts line is unchanged (the pre-phase fmtSyncResult pin).
expect(page.locator("#sync-result")).to_have_text(f"{N_FILES} added")
expect(page.locator("#sync-error-banner")).to_be_hidden()
# ---------------------------------------------------------------------------
# 5. A page load mid-run re-attaches with the box visible (never stale)
# 6. A page load mid-run re-attaches with the box visible (never stale)
# ---------------------------------------------------------------------------
@@ -994,3 +1195,120 @@ def test_reattach_shows_the_box(page: Page, app_url: str, db_ready: None) -> Non
assert snap["cancelVisible"] is True, snap
_settle(page, app_url)
# ---------------------------------------------------------------------------
# 7. Settled cancel: the box carries the neutral result line
# "Sync cancelled" (no banner, no modal)
# ---------------------------------------------------------------------------
def test_settled_cancel_shows_the_result_line(
page: Page, app_url: str, db_ready: None
) -> None:
"""A sync is triggered and observed IN FLIGHT (the box is visible
with the running label), then CANCELLED (the phase-134 control):
the poll's ``cancelled`` branch settles the box with the neutral
result line "Sync cancelled" — VISIBLE (the phase-150 result-line
contract: the box stays visible to carry it, byte-identical to the
pre-phase announcer's string) — the button settles retry-ready
("Sync sources", no busy, no running classes), and the cancel is
never rendered as a failure (no error banner, no modal in the
DOM)."""
_start_sync(page, app_url)
# In flight first (the box is visible + the Cancel control is
# revealed).
snap = page.wait_for_function(
_JS_RUNNING_SNAPSHOT, timeout=SETTLE_TIMEOUT_MS
).json_value()
assert snap["cancelVisible"] is True, snap
# The owner's click: the control disables IMMEDIATELY (the
# requestCancel never-stale contract). The box is NOT blanked at
# cancel-request time (phase 150 — the run is still in flight; the
# live label stays up until the poll's cancelled branch settles
# it).
cancel = page.locator("#sync-cancel-btn")
cancel.click()
expect(cancel).to_be_disabled()
# The deterministic layer: the cancelled terminal (error null — a
# cancel is intent, not failure).
body = _wait_state(app_url, {"cancelled"}, timeout_s=60.0)
assert body["state"] == "cancelled", body
assert body["error"] is None, body
# The UI settle: the box stays VISIBLE with the neutral result
# line (byte-identical to the pre-phase announcer's string)…
expect(page.locator("#sync-progress")).to_be_visible(timeout=30_000)
expect(page.locator("#sync-progress-text")).to_have_text(
"Sync cancelled", timeout=30_000
)
# …the button settles retry-ready…
expect(page.locator("#sync-label")).to_have_text("Sync sources", timeout=30_000)
btn = page.locator("#sync-btn")
expect(btn).to_be_enabled()
expect(btn).not_to_have_attribute("aria-busy")
expect(btn).not_to_have_class(re.compile(r"\bis-running\b"))
expect(btn.locator(".sync-icon")).not_to_have_class(re.compile(r"\bis-spinning\b"))
# …and the cancel is never a failure (no banner, no modal in the
# DOM — the backdrop is created on demand, so count 0 is the
# absence proof).
expect(page.locator("#sync-error-banner")).to_be_hidden()
assert page.locator(".sync-modal-backdrop").count() == 0
# ---------------------------------------------------------------------------
# 8. Settled failure: the box hides + empties; the banner + modal carry
# the error (the pre-phase contract, unchanged)
# ---------------------------------------------------------------------------
def test_settled_failure_hides_the_box(
page: Page, app_url: str, db_ready: None
) -> None:
"""A BROKEN git source (a ``file://`` URL with no repository behind
it) is added as a DB row — which OUTRANKS the env fallback
(effective_sources: any DB row makes ``BOR_GIT_SOURCES`` ignored
entirely), so the sync's only source is the unclonable one — and
the run fails in the clone step (before any import). The settled
failure (phase 150): the box is HIDDEN + empty (the failed
terminal announces nothing in the live region — the error has its
own surfaces), the error banner AND the failure modal carry the
(sanitized) error (the pre-phase contract, unchanged), and the
button settles retry-ready with the error styling (the accessible
name carries the error — never an icon-only mystery)."""
# The failure trigger (own admin session — the endpoint is
# admin-only, like the sync endpoints).
with httpx.Client(base_url=app_url, timeout=30.0) as client:
r = client.post("/api/login", json={"password": ADMIN_PASSWORD})
assert r.status_code == 204, r.text
r = client.post("/api/git-sources", json={"url": BROKEN_REPO_URL})
assert r.status_code == 201, r.text
_start_sync(page, app_url)
# The deterministic layer: the failed terminal (the clone step —
# the probe passed: the LLM is the healthy slow proxy).
body = _wait_state(app_url, {"failed"}, timeout_s=120.0)
assert body["state"] == "failed", body
assert body["error"] and "git clone" in body["error"], body
# The error surfaces in the banner AND the failure modal (the
# pre-phase contract, unchanged)…
expect(page.locator("#sync-error-banner")).to_be_visible(timeout=30_000)
expect(page.locator("#sync-error-text")).to_contain_text("git clone")
expect(page.locator(".sync-modal-backdrop")).to_be_visible(timeout=30_000)
expect(page.locator("#sync-modal-error")).to_contain_text("git clone")
# …and the box hides, empty (the failed terminal announces nothing
# — the banner + modal carry the error).
expect(page.locator("#sync-progress")).to_be_hidden()
expect(page.locator("#sync-progress-text")).to_have_text("")
# The button settles retry-ready with the error styling…
expect(page.locator("#sync-label")).to_have_text("Sync sources", timeout=30_000)
btn = page.locator("#sync-btn")
expect(btn).to_be_enabled()
expect(btn).not_to_have_attribute("aria-busy")
expect(btn).not_to_have_class(re.compile(r"\bis-running\b"))
expect(btn).to_have_class(re.compile(r"\bis-error\b"))
+9 -6
View File
@@ -76,8 +76,9 @@ Test → observable mapping (Playwright Mapping Rule):
pinned count part ``(n/3)`` — the asserted text is the RENDERED
DOM parts, with the button ``title`` riding the full untruncated
string, main + " " + count — A4); on settle the button reads the
terminal ``Synced HH:MM`` (the phase-32 contract) and
``#sync-result`` carries the counts line.
terminal ``Synced HH:MM`` (the phase-32 contract) and the
``#sync-progress`` box carries the counts line (phase 150: the box
is the single sync text surface).
3. ``test_missing_folder_summaries_read_as_pending_and_self_heal``
(tree — the fast mock leg): after a successful sync (every
candidate stores its canned line) NO marker is anywhere; DELETE
@@ -733,9 +734,10 @@ def test_sync_button_names_the_summary_phase(
the detail box (phase 149: the running label rides #sync-progress
under the button row; the button is the spinner) with the button
``title`` carrying the same full untruncated string (A4 — one
source, two surfaces); on settle the box hides, the button shows
the terminal ``Synced HH:MM`` (the phase-32 contract) and
``#sync-result`` carries the counts line. The prelude/import labels
source, two surfaces); on settle the box STAYS visible with the
counts line (phase 150: the settle's result line is the box's —
the single sync text surface), the button shows the terminal
``Synced HH:MM`` (the phase-32 contract). The prelude/import labels
stay the phase-64 shape (the endpoint test pins those phases; the
UI pins the new summaries label)."""
page.set_default_timeout(30_000)
@@ -802,7 +804,8 @@ def test_sync_button_names_the_summary_phase(
)
expect(btn).to_be_enabled()
expect(btn).not_to_have_attribute("aria-busy")
expect(page.locator("#sync-result")).to_have_text(f"{N_FILES} added")
# The counts line rides the box (phase 150 — the sole sync surface).
expect(page.locator("#sync-progress-text")).to_have_text(f"{N_FILES} added")
# ---------------------------------------------------------------------------
+4 -2
View File
@@ -659,7 +659,8 @@ def test_upload_toast_then_navigate_away(
expect(page.locator("#sync-label")).to_have_text(SYNCED_LABEL, timeout=30_000)
expect(btn).to_be_enabled()
expect(btn).not_to_have_attribute("aria-busy")
expect(page.locator("#sync-result")).to_have_text(f"{N_FILES} added")
# The counts line rides the box (phase 150 — the sole sync surface).
expect(page.locator("#sync-progress-text")).to_have_text(f"{N_FILES} added")
# Phase 97: the catalog is the drill-down tree — drill source →
# folder `docs` before the per-level row asserts (the flat total
# no longer exists in one tbody).
@@ -857,7 +858,8 @@ def test_sync_live_file_label(
expect(page.locator("#sync-label")).to_have_text(SYNCED_LABEL, timeout=30_000)
expect(btn).to_be_enabled()
expect(btn).not_to_have_attribute("aria-busy")
expect(page.locator("#sync-result")).to_have_text(f"{N_FILES} added")
# The counts line rides the box (phase 150 — the sole sync surface).
expect(page.locator("#sync-progress-text")).to_have_text(f"{N_FILES} added")
# Phase 97: the catalog is the drill-down tree — drill source →
# folder `notes` before the per-level row asserts (the flat total
# no longer exists in one tbody).
+4 -3
View File
@@ -24,8 +24,9 @@ check + import), so its background run settles in milliseconds and the
2 s status poll settles one tick after the 202. The sync leg is short
(2 files + 1 ignored → a handful of mock-LLM requests), so instead of
racing a timer the suite polls the RAG page's settled sync UI
(``#sync-label`` "Synced HH:MM" + ``#sync-result`` counts) with generous
timeouts — the live "Syncing… <file> (n/m)" label is the
(``#sync-label`` "Synced HH:MM" + the ``#sync-progress`` box's counts
line — phase 150: the box is the single sync text surface) with
generous timeouts — the live "Syncing… <file> (n/m)" label is the
``test_sync_upload_progress.py`` suite's subject, not this one's.
The archive is **built in-test** with Python's ``tarfile``:
@@ -559,7 +560,7 @@ def test_ignore_list_then_sync_scans(
expect(page.locator("#sync-label")).to_have_text(
SYNCED_LABEL, timeout=SYNC_TIMEOUT_MS
)
expect(page.locator("#sync-result")).to_have_text("2 added", timeout=SYNC_TIMEOUT_MS)
expect(page.locator("#sync-progress-text")).to_have_text("2 added", timeout=SYNC_TIMEOUT_MS)
expect(page.locator("#sync-btn")).to_be_enabled()
expect(page.locator("#sync-btn")).not_to_have_attribute("aria-busy")
+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"