phase: 128_sync_label_mobile
Build and Push Containers / build-and-push-app (push) Successful in 2m32s
Build and Push Containers / build-and-push-db (push) Successful in 12s

Everything verified and 100% green. The earlier rejection was a report-format issue, not a test failure — all work is in the working tree. Final report:

**Phase 128 final pass — verified (all 3 tasks complete; no code defects found)**
- Verified: `#sync-label` parts markup, `setSyncLabel` writer, part builders (A4 untruncated title/aria-live), flex-row CSS + mobile media query — all match design 1:1.
- `uv run pytest --cov=app --cov-report=term`: 3024 passed; TOTAL coverage **99%** (>90% gate).
- `uv run pytest tests/e2e/test_sync_label_mobile.py -v --no-cov`: 4 passed (119s, isolated).
- `uv run ruff check .`: clean; `uv run pyright`: 0 errors.
- Criteria: (1) mobile 390×844 label in-viewport, count visible in import+summaries — ✅ E2E; (2) desktop strings/cap/title unchanged — ✅ E2E+unit; (3) E2E isolated green — ✅; (4) full suite green — ✅.
- Docs: README.md fixed 5 pre-existing gaps (PDF phase-126 section + `pdf` token, hidden-folders toggle ph.105, document dates ph.106, private git-source masked tokens ph.121, doc-draft Discard ph.115); AGENTS.md verified consistent — no changes.
- Notable: double-`-q` (addopts `-q` + CLI `-q`) suppresses pytest's summary line — standard `-qq` behavior, not a bug; exit codes confirm green.
- Next pending phase: `129_sync_chunk_progress`.
This commit is contained in:
2026-09-27 23:58:57 -04:00
parent 6e654df387
commit 1139f007cf
34 changed files with 2203 additions and 173 deletions
+35 -24
View File
@@ -71,12 +71,13 @@ Test → observable mapping (Playwright Mapping Rule):
summaries_total == 3`` (the keep-final-counts convention).
2. ``test_sync_button_names_the_summary_phase`` (UI — the slow leg):
a fresh admin page on ``/sources.html`` clicks **Sync sources**;
the page's OWN 2 s poll renders ``#sync-label`` matching
``Summarizing folders… [folder] (n/3)`` (the asserted text is the
RENDERED DOM label, with the button ``title`` riding the same
untruncated value — A4); on settle the button reads the terminal
``Synced HH:MM`` (the phase-32 contract) and ``#sync-result``
carries the counts line.
the page's OWN 2 s poll renders ``#sync-label`` as the D2 PARTS
(phase 128: the main part ``Summarizing folders… [folder]`` + the
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.
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
@@ -727,13 +728,15 @@ def test_sync_button_names_the_summary_phase(
) -> 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] (n/3)`` (asserted on the
RENDERED text — the label is built by the page JS — with the
button ``title`` carrying the same untruncated value, 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)."""
``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)."""
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)
@@ -764,22 +767,30 @@ def test_sync_button_names_the_summary_phase(
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 shape (the folder part is
# optional — the phase's first poll may precede the first hook)
# with the (n/3) counts, and the button ``title`` carries the same
# untruncated value (A4). ``wait_for_function`` checks BOTH in ONE
# in-browser evaluation: a poll tick updates label + title together
# in a single JS task, so the atomic check can never straddle one
# (the JS regex is the D2 shape, unit-pinned in
# tests/unit/test_frontend_sync_upload.py).
# — 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).
page.wait_for_function(
"""() => {
const label = document.querySelector('#sync-label');
const btn = document.querySelector('#sync-btn');
if (!label || !btn) return false;
const text = label.textContent;
const re = /^Summarizing folders…(?: \\S+)? \\(\\d+\\/3\\)$/;
return re.test(text) && btn.getAttribute('title') === text;
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;
}""",
timeout=SETTLE_TIMEOUT_MS,
)