phase: 149_sync_progress_box
Build and Push Containers / build-and-push-app (push) Successful in 3m35s
Build and Push Containers / build-and-push-db (push) Successful in 11s

All verification complete. The `ink-soft`/`--surface` pair the box uses is already pinned AA by the existing theme E2E suites (8.2:1 dark, ~6.9:1 light). Everything is green — no fixes were needed.

**Phase 149 final verification report**

- Verified (no re-work needed): `#sync-progress` box wiring (single `setSyncLabel` writer with `running` flag; `is-running` class travels with the spin; aria-label owned by the writer), the panel/icon-only-button CSS, and the 5-test dedicated E2E; all 6 pre-existing sync E2Es' running pins migrated to the box, settled pins untouched.
- Dedicated E2E: `uv run pytest tests/e2e/test_sync_progress_box.py -v --no-cov` → **5 passed in 198.6 s** (isolation, slow-proxy mock; under the 600 s budget).
- Full suite: `uv run pytest --cov=app --cov-report=term-missing` → **3463 passed, app/ coverage 99%** (>90% gate).
- Migrated sync E2Es, each in isolation: `test_sync_button` 3 passed, `test_sync_cancel` 3, `test_sync_chunk_progress` 2, `test_sync_label_mobile` 4, `test_sync_summary_visibility` 3, `test_sync_upload_progress` 4 — all green.
- Lint/types: `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors.
- Criteria: in-flight spinner-only button + full untruncated wrapped box (desktop & mobile 390×844, `scrollWidth ≤ clientWidth`) ✓; settle byte-identical ("Synced HH:MM" + `3 added`, box hidden, aria restored) ✓; `#sync-result` sole live region (box `aria-hidden`), mid-run reload re-attaches ✓; WCAG (AA contrast both themes via the pinned ink-soft/surface pair, focus-visible, reduced-motion kill intact at styles.css L2201) ✓; cancel/409/error/upload/status API unchanged (green suites above) ✓.
- Documentation: README.md reviewed in full (sync-states paragraph now matches the shipped UI; env-var table matches `app/config.py` and covers all `.env.example` vars; pages/commands verified) and AGENTS.md reviewed in full — **verified consistent, no changes needed**.
- Notable: no defects found in this or prior work; no live infrastructure touched.
- Next pending phase: none — `todo/` will be empty once the harness moves 149 (only 149 was pending).
This commit is contained in:
2026-10-01 21:14:48 -04:00
parent b233c8d714
commit dd524246ec
33 changed files with 2512 additions and 393 deletions
+64 -55
View File
@@ -41,8 +41,10 @@ is asserted at BOTH layers the task pins:
non-null ``current_file`` (``source/relative/path``) observed at
some tick, the counts advancing, and the file-less ticks (model
probe — A4) preceding the first file tick;
* **UI** — polling the label for the ``Syncing…`` prefix plus a file
path (generous timeout), which the page's own 2 s poll ticks render.
* **UI** — polling the detail box (``#sync-progress`` — phase 149:
the running label rides the box under the button row, the button is
the spinner) for the ``Syncing…`` prefix plus a file path (generous
timeout), which the page's own 2 s poll ticks render.
The upload archive is built in-test with Python's ``tarfile`` from
**25 small ``.md`` files** (``e2e-prog.tar.gz`` → source
@@ -64,9 +66,10 @@ Contract under test:
documents** (the upload unpacked + registered only) and the sync
button settled idle with no error UI; clicking **Sync sources**
then imports the uploaded row with the LIVE "Syncing… <file> (n/m)"
label (both layers — the phase-129 pinned chunk part "· chunk n/m"
rides the label + title while a file's embed batch is in flight)
and lands the counts ("N added", the catalog refreshes);
label (both layers — the phase-149 detail box carries the full
string, the phase-129 pinned chunk part "· chunk n/m" rides the box
+ title while a file's embed batch is in flight) and lands the
counts ("N added", the catalog refreshes);
* **upload processing (phase 90 A2)**: the upload button shows the
BARE "Processing…" for the whole background run (no file, no
"(n/m)", no title — proven across a held first status GET); every
@@ -76,11 +79,12 @@ Contract under test:
result line points at the Sync button; the KB stays empty;
* **sync live file (A4)**: a multi-file local source; clicking
**Sync sources** on ``/sources.html`` shows "Syncing…" (bare, the
pre-64 click state) then "Syncing… <file> (n/m)" (both layers — the
phase-129 pinned chunk part "· chunk n/m" rides the label + title
while a file's embed batch is in flight), then the pre-64 success
settle — "Synced HH:MM" + the counts result line — preserved, plus
the file in the label;
pre-64 click state) in the detail box (phase 149 — the button is
the spinner) then "Syncing… <file> (n/m)" (both layers — the phase-
129 pinned chunk part "· chunk n/m" rides the box + title while a
file's embed batch is in flight), then the pre-64 success settle —
"Synced HH:MM" + the counts result line — preserved, plus the file
in the label (the box hides at settle);
* **reload re-attach (A1 + A2, phase 90)**: starting an upload and
reloading ``/git-sources.html`` (the sub-second run may still be in
flight — the boot re-attach enters the bare Processing state — or
@@ -548,9 +552,10 @@ def test_upload_toast_then_navigate_away(
documents** (phase 90 A1: the upload unpacked + registered only)
and the sync button settled idle with no error UI; then click
**Sync sources** (the new leg, phase 90) → the LIVE
"Syncing… <file> (n/m)" label while the sync imports the uploaded
row (both layers), then the success settle with the counts and the
catalog refresh."""
"Syncing… <file> (n/m)" detail while the sync imports the uploaded
row (phase 149: the box under the button row — the button is the
spinner — at both layers), then the success settle with the counts
and the catalog refresh (the box hides)."""
page.set_default_timeout(30_000)
_admin_git_sources_page(page, app_url)
# (A previous test's terminal run may re-render its result line at
@@ -599,33 +604,34 @@ def test_upload_toast_then_navigate_away(
# The click's immediate state (A4 — the bare prefix until the
# import's first file): disabled, aria-busy, spinning icon, no
# error…
# error, and the detail box revealed with the bare label
# (phase 149 — the running label rides the box)…
expect(btn).to_be_disabled()
expect(btn).to_have_attribute("aria-busy", "true")
expect(btn.locator(".sync-icon")).to_have_class(re.compile(r"\bis-spinning\b"))
expect(page.locator("#sync-label")).to_have_text("Syncing…")
expect(page.locator("#sync-progress")).to_have_text("Syncing…")
expect(page.locator("#sync-error-banner")).to_be_hidden()
# UI layer: the label gains the live file at the page's 2 s poll
# tick — the file rides the MAIN part, "(n/m)" the pinned COUNT
# span (phase 128: the space between them is the CSS gap, so the
# rendered concatenation carries none — the FULL string with the
# space rides the button title, A4). Phase 129: the pinned CHUNK
# part ("· chunk n/m" — the file's embed-batch position, batch
# granularity) renders while a file's embed batch is in flight —
# both the rendered label (no space — the CSS gap) and the title
# (with the space, A4) carry it.
expect(page.locator("#sync-label")).to_have_text(
# UI layer (phase 149 task 01): the RUNNING detail rides the
# detail box — the page's 2 s poll tick reveals the live file in
# #sync-progress as the FULL untruncated string: "Syncing…
# <source/rel/path> (n/m)" (+ " · chunk n/m" while a file's embed
# batch is in flight — phase 129, batch granularity). The box is
# the visible running surface — nothing is ellipsized away, the
# "(n/m)" count is IN the full string (the never-clipped pin),
# and the button title carries the same full string (A4 — one
# source, two surfaces).
expect(page.locator("#sync-progress-text")).to_have_text(
re.compile(
rf"Syncing… {re.escape(UPLOAD_NAME)}/.+\.md\(\d+/{N_FILES}\)(?:· chunk \d+/\d+)?"
rf"^Syncing… {re.escape(UPLOAD_NAME)}/.+\.md \(\d+/{N_FILES}\)"
r"(?: · chunk \d+/\d+)?$"
),
timeout=SETTLE_TIMEOUT_MS,
)
# Phase 128: the "(n/m)" is NEVER clipped — it sits in its own
# visible count span, and the button title carries the full
# untruncated string (main + " " + count [+ " " + chunk, phase
# 129], A4).
expect(page.locator("#sync-label .sync-label-count")).to_be_visible(
# The box is the VISIBLE running surface (the count's old
# "never clipped" pin now lives in its full string — the "(n/m)"
# is in the text above).
expect(page.locator("#sync-progress")).to_be_visible(
timeout=SETTLE_TIMEOUT_MS
)
expect(btn).to_have_attribute(
@@ -762,12 +768,14 @@ def test_sync_live_file_label(
page: Page, app_url: str, db_ready: None, sync_local_dir: Path
) -> None:
"""A multi-file local source (the ``test_sync_button.py`` fixture
style): on ``/sources.html`` click **Sync sources** → the label
shows "Syncing…" (bare, the pre-64 click state) then "Syncing…
<file> (n/m)" while running (endpoint layer: ``current_file``
non-null at running ticks; UI layer: the label poll), then the
success settle with the counts result line — the pre-phase-64 sync
UX is preserved, plus the file."""
style): on ``/sources.html`` click **Sync sources** → the detail
box (phase 149 — the running label left the pill; the button is
the spinner) shows "Syncing…" (bare, the pre-64 click state) then
"Syncing… <file> (n/m)" while running (endpoint layer:
``current_file`` non-null at running ticks; UI layer: the box
poll), then the success settle with the counts result line (the
box hides) — the pre-phase-64 sync UX's settle is preserved, plus
the file in the label while running."""
page.set_default_timeout(30_000)
_seed_local_source(str(sync_local_dir))
login(page, app_url) # lands on /sources.html (the button's home)
@@ -792,33 +800,34 @@ def test_sync_live_file_label(
# The click's immediate state is the pre-64 one (A4 — the bare
# prefix until the import's first file): disabled, aria-busy,
# spinning icon, no error…
# spinning icon, no error, and the detail box revealed with the
# bare label (phase 149 — the running label rides the box)…
expect(btn).to_be_disabled()
expect(btn).to_have_attribute("aria-busy", "true")
expect(btn.locator(".sync-icon")).to_have_class(re.compile(r"\bis-spinning\b"))
expect(page.locator("#sync-label")).to_have_text("Syncing…")
expect(page.locator("#sync-progress")).to_have_text("Syncing…")
expect(page.locator("#sync-error-banner")).to_be_hidden()
# UI layer: the label gains the live file at the page's 2 s poll
# tick — the file rides the MAIN part, "(n/m)" the pinned COUNT
# span (phase 128: the space between them is the CSS gap, so the
# rendered concatenation carries none — the FULL string with the
# space rides the button title, A4). Phase 129: the pinned CHUNK
# part ("· chunk n/m" — the file's embed-batch position, batch
# granularity) renders while a file's embed batch is in flight —
# both the rendered label (no space — the CSS gap) and the title
# (with the space, A4) carry it.
expect(page.locator("#sync-label")).to_have_text(
# UI layer (phase 149 task 01): the RUNNING detail rides the
# detail box — the page's 2 s poll tick reveals the live file in
# #sync-progress as the FULL untruncated string: "Syncing…
# <source/rel/path> (n/m)" (+ " · chunk n/m" while a file's embed
# batch is in flight — phase 129, batch granularity). The box is
# the visible running surface — nothing is ellipsized away, the
# "(n/m)" count is IN the full string (the never-clipped pin),
# and the button title carries the same full string (A4 — one
# source, two surfaces).
expect(page.locator("#sync-progress-text")).to_have_text(
re.compile(
rf"Syncing… {re.escape(SYNC_SOURCE_DIR)}/.+\.md\(\d+/{N_FILES}\)(?:· chunk \d+/\d+)?"
rf"^Syncing… {re.escape(SYNC_SOURCE_DIR)}/.+\.md \(\d+/{N_FILES}\)"
r"(?: · chunk \d+/\d+)?$"
),
timeout=SETTLE_TIMEOUT_MS,
)
# Phase 128: the "(n/m)" is NEVER clipped — it sits in its own
# visible count span, and the button title carries the full
# untruncated string (main + " " + count [+ " " + chunk, phase
# 129], A4).
expect(page.locator("#sync-label .sync-label-count")).to_be_visible(
# The box is the VISIBLE running surface (the count's old
# "never clipped" pin now lives in its full string — the "(n/m)"
# is in the text above).
expect(page.locator("#sync-progress")).to_be_visible(
timeout=SETTLE_TIMEOUT_MS
)
expect(btn).to_have_attribute(