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:
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user