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:
@@ -31,7 +31,8 @@ poll and the phase-64 slow-LLM proxy is GONE from this suite. The
|
||||
"Uploading… → Processing… → restored" lifecycle is made observable the
|
||||
old way (the POST is held in the browser via ``page.route``) plus a
|
||||
held FIRST status GET, so the bare "Processing…" in-run label (no
|
||||
file, no "(n/m)") is asserted across the whole background run.
|
||||
file, no `N remaining` count) is asserted across the whole background
|
||||
run.
|
||||
|
||||
The archives are **built in-test** with Python's ``tarfile`` over
|
||||
``tmp_path`` fixture files carrying markdown sentinels (``ALPHA-…`` /
|
||||
@@ -65,7 +66,7 @@ Contract under test:
|
||||
"Uploading…" while the POST is in flight (held via ``page.route``);
|
||||
at the 202 the "Successfully uploaded — <source>" toast fires; the
|
||||
button then carries the BARE "Processing…" label for the whole
|
||||
background run (no file, no "(n/m)" — the first status GET is held
|
||||
background run (no file, no `N remaining` count — the first status GET is held
|
||||
so the in-run window outlives the 2 s poll) until the status
|
||||
``success`` lands: the result line reads "Uploaded e2e-upload
|
||||
(<stamp>) — press Sync sources to import it." (the phase-154
|
||||
@@ -373,7 +374,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}
|
||||
@@ -449,7 +450,7 @@ def test_upload_registers_without_indexing(
|
||||
button is disabled and reads "Uploading…"; at the 202 the
|
||||
"Successfully uploaded — <source>" toast fires (A2); the button
|
||||
then carries the BARE "Processing…" label for the whole background
|
||||
run (no file, no "(n/m)" — phase 90 A2, proven across a held first
|
||||
run (no file, no `N remaining` count — phase 90 A2, proven across a held first
|
||||
status GET); when the status ``success`` lands the result line
|
||||
reads "Uploaded <source> (<stamp>) — press Sync sources to
|
||||
import it." (A3 — the phase-154 completion stamp), the button
|
||||
@@ -499,7 +500,7 @@ def test_upload_registers_without_indexing(
|
||||
# The run is in flight (or just settled) server-side — and the
|
||||
# label STAYS bare across the whole background run: the first
|
||||
# status GET is held, so the tick that should settle the button is
|
||||
# in flight — the label carries no file and no "(n/m)".
|
||||
# in flight — the label carries no file and no `N remaining` count.
|
||||
time.sleep(2.5) # just past the poll's first tick (the fetch is held)
|
||||
expect(btn).to_have_text("Processing…")
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Phase 129 task 05 E2E (Playwright, mock-only): while a file imports,
|
||||
the sync status (endpoint + button label) shows the file's
|
||||
embedding-chunk position — ``Syncing… chunkprog/big.md (1/2) · chunk
|
||||
14/38`` — with ``chunks_total`` the file's TRUE content-chunk count and
|
||||
embedding-chunk position — ``Syncing… chunkprog/big.md 1 remaining ·
|
||||
chunk 14/38`` — with ``chunks_total`` the file's TRUE content-chunk count and
|
||||
``current_chunk`` the 1-based first chunk of the in-flight
|
||||
token-budgeted batch (batch granularity, assumption A6), climbing 1 →
|
||||
N through the file's embed batches, restarting per file, and cleared
|
||||
@@ -10,7 +10,10 @@ N through the file's embed batches, restarting per file, and cleared
|
||||
The dedicated story suite for ``129_sync_chunk_progress`` (A16 — one
|
||||
Playwright file per phase, run in isolation): the owner's report was
|
||||
that ``Syncing… name/of/file/here (2/3)`` "needs to show what chunk
|
||||
we're on out of how many".
|
||||
we're on out of how many" (the file count read ``(position/total)``
|
||||
then — phase 193 moved that part to the distance to the end,
|
||||
``N remaining``, on the owner's directive; the ``· chunk`` part this
|
||||
suite is about is untouched).
|
||||
|
||||
Run in isolation (DB must be up: ``podman compose up -d db``):
|
||||
|
||||
@@ -60,9 +63,12 @@ the same host) registered as a ``kind=local`` source (the
|
||||
pattern; no git anywhere): ``chunkprog/`` with EXACTLY TWO files, both
|
||||
root-level (no subfolders → the summaries candidate set is just the
|
||||
source root): ``big.md`` (the ~60 KB file above) + ``small.md`` (one
|
||||
tiny chunk). The sorted walk processes ``big.md`` FIRST, so the file
|
||||
counter reads ``(1/2)`` during the big span and ``(2/2)`` during the
|
||||
small one — and the small file's single chunk is the proof that the
|
||||
tiny chunk). The sorted walk processes ``big.md`` FIRST, so the label's
|
||||
phase-193 count part reads ``1 remaining`` during the big span and is
|
||||
HIDDEN during the small one (the final file: N = total - done = 0 — the
|
||||
endpoint's ``files_done``/``files_total`` stay the 1-based 1/2 then 2/2
|
||||
position; only the LABEL counts down) — and the small file's single
|
||||
chunk is the proof that the
|
||||
chunk position RESTARTS per file (the big file ends at its last
|
||||
batch's first index, the small file reports ``(1, 1)``).
|
||||
|
||||
@@ -98,7 +104,8 @@ Test → observable mapping (Playwright Mapping Rule):
|
||||
own 2 s poll): after clicking **Sync sources**, the page's own
|
||||
poll renders the import span's ``#sync-label`` parts —
|
||||
``.sync-label-main`` == ``Syncing… chunkprog/big.md``,
|
||||
``.sync-label-count`` == ``(1/2)``, and ``.sync-label-chunk``
|
||||
``.sync-label-count`` == ``1 remaining`` (phase 193), and
|
||||
``.sync-label-chunk``
|
||||
VISIBLE matching ``· chunk \\d+/\\d+`` with the total == the
|
||||
expected chunk count — and the button ``title`` (and the
|
||||
``#sync-progress`` box — the aria-live announcer, phase 150: the
|
||||
@@ -177,8 +184,9 @@ SLOW_DELAY_S = "1.5"
|
||||
#: the directory's basename is the source name, phase 38).
|
||||
SOURCE = "chunkprog"
|
||||
|
||||
#: The two fixture files (sorted walk: ``big.md`` FIRST → the file
|
||||
#: counter reads (1/2) on the big span, (2/2) on the small one).
|
||||
#: The two fixture files (sorted walk: ``big.md`` FIRST → the label's
|
||||
#: phase-193 count part reads `1 remaining` on the big span and is
|
||||
#: HIDDEN on the small one — the final file, N = total - done = 0).
|
||||
BIG_FILE = f"{SOURCE}/big.md"
|
||||
SMALL_FILE = f"{SOURCE}/small.md"
|
||||
N_FILES = 2
|
||||
@@ -314,12 +322,12 @@ def _dedup_consecutive(values: list[int]) -> list[int]:
|
||||
#: Test 2, step 1 — the in-flight IMPORT label with the VISIBLE chunk
|
||||
#: part: the main part carries "Syncing… chunkprog/big.md" (the
|
||||
#: untruncated A4 text — the big file is the first of the two, so the
|
||||
#: count part is "(1/2)"), the pinned chunk part is visible matching
|
||||
#: phase-193 count part is "1 remaining"), the pinned chunk part is visible matching
|
||||
#: "· chunk n/m" with m == the expected chunk count (the arg), and the
|
||||
#: button title + the #sync-progress box (the aria-live announcer,
|
||||
#: phase 150) carry the UNTRUNCATED concatenation
|
||||
#: (main + " " + count + " " + chunk — A4: the announcer hears the
|
||||
#: whole "Syncing… <file> (1/2) · chunk n/m").
|
||||
#: whole "Syncing… <file> 1 remaining · chunk n/m").
|
||||
_JS_IMPORT_CHUNK_SNAPSHOT = """
|
||||
(n) => {
|
||||
const label = document.querySelector('#sync-label');
|
||||
@@ -330,7 +338,7 @@ _JS_IMPORT_CHUNK_SNAPSHOT = """
|
||||
const chunk = label.querySelector('.sync-label-chunk');
|
||||
if (!main || !count || !chunk || count.hidden || chunk.hidden) return null;
|
||||
if (main.textContent !== 'Syncing… chunkprog/big.md') return null;
|
||||
if (count.textContent !== '(1/2)') return null;
|
||||
if (count.textContent !== '1 remaining') return null;
|
||||
const m = chunk.textContent.match(/^· chunk (\\d+)\\/(\\d+)$/);
|
||||
if (!m || Number(m[2]) !== n) return null;
|
||||
const full = main.textContent + ' ' + count.textContent + ' ' + chunk.textContent;
|
||||
@@ -861,8 +869,8 @@ def test_endpoint_reports_the_chunk_position(
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. UI — the label's chunk part: "Syncing… chunkprog/big.md (1/2) ·
|
||||
# chunk n/m" while a batch is in flight, the untruncated label on
|
||||
# 2. UI — the label's chunk part: "Syncing… chunkprog/big.md
|
||||
# 1 remaining · chunk n/m" while a batch is in flight, the untruncated label on
|
||||
# the title + announcer, the part cleared after the file's span
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -873,7 +881,8 @@ def test_label_shows_the_chunk_part(
|
||||
"""A fresh admin page on ``/sources.html`` clicks **Sync sources**;
|
||||
the page's OWN 2 s poll renders the import span's ``#sync-label``
|
||||
as the phase-128/129 PARTS — the main part
|
||||
``Syncing… chunkprog/big.md``, the pinned count part ``(1/2)``,
|
||||
``Syncing… chunkprog/big.md``, the pinned phase-193 count part
|
||||
``1 remaining`` (the big file is the first of two — N = total − done),
|
||||
and the VISIBLE chunk part matching ``· chunk n/m`` with the total
|
||||
== the expected chunk count (the big file's span is ~45 s under
|
||||
the slow proxy — the poll cannot miss it) — with the button
|
||||
@@ -903,14 +912,14 @@ def test_label_shows_the_chunk_part(
|
||||
_JS_IMPORT_CHUNK_SNAPSHOT, arg=n_chunks, timeout=SETTLE_TIMEOUT_MS
|
||||
).json_value()
|
||||
assert snap["mainText"] == f"Syncing… {SOURCE}/big.md", snap
|
||||
assert snap["countText"] == "(1/2)", snap
|
||||
assert snap["countText"] == "1 remaining", snap
|
||||
m = CHUNK_PART_RE.match(snap["chunkText"])
|
||||
assert m, snap
|
||||
assert int(m.group(2)) == n_chunks, snap # the TRUE chunk count, not a batch count
|
||||
assert int(m.group(1)) >= 1, snap
|
||||
# A4: the untruncated concatenation rides the title AND the
|
||||
# aria-live announcer (the "· chunk" part included).
|
||||
full = f"Syncing… {SOURCE}/big.md (1/2) {snap['chunkText']}"
|
||||
full = f"Syncing… {SOURCE}/big.md 1 remaining {snap['chunkText']}"
|
||||
assert snap["title"] == full and "· chunk" in snap["title"], snap
|
||||
# The box IS the announcer (phase 150) — it carries the same full
|
||||
# string the title does.
|
||||
|
||||
@@ -92,7 +92,9 @@ Test → observable mapping (Playwright Mapping Rule):
|
||||
first ``· image `` label — the two positions are distinct) and
|
||||
every image-span label carries the chunk part WITH IT (the last
|
||||
chunk position persists through the file's image span —
|
||||
"… (1/1) · chunk 19/20 · image 3/4"), its total == the file's
|
||||
"Syncing… imgpass/imgpass.pdf · chunk 19/20 · image 3/4" — no
|
||||
``N remaining`` part, phase 193 hides it on this final file), its
|
||||
total == the file's
|
||||
TRUE chunk count; (c) the button ``title`` + ``aria-label`` carry
|
||||
the SAME full string as the box on every running sample (A4 —
|
||||
asserted per sample, atomically); (d) at settle the box shows the
|
||||
@@ -109,6 +111,13 @@ Test → observable mapping (Playwright Mapping Rule):
|
||||
running sync re-enters with the WHOLE status, phase 98) renders
|
||||
the CURRENT image position with the A4 title/aria — then the run
|
||||
settles to the counts line.
|
||||
|
||||
Phase 193 (task 02) moves the running file-count part to the distance
|
||||
to the end — ``N remaining``, ``N = files_total - files_done`` — and
|
||||
this fixture makes that edge VISIBLE rather than pinned away: its ONE
|
||||
file is also its FINAL file (``N == 0``), so the import span renders NO
|
||||
count part at all (the main part names the last file). The ``· chunk`` /
|
||||
``· image`` positions and the terminal counts line are byte-identical.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -169,8 +178,9 @@ SLOW_DELAY_S = "1.5"
|
||||
#: The local source — the temp directory's basename (``kind=local`` →
|
||||
#: the directory's basename is the source name, phase 38).
|
||||
SOURCE = "imgpass"
|
||||
#: The single fixture file (the source is one file — the count part
|
||||
#: reads "(1/1)" throughout the import phase).
|
||||
#: The single fixture file (the source is one file, so phase 193 hides
|
||||
#: the count part for the whole import span — N = total - done = 0 on
|
||||
#: the only file there is).
|
||||
PDF_NAME = "imgpass.pdf"
|
||||
N_FILES = 1
|
||||
|
||||
@@ -739,8 +749,14 @@ def test_label_advances_through_the_image_span(
|
||||
# sampled) ---
|
||||
assert any(t == "Syncing…" for t in seen), f"no bare prelude label: {seen[:6]}"
|
||||
import_labels = [t for t in seen if t.startswith("Syncing… ")]
|
||||
assert any(f"({N_FILES}/{N_FILES})" in t for t in import_labels), (
|
||||
f"no file count part: {import_labels[:6]}"
|
||||
assert any(f"Syncing… {SOURCE}/{PDF_NAME}" in t for t in import_labels), (
|
||||
f"no live-file label: {import_labels[:6]}"
|
||||
)
|
||||
# Phase 193 (A1): the count part counts DOWN and hides at the FINAL
|
||||
# file — this fixture's one file IS its final file (N = 0), so no
|
||||
# running label carries it (the pre-phase "(1/1)" is gone).
|
||||
assert all(" remaining" not in t for t in import_labels), (
|
||||
f"the final file must render no count part: {import_labels[:6]}"
|
||||
)
|
||||
|
||||
# --- the chunk part appeared EARLIER in the run (the two
|
||||
@@ -783,8 +799,9 @@ def test_label_advances_through_the_image_span(
|
||||
ns = [n for n, _ in positions]
|
||||
assert ns == sorted(ns), f"the climb is backwards somewhere: {positions}"
|
||||
assert N_IMAGES in ns, f"the climb never reached the total: {positions}"
|
||||
# The two positions COEXIST (distinct and both may show — "… (1/1)
|
||||
# · chunk 19/20 · image 3/4"): every image-span label carries the
|
||||
# The two positions COEXIST (distinct and both may show —
|
||||
# "Syncing… imgpass/imgpass.pdf · chunk 19/20 · image 3/4", with no
|
||||
# `N remaining` part on this single-file run — phase 193): every image-span label carries the
|
||||
# chunk part with it (the last chunk position persists through the
|
||||
# file's image span — the per-file reset + the phase transitions
|
||||
# clear it, not the end of the chunk span).
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -4,7 +4,7 @@ the **button is the loading spinner** (``is-running`` hides the label
|
||||
parts; the spinning icon is the whole control) and the bordered text
|
||||
box directly under the button row (``#sync-progress``) carries the
|
||||
**EXACT full untruncated live label** — ``Syncing…
|
||||
<source/relative/path> (n/m) · chunk n/m`` — wrapping, never clipping
|
||||
<source/relative/path> N remaining · chunk n/m`` — wrapping, never clipping
|
||||
(the long ``source/relative/path`` that overflows the pill is the
|
||||
fixture's whole reason to exist). Phase 150 folded the announcer into
|
||||
the box: it is the **single sync text surface AND the sole aria-live
|
||||
@@ -22,6 +22,12 @@ behavioral proof through the REAL sync path: a real ``git clone`` of a
|
||||
local ``file://`` fixture repo, a real import, the slowed mock LLM so
|
||||
the in-flight state is observable.
|
||||
|
||||
Phase 193 (task 02) re-points the file-count part of that label to the
|
||||
distance to the end — ``N remaining`` with ``N = total - done`` — so the
|
||||
long file (the FIRST of the three) renders ``2 remaining`` while the
|
||||
endpoint's ``files_done``/``files_total`` stay the 1-based 1/3 position.
|
||||
The ``· chunk`` part and the terminal counts line are byte-identical.
|
||||
|
||||
Run in isolation (DB must be up: ``podman compose up -d db``; git on PATH —
|
||||
a documented environment prerequisite, phase 28):
|
||||
|
||||
@@ -105,7 +111,7 @@ in one JS task):
|
||||
phase-134 contract unchanged).
|
||||
3. ``test_box_carries_the_full_untruncated_label`` — during the
|
||||
in-flight window of the LONG-path file (poll until the box text is
|
||||
``Syncing… <full long path> (1/3)`` — the path string IN FULL, the
|
||||
``Syncing… <full long path> 2 remaining`` — the path string IN FULL, the
|
||||
pre-phase ellipsis would have truncated it): the box's
|
||||
``scrollWidth <= clientWidth`` AND the text's ``scrollWidth <=
|
||||
clientWidth`` (the wrap engaged — nothing is clipped), the box IS the
|
||||
@@ -114,7 +120,7 @@ in one JS task):
|
||||
box's text too, and the box ink on the panel background clears WCAG
|
||||
2.1 AA (≥ 4.5:1). Then a retry loop over the slow embed window pins
|
||||
the phase-129 chunk part IN THE BOX (``Syncing… <full long path>
|
||||
(1/3) · chunk n/m`` — the part is only present while an embed batch
|
||||
2 remaining · chunk n/m`` — the part is only present while an embed batch
|
||||
is in flight; the proxy's 1.5 s delay makes the ~15 s window ≫ the
|
||||
2 s poll).
|
||||
4. ``test_mobile_box_full_width`` — a fresh page at the house mobile
|
||||
@@ -226,7 +232,8 @@ N_FILES = 3
|
||||
#: The in-flight label's file part — the status endpoint's
|
||||
#: ``current_file`` = ``<source>/<rel>`` (the sync's progress hook),
|
||||
#: verbatim. The long file is the FIRST of the three (the sorted walk —
|
||||
#: ``notes/a-…`` < ``notes/short-…``), so its count part is ``(1/3)``.
|
||||
#: ``notes/a-…`` < ``notes/short-…``), so its phase-193 count part is
|
||||
#: ``2 remaining`` (N = total - done = 3 - 1).
|
||||
LONG_PATH = f"{SOURCE}/{LONG_REL}"
|
||||
|
||||
#: The long file's shape (module docstring): an H1 preamble (one chunk)
|
||||
@@ -370,7 +377,7 @@ _JS_RUNNING_SNAPSHOT = """
|
||||
|
||||
#: Test 3, wait A — the in-flight window of the LONG-path file: the box
|
||||
#: is VISIBLE and its text is the FULL label for the long file —
|
||||
#: "Syncing… <full long path> (1/3)" (+ the optional phase-129
|
||||
#: "Syncing… <full long path> 2 remaining" (+ the optional phase-129
|
||||
#: " · chunk n/m" part while an embed batch is in flight — the
|
||||
#: pre-first-batch frame has none) — the complete
|
||||
# source/relative/path IN the text (the pre-phase ellipsis would have
|
||||
@@ -385,10 +392,10 @@ _JS_LONG_FILE_SNAPSHOT = """
|
||||
const btn = document.querySelector('#sync-btn');
|
||||
if (!box || !textEl || !btn || box.hidden) return null;
|
||||
const t = textEl.textContent;
|
||||
const prefix = 'Syncing… ' + longPath + ' (';
|
||||
const prefix = 'Syncing… ' + longPath + ' ';
|
||||
if (!t.startsWith(prefix)) return null;
|
||||
const tail = t.slice(prefix.length);
|
||||
if (!/^\\d+\\/3\\)( · chunk \\d+\\/\\d+)?$/.test(tail)) return null;
|
||||
if (!/^2 remaining( · chunk \\d+\\/\\d+)?$/.test(tail)) return null;
|
||||
const cs = (el) => getComputedStyle(el);
|
||||
return {
|
||||
boxText: t,
|
||||
@@ -407,8 +414,8 @@ _JS_LONG_FILE_SNAPSHOT = """
|
||||
"""
|
||||
|
||||
#: Test 3, wait B — the phase-129 chunk part IN THE BOX during the slow
|
||||
#: embed window: the box text is "Syncing… <full long path> (1/3) ·
|
||||
#: chunk n/m" (the part is only present while an in-flight file's embed
|
||||
#: embed window: the box text is "Syncing… <full long path>
|
||||
#: 2 remaining · chunk n/m" (the part is only present while an in-flight file's embed
|
||||
#: batch is in flight — the proxy's 1.5 s delay makes the ~15 s
|
||||
#: long-file window ≫ the 2 s poll, so the retry loop resolves
|
||||
#: deterministically).
|
||||
@@ -418,7 +425,7 @@ _JS_LONG_FILE_CHUNK_SNAPSHOT = """
|
||||
const textEl = document.querySelector('#sync-progress-text');
|
||||
if (!box || !textEl || box.hidden) return null;
|
||||
const t = textEl.textContent;
|
||||
if (!t.startsWith('Syncing… ' + longPath + ' (1/3) · chunk ')) return null;
|
||||
if (!t.startsWith('Syncing… ' + longPath + ' 2 remaining · chunk ')) return null;
|
||||
const m = t.match(/· chunk (\\d+)\\/(\\d+)$/);
|
||||
if (!m) return null;
|
||||
return {
|
||||
@@ -453,7 +460,7 @@ _JS_MOBILE_SNAPSHOT = """
|
||||
if (!box || !textEl || !btn || !label || !icon || !container || !head) return null;
|
||||
if (box.hidden) return null;
|
||||
const t = textEl.textContent;
|
||||
if (!t.startsWith('Syncing… ' + longPath + ' (')) return null;
|
||||
if (!t.startsWith('Syncing… ' + longPath + ' 2 remaining')) return null;
|
||||
const cs = (el) => getComputedStyle(el);
|
||||
const rect = (el) => {
|
||||
const b = el.getBoundingClientRect();
|
||||
@@ -998,7 +1005,7 @@ def test_box_carries_the_full_untruncated_label(
|
||||
page: Page, app_url: str, db_ready: None
|
||||
) -> None:
|
||||
"""During the in-flight window of the LONG-path file (poll until the
|
||||
box text is ``Syncing… <full long path> (1/3)`` — the slow proxy
|
||||
box text is ``Syncing… <full long path> 2 remaining`` — the slow proxy
|
||||
makes the ~15 s span deterministic): the box's text contains the
|
||||
FULL label shape with the complete ``source/relative/path`` as a
|
||||
substring (the pre-phase ellipsis would have truncated it — the
|
||||
@@ -1022,7 +1029,7 @@ def test_box_carries_the_full_untruncated_label(
|
||||
|
||||
# The FULL label shape — the complete long path IN the text (the
|
||||
# clipping trigger, untruncated).
|
||||
expected_prefix = f"Syncing… {LONG_PATH} (1/3)"
|
||||
expected_prefix = f"Syncing… {LONG_PATH} 2 remaining"
|
||||
assert snap["boxText"].startswith(expected_prefix), snap["boxText"]
|
||||
assert LONG_PATH in snap["boxText"], snap["boxText"]
|
||||
# The wrap engaged — no horizontal overflow (the whole point of the
|
||||
@@ -1084,7 +1091,9 @@ def test_mobile_box_full_width(page: Page, app_url: str, db_ready: None) -> None
|
||||
|
||||
# The label wraps — the full long path is IN the box's text (the
|
||||
# old mobile-stuffing ellipsis is gone).
|
||||
assert snap["boxText"].startswith(f"Syncing… {LONG_PATH} ("), snap["boxText"]
|
||||
assert snap["boxText"].startswith(
|
||||
f"Syncing… {LONG_PATH} 2 remaining"
|
||||
), snap["boxText"]
|
||||
assert LONG_PATH in snap["boxText"], snap["boxText"]
|
||||
# The wrap engaged — no horizontal clipping at the phone width.
|
||||
assert snap["boxScrollWidth"] <= snap["boxClientWidth"], snap
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -705,8 +705,8 @@ def test_running_upload_line_unchanged(
|
||||
tarball_c: Path,
|
||||
) -> None:
|
||||
"""At the 202 moment the upload run is in flight: the button is
|
||||
the BARE "Processing…" (phase 90, A2 — no file, no "(n/m)"
|
||||
counts, the title cleared) and ``#archive-upload-result`` is
|
||||
the BARE "Processing…" (phase 90, A2 — no file, no `N remaining`
|
||||
count, the title cleared) and ``#archive-upload-result`` is
|
||||
HIDDEN — the ready-for-sync line NEVER renders during the run
|
||||
(the phase-90 contract: the line appears ONLY on settle). The
|
||||
deterministic in-flight window: the 202 handler synchronously
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -26,7 +26,7 @@ check + import), so its background run settles in milliseconds and the
|
||||
racing a timer the suite polls the RAG page's settled sync UI
|
||||
(``#sync-label`` "Synced HH:MM" + the ``#sync-progress`` box's counts
|
||||
line — phase 150: the box is the single sync text surface) with
|
||||
generous timeouts — the live "Syncing… <file> (n/m)" label is the
|
||||
generous timeouts — the live "Syncing… <file> N remaining" label is the
|
||||
``test_sync_upload_progress.py`` suite's subject, not this one's.
|
||||
|
||||
The archive is **built in-test** with Python's ``tarfile``:
|
||||
|
||||
Reference in New Issue
Block a user