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
+175 -61
View File
@@ -46,6 +46,23 @@ pass the WHOLE status; the upload job and the 202/409 click stay on
the bare label — the upload status has no phase) and the A4 title-rule
adjustment (the full untruncated label rides the button title for
EVERY running label — the file-only rule is gone).
Phase 128 (task 01) re-points the label pins to the PARTS shape: the
label's string is split into a shrinks-to-fit MAIN part (prefix +
file/folder — the part CSS may ellipsize) and a pinned COUNT part
("(n/m)" — NEVER clipped, hidden whenever absent), so on a phone the
file path ellipsizes while the position stays on-screen. The builders
no longer return one string — they return the parts ({ main, count },
the same decision tree, the same byte-identical copy) — and ONE
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
other label state (idle "Sync sources", settle "Synced HH:MM", the
202/409 click, the re-attach) goes through setSyncLabel(main) —
count hidden.
"""
from __future__ import annotations
@@ -159,18 +176,22 @@ def _tick(js: str) -> str:
return js[tick:end]
# ---------- fmtSyncLabel: the live-file label contract ----------
# ---------- fmtSyncLabel: the live-file label PARTS contract ----------
def test_fmt_sync_label_signature_and_prefixes() -> None:
"""fmtSyncLabel(kind, currentFile, done, total): `kind` picks the
prefix — "upload" → "Importing" (the background run's word, A3),
anything else → "Syncing…". The file is appended only when present
(sync: the bare prefix shows during clone/pull, before any file is
indexed — A4; phase 90: the upload run never carries a file or
counts, so its label is always the bare "Importing…"); the counts
are appended only when total > 0 (the import has started); file
before counts."""
def test_fmt_sync_label_returns_the_parts_contract() -> None:
"""fmtSyncLabel(kind, currentFile, done, total) — Phase 128
(task 01): the builder returns the PARTS, not one string. `kind`
picks the prefix — "upload" → "Importing" (the background run's
word, A3), anything else → "Syncing…". The MAIN part — the
prefix, the file appended only when present (sync: the bare
prefix shows during clone/pull, before any file is indexed — A4;
phase 90: the upload run never carries a file or counts, so its
label is always the bare "Importing"); the COUNT part — only when
total > 0 (the import has started), NO join space (the space is
the CSS gap between the parts — the full string's space is
re-joined by setSyncLabel for the title / announcer, A4). File
before counts in the full-string order."""
body = _fn(_js(), "fmtSyncLabel")
assert "function fmtSyncLabel(kind, currentFile, done, total)" in body
assert 'kind === "upload"' in body
@@ -180,31 +201,74 @@ def test_fmt_sync_label_signature_and_prefixes() -> None:
i_importing = body.find('"Importing"')
i_syncing = body.find('"Syncing…"')
assert -1 < i_kind < i_importing < i_syncing
# file appended only when present
# the MAIN part: the file appended only when present
assert "currentFile ?" in body
assert "`${prefix} ${currentFile}`" in body
# counts only when the import has started (total > 0)
# the COUNT part: only when the import has started (total > 0),
# no join space (the parts are joined by setSyncLabel, A4)
assert "total > 0" in body
assert "` (${done}/${total})`" in body
assert "`(${done}/${total})`" in body
assert "` (${done}/${total})`" not in body, ("no space in the count part")
i_file = body.find("currentFile ?")
i_counts = body.find("total > 0")
assert -1 < i_file < i_counts, "the file lands before the counts"
# the parts return (the byte-identical full strings hold when
# setSyncLabel joins them: "Syncing…", "Syncing… <file>",
# "Syncing… <file> (n/m)", "Importing")
assert "return { main, count }" in body
# ---------- enterSyncRunningState: full path to title + announcer ----------
def test_set_sync_label_is_the_single_parts_writer() -> None:
"""setSyncLabel(mainText, countText = "") — Phase 128 (task 01):
the SINGLE point that writes #sync-label. The MAIN part gets
mainText (textContent only — the house rule, no HTML strings);
the COUNT part gets countText when present (textContent +
hidden=false — the "(n/m)" position is visible, NEVER clipped) and
is emptied + hidden=true when absent (the count span is hidden
whenever there is no "(n/m)" to show). Then the UNTRUNCATED full
label — mainText + (countText ? " " + countText : "") — rides the
button title + #sync-result (the aria-live announcer) exactly as
before (A4 — the screen reader hears the whole thing)."""
body = _fn(_js(), "setSyncLabel")
assert 'function setSyncLabel(mainText, countText = "")' in body
# the parts: textContent only (the house rule), null-guarded refs
assert "syncLabelMain.textContent = mainText" in body
assert "syncLabelCount.textContent = countText" in body
# present count → visible; absent count → hidden, emptied
assert "syncLabelCount.hidden = false" in body
assert 'syncLabelCount.textContent = ""' in body
assert "syncLabelCount.hidden = true" in body
i_vis = body.find("syncLabelCount.hidden = false")
i_hid = body.find("syncLabelCount.hidden = true")
assert -1 < i_vis < i_hid, "the present-count branch precedes the absent one"
# A4: the untruncated full label rides the title + the announcer —
# main + " " + count (the join space), bare main when no count.
assert (
'const full = countText ? `${mainText} ${countText}` : mainText'
) in body, "the full label is main + ' ' + count (bare main when absent)"
i_full = body.find("const full")
i_title = body.find("syncBtn.title = full")
i_result = body.find("syncResult.textContent = full")
assert -1 < i_full < i_title < i_result, ("built once: full → title → announcer")
assert "innerHTML" not in body, "the label path is textContent only"
def test_fmt_sync_phase_label_is_the_exact_d2_contract() -> None:
# ---------- enterSyncRunningState: the parts + the A4 writer ----------
def test_fmt_sync_phase_label_is_the_exact_d2_parts_contract() -> None:
"""fmtSyncPhaseLabel(status) — the SYNC job's phase-aware label
(phase 98 task 02, D2): "overview" → exactly "Writing KB
overview…" (no folder, no counts); "summaries" →
"Summarizing folders… <current_summary> (n/m)" — the folder part
(one leading space + current_summary) is omitted while
current_summary is null (the phase's first poll → the bare
"Summarizing folders… (n/m)"); anything else (the null prelude,
"import") → the phase-64 fmtSyncLabel fall-through, byte-identical
(same builder, same fields, same order). The UPLOAD job never
enters here — its status has no phase (D2)."""
(phase 98 task 02, D2; phase 128 task 01: the PARTS). "overview"
→ main exactly "Writing KB overview…" (no folder, no count part);
"summaries" → main "Summarizing folders… <current_summary>" +
count "(n/m)" — the folder part (one leading space +
current_summary) is omitted while current_summary is null (the
phase's first poll → the bare "Summarizing folders…" main), and
the COUNT IS ALWAYS EMITTED in this phase (the hook always carries
a total); anything else (the null prelude, "import") → the
phase-64 fmtSyncLabel parts, byte-identical (same builder, same
fields, same order). The UPLOAD job never enters here — its status
has no phase (D2)."""
body = _fn(_js(), "fmtSyncPhaseLabel")
assert "function fmtSyncPhaseLabel(status)" in body
# "overview": the exact copy, no folder, no counts.
@@ -212,7 +276,8 @@ def test_fmt_sync_phase_label_is_the_exact_d2_contract() -> None:
i_sum = body.find('status.phase === "summaries"')
assert -1 < i_ov < i_sum
ov = body[i_ov:i_sum]
assert 'return "Writing KB overview…";' in ov
assert 'main: "Writing KB overview…"' in ov
assert 'count: ""' in ov, "the overview label carries no count part"
assert "current_summary" not in ov and "summaries_done" not in ov, (
"the overview label carries no folder and no counts"
)
@@ -223,10 +288,13 @@ def test_fmt_sync_phase_label_is_the_exact_d2_contract() -> None:
assert 'status.current_summary ? ` ${status.current_summary}` : ""' in sum_, (
"the folder part — one leading space + the folder, or nothing"
)
assert "main: `Summarizing folders…${folder}`" in sum_, (
"the exact D2 main copy (with and without the folder)"
)
assert (
"return `Summarizing folders…${folder} "
"(${status.summaries_done}/${status.summaries_total})`;"
) in sum_, "the exact D2 copy (with and without the folder)"
"count: `(${status.summaries_done}/${status.summaries_total})`"
) in sum_, "the count part is ALWAYS emitted in the summaries phase"
assert "total > 0" not in sum_, ("no conditional — the hook always carries a total")
# anything else (null prelude, "import"): byte-identical to today's
# sync label — same builder, same fields, same order.
assert (
@@ -235,18 +303,19 @@ def test_fmt_sync_phase_label_is_the_exact_d2_contract() -> None:
) in body[i_fall:]
def test_enter_running_state_is_phase_aware_and_announces_full_label() -> None:
def test_enter_running_state_is_phase_aware_and_writes_the_parts() -> None:
"""The running-state entry keeps the §7.4 never-stale mechanics
(disabled, aria-busy, spinning icon, the stale .is-error removed)
and — phase 98 (task 02, D2) — is phase-aware for the SYNC job:
a sync-kind call carrying the status object takes the
fmtSyncPhaseLabel form, while the upload job (no status — its
fmtSyncPhaseLabel parts, while the upload job (no status — its
endpoint has no phase) and the 202/409 click keep the bare
fmtSyncLabel label, byte-identical. The FULL untruncated label —
whatever its form — rides the button title (set for EVERY running
label: the phase-64 "title only when there is a current file"
rule is adjusted away) and #sync-result (the aria-live announcer);
CSS ellipsizes only the label span."""
fmtSyncLabel parts, byte-identical. Phase 128 (task 01): the entry
itself no longer writes the label — it hands the parts to the
SINGLE writer (setSyncLabel(main, count)), which lands the main
part, the pinned count part (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)."""
body = _fn(_js(), "enterSyncRunningState")
assert (
"function enterSyncRunningState(kind, currentFile, done, total, status)"
@@ -256,25 +325,48 @@ def test_enter_running_state_is_phase_aware_and_announces_full_label() -> None:
assert 'syncBtn.setAttribute("aria-busy", "true")' in body
assert "syncIcon.classList.add(\"is-spinning\")" in body
assert "syncBtn.classList.remove(\"is-error\")" in body
# the phase-aware split: sync + status → the D2 builder; the
# the phase-aware split: sync + status → the D2 parts builder; the
# fall-through (upload / the click, no status) is the byte-
# identical phase-64 builder.
# identical phase-64 parts builder.
i_phase = body.find("fmtSyncPhaseLabel(status)")
i_fall = body.find("fmtSyncLabel(kind, currentFile, done, total)")
assert 'kind === "sync" && status' in body
assert -1 < i_phase < i_fall, "the phase label precedes the fall-through"
# A4: the untruncated label rides the title for EVERY running label
# (the remove-when-no-file rule is gone) and #sync-result.
assert "syncBtn.title = label" in body, "the full label on hover, always"
assert 'syncBtn.removeAttribute("title")' not in body, (
"the title is set for every running label (D2: not just file labels)"
)
i_title = body.find("syncBtn.title = label")
i_span = body.find("syncLabel.textContent = label")
i_result = body.find("syncResult.textContent = label")
assert -1 < i_title < i_span < i_result, (
"one label: built once, written to title, span AND the announcer"
)
# the parts are handed to the single writer (A4 lives in it).
i_set = body.find("setSyncLabel(main, count)")
assert -1 < i_fall < i_set, "the parts are written after the split"
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 "syncLabel.textContent" not in body, ("no direct span write — the parts")
def test_settle_and_idle_labels_go_through_the_parts_writer() -> None:
"""Every OTHER #sync-label writer — the idle "Sync sources", the
success settle "Synced HH:MM" (via settleSyncButton from
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
enterSyncRunningState (the pins above) — no direct span write
remains anywhere in the file."""
js = _js()
settle = _fn(js, "settleSyncButton")
assert "function settleSyncButton(label)" in settle
assert "setSyncLabel(label)" in settle, "the settle label goes through the parts writer"
assert settle.count("setSyncLabel(") == 1
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).
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"
# NO direct #sync-label write remains anywhere in the module.
assert "syncLabel.textContent" not in js, ("the parts writer is the single entry point")
# ---------- the two-job tick (startSyncPolling) ----------
@@ -522,34 +614,56 @@ def test_sources_html_comment_documents_the_live_announcer() -> None:
def test_sync_label_css_ellipsis_truncation() -> None:
""".sync-label: the live-file label ellipsizes a long
source/relative/path inside the pill (A4) — inline-block with the
min(16rem, 40vw) cap, overflow hidden, text-overflow ellipsis, no
wrap, baseline-aligned; on mobile the pill goes full width with
the label visible (the cap lifts, and min-width: 0 engages the
ellipsis against the full width — the icon-only squeeze is
gone)."""
""".sync-label (phase 128 task 02): the live-file label is a FLEX
ROW of two parts inside the pill (A4) — the container is
inline-flex with the min(16rem, 40vw) cap + min-width: 0; the
.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
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)."""
css = _css()
block = re.search(r"\.sync-label\s*\{([^}]*)\}", css)
assert block, "styles.css must style .sync-label"
body = block.group(1)
for prop in (
"display: inline-block",
"display: inline-flex",
"align-items: baseline",
"gap: 0.35rem",
"min-width: 0",
"max-width: min(16rem, 40vw)",
):
assert prop in body, f".sync-label must carry {prop!r}"
assert "inline-block" not in body, "the old inline-block container is gone"
assert "vertical-align" not in body, "the flex row aligns the parts"
main_block = re.search(r"\.sync-label-main\s*\{([^}]*)\}", css)
assert main_block, "styles.css must style .sync-label-main"
for prop in (
"min-width: 0",
"overflow: hidden",
"text-overflow: ellipsis",
"white-space: nowrap",
"vertical-align: bottom",
):
assert prop in body, f".sync-label must carry {prop!r}"
assert prop in main_block.group(1), f".sync-label-main must carry {prop!r}"
count_block = re.search(r"\.sync-label-count\s*\{([^}]*)\}", css)
assert count_block, "styles.css must style .sync-label-count"
for prop in (
"flex: 0 0 auto",
"white-space: nowrap",
"font-family: var(--mono)",
):
assert prop in count_block.group(1), f".sync-label-count must carry {prop!r}"
mobile = re.search(r"@media \(max-width: 640px\) \{([\s\S]*?)\n\}", css)
assert mobile, "the ≤640px media query must stay"
mbody = mobile.group(1)
assert ".sync-btn { width: 100%; }" in mbody, (
"the mobile Sync pill is full width (the icon-only squeeze is gone)"
)
assert ".sync-label { max-width: none; min-width: 0; }" in mbody, (
"the label stays visible and ellipsizes against the full width"
assert ".sync-label { max-width: none; flex: 1 1 auto; min-width: 0; }" in mbody, (
"the label fills the full-width pill; the main part ellipsizes against the pinned count"
)