phase: 193_sync_remaining_count
**Phase 193 — final verification pass: all gates green.**
- **Verified (all 3 tasks already complete in `complete/193_sync_remaining_count/`):** `fmtSyncLabel`'s count part is `total > 0 && total - done > 0 ? \`${total - done} remaining\` : ""` (plain int, hidden at `N==0`, `total===0` → no part); docstrings updated; chunk/image/summaries parts + `counts_line` terminal + `app/` byte-untouched (`git diff --stat`: no `app/` files).
- **Completion criteria:** `N remaining` shape + marker pins ✅ · upload shares the builder (E2E upload arm) ✅ · chunk/image/summaries pins green ✅ · terminal `counts_line` green unedited + A4 title/`#sync-progress` pin ✅ · suite green, coverage 99% ✅ · lint/types clean ✅ · commit + `todo/→complete/` left to the harness (no git index/history commands run).
- `uv run pytest -q --cov=app --cov-report=term-missing` → **exit 0**, junit: **4792 tests, 0 failures/errors/skipped**, `TOTAL 7181 16 → 99%` (>90%)
- `uv run pytest tests/e2e/test_sync_remaining_count.py -v --no-cov` → **3 passed** (isolated, 53 s)
- Isolated `--no-cov` sweep of the 14 sync-lineage suites (label_mobile, upload_progress, chunk_progress, image_progress, progress_box, summary_visibility, archive_upload_sources, status_freshness, upload_no_scan, peak_memory_visible, session_reset, fast_path_deferred_content, git_freeze, smoke) → **14/14 exit 0**
- `uv run ruff check .` → All checks passed · `uv run pyright` → **0 errors**
- **Docs:** README.md reviewed in full (all 2408 lines) + AGENTS.md. Fixed 2 inconsistencies: the sync running-label clause claimed the upload label shows the count — now states it shares the ONE builder/shape but reports no counts since phase 90 (renders bare); phase-190's "the other ~72 rows" → "the other 83 rows of the panel's 86-row catalogue" (mechanically: 86 CATALOG rows, 84 overridable). `N remaining` already in the sync paragraph; task-01's `(n/m)` examples gone. AGENTS.md verified consistent (no clause names the running count part → unchanged per the phase's "do not add if absent"). Mechanically re-verified: all 86 `BOR_*` settings documented + defaults match, `.env.example` 88 vars covered, 0 broken README anchors, all cited file paths exist, 17 palette colors, migrations head `0030`, pages/shell routes, ports 8123/8901, E2E port allocations (new suite's 8211/8914 unique).
- **Notable:** task 02's documented deviation stands — `files_done` is the importer's 1-based *current* file, so the phase doc's `[1-4]` range is unreachable; count-bearing frames are `1 … N−1` (pinned). Pre-existing, untouched: `E2E_APP_PORT_TOOLBUDGET`/`SAVEDOC` both 8129 and `SYNCUP`/`SYCNCEL` both 8133 — harmless under the one-suite-at-a-time rule.
- **Next pending phase:** none — `193_sync_remaining_count` is the last entry in `.agents/phases/todo/`.
This commit is contained in:
@@ -26,8 +26,8 @@ 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>
|
||||
the full label ("Syncing… <full
|
||||
source/relative/path> N remaining" / "Summarizing folders… <folder>
|
||||
(n/3)" + the phase-129 "· chunk n/m" while a batch is in flight),
|
||||
inside the viewport at 390 px and 1280 px, the button in its
|
||||
running state, and the ``title`` carrying the same full string
|
||||
@@ -47,6 +47,18 @@ pre-phase, that rule simply did not exist, so the faithful
|
||||
pre-phase counterfactual restores the parts' inline-block display
|
||||
at matching specificity, last in the cascade.
|
||||
|
||||
Phase 193 (task 02 — the owner's "how many files are LEFT, not how
|
||||
many files out of the total") re-points the import label's COUNT part:
|
||||
the distance to the end — ``N remaining`` with ``N = total - done`` —
|
||||
shown while ``N >= 1`` and HIDDEN at the final file (``N == 0``; the
|
||||
main part names the file in flight). ``files_done`` is the importer's
|
||||
1-based CURRENT file, so on this 4-file fixture the count-bearing
|
||||
frames are ``3 remaining`` / ``2 remaining`` / ``1 remaining`` and the
|
||||
LAST file renders no count part — the count pins below therefore name
|
||||
``[1-3] remaining``, and every wait resolves on a count-bearing frame.
|
||||
The summaries ``(n/3)`` folder position, the phase-129 chunk part and
|
||||
the terminal counts line are byte-identical.
|
||||
|
||||
Run in isolation (DB must be up: ``podman compose up -d db``):
|
||||
|
||||
uv run pytest tests/e2e/test_sync_label_mobile.py -v --no-cov
|
||||
@@ -90,9 +102,9 @@ Test → observable mapping (Playwright Mapping Rule):
|
||||
**Sync sources**; while the import phase is running (the page's
|
||||
own 2 s poll): the detail box (``#sync-progress``) is VISIBLE and
|
||||
an atomic snapshot of: its text the FULL untruncated label
|
||||
``Syncing… <full source/relative/path> (n/4)`` (+ the phase-129
|
||||
``Syncing… <full source/relative/path> N remaining`` (+ the phase-129
|
||||
"· chunk n/m" part while a batch is in flight — optional, the
|
||||
pre-first-batch frame has none) with the ``(n/4)`` count IN the
|
||||
pre-first-batch frame has none) with the ``N remaining`` count IN the
|
||||
text (never clipped — nothing is ellipsized away), the box's
|
||||
bounding box INSIDE the viewport (left ≥ 0, right ≤ 390 — the
|
||||
bug's direct negation), the button in its running state
|
||||
@@ -110,10 +122,10 @@ Test → observable mapping (Playwright Mapping Rule):
|
||||
count span is hidden + empty, and the box hides.
|
||||
3. ``test_desktop_label_unchanged`` — viewport 1280×800; the same
|
||||
sync: during import the box text matches ``Syncing… <full
|
||||
source/relative/path> (n/m)`` (+ the phase-129 chunk part while a
|
||||
batch is in flight) — the pre-phase byte-identical FULL string
|
||||
(phase 149 moved it from the pill's parts to the box — the text
|
||||
is unchanged), the box inside the viewport, the button in its
|
||||
source/relative/path> N remaining`` (+ the phase-129 chunk part
|
||||
while a batch is in flight) — the FULL string (phase 149 moved it
|
||||
from the pill's parts to the box, phase 193 re-shaped the count
|
||||
part), the box inside the viewport, the button in its
|
||||
running state (``is-running``), and the ``title`` carrying the
|
||||
same untruncated string (A4).
|
||||
4. ``test_pre_phase_css_reproduces_the_clip`` — the pin check: the
|
||||
@@ -210,12 +222,14 @@ FILE_RE = r"synclabel/(?:alpha|bravo)/(?:alpha|bravo)-[ab]\.md"
|
||||
|
||||
#: The in-flight import label's PARTS (the phase-64 byte-identical
|
||||
#: strings, unit-pinned in tests/unit/test_frontend_sync_upload.py):
|
||||
#: main "Syncing… <full source/relative/path>" + count "(n/4)" —
|
||||
#: ``done`` starts at 1 (the importer counts the file as current
|
||||
#: BEFORE indexing it), so ``n`` ∈ 1..4. (Test 4 — the pre-phase
|
||||
#: main "Syncing… <full source/relative/path>" + count "N remaining"
|
||||
#: (phase 193) — ``files_done`` starts at 1 (the importer counts the
|
||||
#: file as current BEFORE indexing it) and the part hides at
|
||||
#: ``total - done == 0``, so ``N`` ∈ 1..3 on this 4-file fixture and
|
||||
#: the LAST file renders no count part at all. (Test 4 — the pre-phase
|
||||
#: rules' clip reproduction — pins the PARTS' DOM text directly.)
|
||||
IMPORT_MAIN_RE = re.compile(rf"^Syncing… {FILE_RE}$")
|
||||
IMPORT_COUNT_RE = re.compile(r"^\([1-4]/4\)$")
|
||||
IMPORT_COUNT_RE = re.compile(r"^[1-3] remaining$")
|
||||
#: The pre-phase byte-identical FULL string (main + " " + count — the
|
||||
#: single span's text before phase 128; the A4 title keeps it), plus
|
||||
#: the OPTIONAL phase-129 chunk part (" · chunk n/m" — the in-flight
|
||||
@@ -223,7 +237,9 @@ IMPORT_COUNT_RE = re.compile(r"^\([1-4]/4\)$")
|
||||
#: 01): the box (#sync-progress) carries this EXACT string while the
|
||||
#: import is in flight (tests 1 + 3) — the title carries it too
|
||||
#: (A4 — one source, two surfaces).
|
||||
IMPORT_FULL_RE = re.compile(rf"^Syncing… {FILE_RE} \([1-4]/4\)(?: · chunk \d+/\d+)?$")
|
||||
IMPORT_FULL_RE = re.compile(
|
||||
rf"^Syncing… {FILE_RE} [1-3] remaining(?: · chunk \d+/\d+)?$"
|
||||
)
|
||||
|
||||
#: The summaries label's FULL string (phase 98 task 02, unit-pinned):
|
||||
#: "Summarizing folders… [source/folder] (n/3)" (the folder part
|
||||
@@ -280,7 +296,9 @@ def _md(title: str, body: str) -> str:
|
||||
|
||||
#: Test 1/3 — the in-flight IMPORT detail: the box is VISIBLE and its
|
||||
#: text is the FULL untruncated label — "Syncing… <full
|
||||
#: source/relative/path> (n/4)" (+ the optional phase-129 " · chunk
|
||||
#: source/relative/path> N remaining" (phase 193 — ``N`` ∈ 1..3 here;
|
||||
#: the final file carries no count part, so the wait resolves on a
|
||||
#: count-bearing frame) (+ the optional phase-129 " · chunk
|
||||
#: n/m" part while a file's embed batch is in flight — the
|
||||
#: pre-first-batch frame has none; the phase-129 dedicated suite pins
|
||||
#: it strictly) — the count is IN the text (never clipped — nothing
|
||||
@@ -296,7 +314,7 @@ _JS_IMPORT_BOX_SNAPSHOT = """
|
||||
const t = textEl.textContent;
|
||||
const reFull = new RegExp(
|
||||
"^Syncing… synclabel/(?:alpha|bravo)/(?:alpha|bravo)-[ab]\\\\.md " +
|
||||
"\\\\([1-4]/4\\\\)(?: · chunk \\\\d+/\\\\d+)?$"
|
||||
"[1-3] remaining(?: · chunk \\\\d+/\\\\d+)?$"
|
||||
);
|
||||
if (!reFull.test(t)) return null;
|
||||
const title = btn.getAttribute('title');
|
||||
@@ -395,7 +413,7 @@ _JS_PARTS_IMPORT_SNAPSHOT = """
|
||||
const count = label.querySelector('.sync-label-count');
|
||||
if (!main || !count || count.hidden) return null;
|
||||
const reMain = /^Syncing… synclabel\\/(?:alpha|bravo)\\/(?:alpha|bravo)-[ab]\\.md$/;
|
||||
const reCount = /^\\([1-4]\\/4\\)$/;
|
||||
const reCount = /^[1-3] remaining$/;
|
||||
if (!reMain.test(main.textContent) || !reCount.test(count.textContent)) return null;
|
||||
const chunk = label.querySelector('.sync-label-chunk');
|
||||
const chunkText = (chunk && !chunk.hidden) ? chunk.textContent : '';
|
||||
@@ -701,7 +719,7 @@ def _assert_box_fits_viewport(snap: dict[str, Any], viewport_w: int) -> None:
|
||||
box is a block in the 72rem centered container, so it can only
|
||||
shrink toward the container's content width — it never overflows
|
||||
the viewport — and its text is the FULL untruncated label (the
|
||||
"(n/m)" count is IN the text; the wrap that keeps long paths
|
||||
"N remaining" count is IN the text; the wrap that keeps long paths
|
||||
on-screen is task 02's overflow-wrap: anywhere, pinned by the
|
||||
phase-149 dedicated suite). The 1 px slack covers float rounding
|
||||
only."""
|
||||
@@ -718,7 +736,7 @@ def _assert_box_fits_viewport(snap: dict[str, Any], viewport_w: int) -> None:
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. Mobile (390×844) — import: the label fits the viewport and the
|
||||
# (n/m) count is visible, never clipped
|
||||
# N remaining count is visible, never clipped
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -732,9 +750,10 @@ def test_mobile_label_fits_viewport_with_count_in_import(
|
||||
(``#sync-progress`` — phase 149: the running detail left the
|
||||
pill; the button is the spinner) is an atomic snapshot of:
|
||||
VISIBLE with the FULL untruncated label ``Syncing… <full
|
||||
source/relative/path> (n/4)`` (+ the phase-129 "· chunk n/m" part
|
||||
while a batch is in flight — optional) with the ``(n/4)`` count IN
|
||||
the text (NEVER clipped — nothing is ellipsized away), the box's
|
||||
source/relative/path> N remaining`` (+ the phase-129 "· chunk n/m"
|
||||
part while a batch is in flight — optional) with the
|
||||
``N remaining`` count IN the text (NEVER clipped — nothing is
|
||||
ellipsized away), the box's
|
||||
bounding box INSIDE the viewport (left ≥ 0, right ≤ 390 — the
|
||||
bug's direct negation), the button in its running state
|
||||
(``is-running``), and the button ``title`` carrying the SAME full
|
||||
@@ -752,7 +771,7 @@ def test_mobile_label_fits_viewport_with_count_in_import(
|
||||
).json_value()
|
||||
|
||||
# The FULL untruncated label — the complete
|
||||
# source/relative/path, the (n/4) count IN the text (never
|
||||
# source/relative/path, the N remaining count IN the text (never
|
||||
# clipped), the optional phase-129 chunk part.
|
||||
assert IMPORT_FULL_RE.match(snap["boxText"]), snap["boxText"]
|
||||
# A4: the button title carries the SAME untruncated label (the
|
||||
@@ -819,9 +838,10 @@ def test_desktop_label_unchanged(
|
||||
phase 149 moved the running detail out of the pill to the box;
|
||||
the owner's report was that the full label was clipped in the
|
||||
pill even on desktop) carries ``Syncing… <full
|
||||
source/relative/path> (n/m)`` (+ the phase-129 chunk part while a
|
||||
batch is in flight): the pre-phase byte-identical FULL string
|
||||
(phase 149 changed the SURFACE, not the text). The box is inside
|
||||
source/relative/path> N remaining`` (+ the phase-129 chunk part
|
||||
while a batch is in flight): the FULL string (phase 149 changed
|
||||
the SURFACE, phase 193 the count part — the desktop keeps the
|
||||
same text as the phone). The box is inside
|
||||
the viewport, the button is in its running state (``is-running``
|
||||
— the spinner), and the button ``title`` carries the same
|
||||
untruncated string (A4)."""
|
||||
@@ -832,9 +852,9 @@ def test_desktop_label_unchanged(
|
||||
_JS_IMPORT_BOX_SNAPSHOT, timeout=SETTLE_TIMEOUT_MS
|
||||
).json_value()
|
||||
|
||||
# The pre-phase byte-identical FULL string ("Syncing… <full
|
||||
# source/relative/path> (n/m)" + " · chunk n/m" while a batch is
|
||||
# in flight) — now in the box, untruncated and wrapping.
|
||||
# The FULL string ("Syncing… <full source/relative/path>
|
||||
# N remaining" + " · chunk n/m" while a batch is in flight) — in
|
||||
# the box, untruncated and wrapping.
|
||||
assert IMPORT_FULL_RE.match(snap["boxText"]), snap["boxText"]
|
||||
assert snap["title"] == snap["boxText"], snap
|
||||
# Phase 149: the running button is the spinner (is-running);
|
||||
@@ -900,7 +920,8 @@ def test_pre_phase_css_reproduces_the_clip(
|
||||
# The clip: the count's box runs past the label's clip edge — the
|
||||
# label's overflow: hidden paints it away (the probe measured the
|
||||
# count FULLY past the edge at 390 px with the repo's fonts — the
|
||||
# "(n/m) doesn't show up on mobile" report).
|
||||
# "(n/m) doesn't show up on mobile" report — the owner's words
|
||||
# before phase 193 turned the part into `N remaining`).
|
||||
assert count["right"] > label["right"] + 1, (
|
||||
f"pre-phase CSS did not clip the count — the pin would be vacuous: "
|
||||
f"label={label} count={count}"
|
||||
|
||||
Reference in New Issue
Block a user