phase: 193_sync_remaining_count
Build and Push Containers / build-and-push-app (push) Successful in 2m3s
Build and Push Containers / build-and-push-db (push) Successful in 16s

**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:
2026-10-19 17:30:03 -04:00
parent fb17d15fbe
commit ae080ac7f3
36 changed files with 2111 additions and 174 deletions
+49 -33
View File
@@ -16,7 +16,7 @@ unpack → swap → row upsert continues server-side behind
pattern; the phase-64 key set with ``current_file``/``files_done``/
``files_total`` null/0/0 for the whole run — phase 90 A2), and the
upload's UI processing state is the BARE "Processing…" (no file, no
"(n/m)") until the no-count "Uploaded <name> (<stamp>) — press Sync
`N remaining` count) until the no-count "Uploaded <name> (<stamp>) — press Sync
sources to import it." result line lands (phase 90 A3 — the phase-154
completion stamp). The scan — with its live
file label — is the RAG page's **Sync sources** button's job, and the
@@ -47,6 +47,14 @@ is asserted at BOTH layers the task pins:
the spinner) for the ``Syncing…`` prefix plus a file path (generous
timeout), which the page's own 2 s poll ticks render.
Phase 193 (task 02) moves the running file-count part to the distance
to the end — ``N remaining`` with ``N = total - done`` (the owner's
"how many files are LEFT, not how many files out of the total"), the
upload job sharing the one builder and the shape. The box/title count
pins below therefore make the part optional: it is hidden on the FINAL
file, and this suite's 2 s poll can land exactly there — the pattern
still rejects the pre-phase ``(n/m)`` tail.
The upload archive is built in-test with Python's ``tarfile`` from
**25 small ``.md`` files** (``e2e-prog.tar.gz`` → source
``e2e-prog``); the sync subjects are the uploaded row itself (the new
@@ -66,14 +74,16 @@ Contract under test:
the 202; navigating to ``/sources.html`` shows **zero indexed
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-149 detail box carries the full
then imports the uploaded row with the LIVE "Syncing… <file>
N remaining" label (phase 193 — the count part counts DOWN to the
end; 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
`N remaining` count, no title — proven across a held first status
GET); every
running tick the recorder catches carries a null ``current_file``
and 0/0 counts; the terminal status is ``success`` with the no-count
``{"message": "uploaded"}`` detail and null/0/0 progress; the
@@ -81,7 +91,8 @@ Contract under test:
* **sync live file (A4)**: a multi-file local source; clicking
**Sync sources** on ``/sources.html`` shows "Syncing…" (bare, the
pre-64 click state) in the detail box (phase 149 — the button is
the spinner) then "Syncing… <file> (n/m)" (both layers — the phase-
the spinner) then "Syncing… <file> N remaining" (phase 193; 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
@@ -420,7 +431,7 @@ def _hold_first_status_fetch(page: Page, hold_s: float) -> None:
AFTER the page's boot re-attach fetch, before the submit. The
poll's first tick fires 2 s after the 202; holding its fetch keeps
the button in the in-run state long enough to assert the bare
"Processing…" label (no file, no "(n/m)") across the whole
"Processing…" label (no file, no `N remaining` count) across the whole
background run — phase 90's run settles in milliseconds, so without
the hold the in-run window is only the 2 s pre-tick gap."""
state = {"held": False}
@@ -579,7 +590,7 @@ 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)" detail while the sync imports the uploaded
"Syncing… <file> N remaining" 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)."""
@@ -642,29 +653,32 @@ def test_upload_toast_then_navigate_away(
# 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).
# <source/rel/path> N remaining" (phase 193 — the count part
# counts DOWN and hides on the FINAL file, so the part is optional
# in this wait; the pre-phase "(n/m)" tail can no longer match)
# (+ " · 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 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(counts_line(
re.compile(
rf"^Syncing… {re.escape(UPLOAD_NAME)}/.+\.md \(\d+/{N_FILES}\)"
r"(?: · chunk \d+/\d+)?$"
rf"^Syncing… {re.escape(UPLOAD_NAME)}/.+\.md"
r"(?: [0-9]+ remaining)?(?: · chunk \d+/\d+)?$"
)),
timeout=SETTLE_TIMEOUT_MS,
)
# 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).
# "never clipped" pin now lives in its full string — the
# "N remaining" part is in the text above).
expect(page.locator("#sync-progress")).to_be_visible(
timeout=SETTLE_TIMEOUT_MS
)
expect(btn).to_have_attribute(
"title",
re.compile(
rf"^Syncing… {re.escape(UPLOAD_NAME)}/.+\.md \(\d+/{N_FILES}\)(?: · chunk \d+/\d+)?$"
rf"^Syncing… {re.escape(UPLOAD_NAME)}/.+\.md"
r"(?: [0-9]+ remaining)?(?: · chunk \d+/\d+)?$"
),
timeout=SETTLE_TIMEOUT_MS,
)
@@ -710,7 +724,7 @@ def test_upload_progress_is_bare(
) -> None:
"""Phase 90 A2: the upload's background run has NO file-level
progress. The button shows the BARE "Processing…" for the whole
run (no file, no "(n/m)", no title — proven across a held first
run (no file, no `N remaining` count, no title — proven across a held first
status GET); every running tick the recorder catches carries a
null ``current_file`` and 0/0 counts; the terminal status is
``success`` with the no-count ``{"message": "uploaded"}`` detail
@@ -753,7 +767,7 @@ def test_upload_progress_is_bare(
expect(btn).to_have_attribute("title", "")
# …and it STAYS bare across the whole background run: the first
# status GET is held, so the settling tick is in flight — no file
# and no "(n/m)" can have rendered.
# and no `N remaining` count can have rendered.
time.sleep(2.5)
expect(btn).to_have_text("Processing…")
@@ -799,7 +813,7 @@ def test_sync_live_file_label(
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:
"Syncing… <file> N remaining" 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
@@ -839,22 +853,24 @@ def test_sync_live_file_label(
# 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).
# <source/rel/path> N remaining" (phase 193 — the count part
# counts DOWN and hides on the FINAL file, so the part is optional
# in this wait; the pre-phase "(n/m)" tail can no longer match)
# (+ " · 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 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(counts_line(
re.compile(
rf"^Syncing… {re.escape(SYNC_SOURCE_DIR)}/.+\.md \(\d+/{N_FILES}\)"
r"(?: · chunk \d+/\d+)?$"
rf"^Syncing… {re.escape(SYNC_SOURCE_DIR)}/.+\.md"
r"(?: [0-9]+ remaining)?(?: · chunk \d+/\d+)?$"
)),
timeout=SETTLE_TIMEOUT_MS,
)
# 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).
# "never clipped" pin now lives in its full string — the
# "N remaining" part is in the text above).
expect(page.locator("#sync-progress")).to_be_visible(
timeout=SETTLE_TIMEOUT_MS
)
@@ -862,8 +878,8 @@ def test_sync_live_file_label(
"title",
re.compile(
"^Syncing… "
rf"{re.escape(SYNC_SOURCE_DIR)}/.+\.md \(\d+/{N_FILES}\)"
r"(?: · chunk \d+/\d+)?$"
rf"{re.escape(SYNC_SOURCE_DIR)}/.+\.md"
r"(?: [0-9]+ remaining)?(?: · chunk \d+/\d+)?$"
),
timeout=SETTLE_TIMEOUT_MS,
)