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
+31 -33
View File
@@ -727,16 +727,17 @@ def test_sync_button_names_the_summary_phase(
page: Page, app_url: str, syncsum_dir: Path, db_ready: None
) -> None:
"""A fresh admin page on ``/sources.html`` clicks **Sync sources**;
the page's OWN 2 s poll renders ``#sync-label`` as
``Summarizing folders… [source/folder]`` + the pinned ``(n/3)``
count span (phase 128: the label is PARTS — the folder part is
optional — the phase's first poll may precede the first hook — and
the button ``title`` carries the full untruncated string, main +
" " + count, A4); on settle the button shows the terminal
``Synced HH:MM`` (the phase-32 contract) and ``#sync-result``
carries the counts line. The prelude/import labels stay the
phase-64 shape (the endpoint test pins those phases; the UI pins
the new summaries label)."""
the page's OWN 2 s poll renders the summaries label —
``Summarizing folders… [source/folder] (n/3)`` (the folder part is
optional — the phase's first poll may precede the first hook) — in
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
stay the phase-64 shape (the endpoint test pins those phases; the
UI pins the new summaries label)."""
page.set_default_timeout(30_000)
_register_source(app_url, str(syncsum_dir))
login(page, app_url) # lands on /sources.html (the button's home)
@@ -759,38 +760,35 @@ def test_sync_button_names_the_summary_phase(
# The click's immediate state (the 202 moment — the bare prefix,
# the phase-64 contract): 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 (D2): the page's own 2 s poll names the summaries phase
# — the RENDERED label matches the D2 PARTS shape (phase 128:
# the MAIN part carries "Summarizing folders… [folder]" — the
# folder part is optional, the phase's first poll may precede the
# first hook — and the pinned COUNT part carries the (n/3)
# counts), and the button ``title`` carries the full untruncated
# string (main + " " + count, A4). ``wait_for_function`` checks
# BOTH in ONE in-browser evaluation: a poll tick updates the parts
# + the title together in a single JS task, so the atomic check
# can never straddle one (the JS regexes are the D2 shapes,
# unit-pinned in tests/unit/test_frontend_sync_upload.py).
# — the RENDERED label in the detail box (phase 149: #sync-progress
# — the button is the spinner) matches the D2 shape —
# "Summarizing folders… [folder] (n/3)" (the folder part is
# optional, the phase's first poll may precede the first hook — the
# (n/3) counts always present) — and the button ``title`` carries
# the same full untruncated string (A4 — one source, two surfaces).
# ``wait_for_function`` checks BOTH in ONE in-browser evaluation:
# a poll tick updates the box + the title together in a single JS
# task (the single setSyncLabel writer), so the atomic check can
# never straddle one (the D2 shapes are unit-pinned in
# tests/unit/test_frontend_sync_upload.py).
page.wait_for_function(
"""() => {
const label = document.querySelector('#sync-label');
const box = document.querySelector('#sync-progress');
const textEl = document.querySelector('#sync-progress-text');
const btn = document.querySelector('#sync-btn');
if (!label || !btn) return false;
const main = label.querySelector('.sync-label-main');
const count = label.querySelector('.sync-label-count');
if (!main || !count || count.hidden) return false;
const reMain = /^Summarizing folders…(?: \\S+)?$/;
const reCount = /^\\(\\d+\\/3\\)$/;
return reMain.test(main.textContent)
&& reCount.test(count.textContent)
&& btn.getAttribute('title')
=== main.textContent + ' ' + count.textContent;
if (!box || !textEl || !btn || box.hidden) return false;
const t = textEl.textContent;
const reFull = /^Summarizing folders…(?: \\S+)? \\(\\d+\\/3\\)$/;
return reFull.test(t) && btn.getAttribute('title') === t;
}""",
timeout=SETTLE_TIMEOUT_MS,
)