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
@@ -0,0 +1,11 @@
**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/`.
@@ -0,0 +1,150 @@
........................................................................ [ 1%]
........................................................................ [ 3%]
........................................................................ [ 4%]
........................................................................ [ 6%]
........................................................................ [ 7%]
........................................................................ [ 9%]
........................................................................ [ 10%]
........................................................................ [ 12%]
........................................................................ [ 13%]
........................................................................ [ 15%]
........................................................................ [ 16%]
........................................................................ [ 18%]
........................................................................ [ 19%]
........................................................................ [ 21%]
........................................................................ [ 22%]
........................................................................ [ 24%]
........................................................................ [ 25%]
........................................................................ [ 27%]
........................................................................ [ 28%]
........................................................................ [ 30%]
........................................................................ [ 31%]
........................................................................ [ 33%]
........................................................................ [ 34%]
........................................................................ [ 36%]
........................................................................ [ 37%]
........................................................................ [ 39%]
........................................................................ [ 40%]
........................................................................ [ 42%]
........................................................................ [ 43%]
........................................................................ [ 45%]
........................................................................ [ 46%]
........................................................................ [ 48%]
........................................................................ [ 49%]
........................................................................ [ 51%]
........................................................................ [ 52%]
........................................................................ [ 54%]
........................................................................ [ 55%]
........................................................................ [ 57%]
........................................................................ [ 58%]
........................................................................ [ 60%]
........................................................................ [ 61%]
........................................................................ [ 63%]
........................................................................ [ 64%]
........................................................................ [ 66%]
........................................................................ [ 67%]
........................................................................ [ 69%]
........................................................................ [ 70%]
........................................................................ [ 72%]
........................................................................ [ 73%]
........................................................................ [ 75%]
........................................................................ [ 76%]
........................................................................ [ 78%]
........................................................................ [ 79%]
........................................................................ [ 81%]
........................................................................ [ 82%]
........................................................................ [ 84%]
........................................................................ [ 85%]
........................................................................ [ 87%]
........................................................................ [ 88%]
........................................................................ [ 90%]
........................................................................ [ 91%]
........................................................................ [ 93%]
........................................................................ [ 94%]
........................................................................ [ 96%]
........................................................................ [ 97%]
........................................................................ [ 99%]
........................................ [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/admin_settings.py 114 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 301 1 99%
app/api/chat_images.py 52 0 100%
app/api/chats.py 111 0 100%
app/api/config.py 13 0 100%
app/api/doc_drafts.py 99 0 100%
app/api/docs.py 239 1 99%
app/api/git_sources.py 259 0 100%
app/api/health.py 10 0 100%
app/api/source_labels.py 52 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 33 0 100%
app/api/sync.py 231 0 100%
app/api/tokens.py 40 0 100%
app/api/ui_settings.py 39 0 100%
app/config.py 332 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 124 0 100%
app/core/compression.py 59 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 43 0 100%
app/core/errors.py 5 0 100%
app/core/http_cache.py 22 0 100%
app/core/logging.py 13 0 100%
app/core/memory.py 32 0 100%
app/core/rate_limit.py 44 0 100%
app/core/response_budget.py 63 0 100%
app/core/security_headers.py 20 0 100%
app/core/settings_catalog.py 18 0 100%
app/core/settings_migrate.py 32 0 100%
app/core/settings_overlay.py 117 0 100%
app/core/theming.py 45 0 100%
app/core/tokens.py 44 0 100%
app/db.py 22 0 100%
app/main.py 107 0 100%
app/models.py 148 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 501 1 99%
app/rag/archive_upload.py 134 0 100%
app/rag/chunker.py 220 4 98%
app/rag/doc_dates.py 18 0 100%
app/rag/folder_summaries.py 134 0 100%
app/rag/git_sources.py 77 0 100%
app/rag/importer.py 865 3 99%
app/rag/llm.py 253 1 99%
app/rag/md_images.py 63 0 100%
app/rag/overview.py 82 0 100%
app/rag/pdf.py 192 0 100%
app/rag/prompts.py 178 0 100%
app/rag/retriever.py 270 3 99%
app/rag/scaffolding.py 55 0 100%
app/rag/source_labels.py 87 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 58 0 100%
app/rag/svg_text.py 111 0 100%
app/rag/verbatim.py 160 0 100%
app/schemas.py 587 0 100%
--------------------------------------------------
TOTAL 7181 16 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
validation OK
@@ -0,0 +1,16 @@
**Task 01 complete — `fmtSyncLabel` count part is now `N remaining` (D1).**
**Implemented**
- `frontend/assets/sources.js` — `fmtSyncLabel` count: `total > 0 && total - done > 0 ? `${total - done} remaining` : ""` (pre-walk `total > 0` guard kept, hidden at `N == 0`, plain integer, upload shares via the one builder); `fmtSyncLabel` + `fmtSyncPhaseLabel` docstrings updated to the count-down shape (lineage kept; summaries `(n/m)`, `· chunk n/m`, `· image n/m`, terminal `counts_line`, `setSyncLabel`/`#sync-progress` and the server/payload untouched).
- `tests/unit/test_frontend_sync_upload.py` — new marker pins `test_fmt_sync_label_count_part_is_the_n_remaining_part` (exact expression, both guards, no-separator, docstring clauses incl. the hidden-at-final-file edge + upload sharing) and `test_remaining_part_leaves_the_other_parts_byte_identical` (chunk/image/summaries builders + `fmtSyncResult` unchanged); retargeted the `(2/3)` contract/harness pins to `1 remaining`; node harness gained the `import-final` row (count part behaviorally hidden at N == 0) wired into the chunk/image/box/Cancel loops; module docstring + `_doc_above` helper added.
- `README.md` — sync running-label description: "Syncing… <file> (n/m)" → "Syncing… <file> N remaining" with the N = total − done / hidden-at-final-file / upload-sharing clause. `AGENTS.md`: verified no clause names the count part — no change (per phase doc note).
**Gates (all green)**
- `uv run pytest tests/unit/test_frontend_sync_upload.py` → pass (incl. node harness)
- `uv run pytest --cov=app --cov-report=term-missing` → **4792 passed**, `app/` coverage **99%** (>90%)
- `uv run ruff check .` → clean; `uv run pyright` → clean
**Decisions / deviations**
- E2E running-label count pins (`test_sync_label_mobile.py`, `test_sync_upload_progress.py`, `test_sync_chunk_progress.py`, `test_sync_image_progress.py`, `test_sync_progress_box.py`) are deliberately left for **task 02** ("the pinned suites move"), per phase 193 D2 — the project's default suite (`testpaths`: unit+integration) is 100% green.
**Next pending task:** `193_sync_remaining_count/02_update_pinned_e2e.md`
@@ -0,0 +1,150 @@
........................................................................ [ 1%]
........................................................................ [ 3%]
........................................................................ [ 4%]
........................................................................ [ 6%]
........................................................................ [ 7%]
........................................................................ [ 9%]
........................................................................ [ 10%]
........................................................................ [ 12%]
........................................................................ [ 13%]
........................................................................ [ 15%]
........................................................................ [ 16%]
........................................................................ [ 18%]
........................................................................ [ 19%]
........................................................................ [ 21%]
........................................................................ [ 22%]
........................................................................ [ 24%]
........................................................................ [ 25%]
........................................................................ [ 27%]
........................................................................ [ 28%]
........................................................................ [ 30%]
........................................................................ [ 31%]
........................................................................ [ 33%]
........................................................................ [ 34%]
........................................................................ [ 36%]
........................................................................ [ 37%]
........................................................................ [ 39%]
........................................................................ [ 40%]
........................................................................ [ 42%]
........................................................................ [ 43%]
........................................................................ [ 45%]
........................................................................ [ 46%]
........................................................................ [ 48%]
........................................................................ [ 49%]
........................................................................ [ 51%]
........................................................................ [ 52%]
........................................................................ [ 54%]
........................................................................ [ 55%]
........................................................................ [ 57%]
........................................................................ [ 58%]
........................................................................ [ 60%]
........................................................................ [ 61%]
........................................................................ [ 63%]
........................................................................ [ 64%]
........................................................................ [ 66%]
........................................................................ [ 67%]
........................................................................ [ 69%]
........................................................................ [ 70%]
........................................................................ [ 72%]
........................................................................ [ 73%]
........................................................................ [ 75%]
........................................................................ [ 76%]
........................................................................ [ 78%]
........................................................................ [ 79%]
........................................................................ [ 81%]
........................................................................ [ 82%]
........................................................................ [ 84%]
........................................................................ [ 85%]
........................................................................ [ 87%]
........................................................................ [ 88%]
........................................................................ [ 90%]
........................................................................ [ 91%]
........................................................................ [ 93%]
........................................................................ [ 94%]
........................................................................ [ 96%]
........................................................................ [ 97%]
........................................................................ [ 99%]
........................................ [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/admin_settings.py 114 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 301 1 99%
app/api/chat_images.py 52 0 100%
app/api/chats.py 111 0 100%
app/api/config.py 13 0 100%
app/api/doc_drafts.py 99 0 100%
app/api/docs.py 239 1 99%
app/api/git_sources.py 259 0 100%
app/api/health.py 10 0 100%
app/api/source_labels.py 52 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 33 0 100%
app/api/sync.py 231 0 100%
app/api/tokens.py 40 0 100%
app/api/ui_settings.py 39 0 100%
app/config.py 332 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 124 0 100%
app/core/compression.py 59 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 43 0 100%
app/core/errors.py 5 0 100%
app/core/http_cache.py 22 0 100%
app/core/logging.py 13 0 100%
app/core/memory.py 32 0 100%
app/core/rate_limit.py 44 0 100%
app/core/response_budget.py 63 0 100%
app/core/security_headers.py 20 0 100%
app/core/settings_catalog.py 18 0 100%
app/core/settings_migrate.py 32 0 100%
app/core/settings_overlay.py 117 0 100%
app/core/theming.py 45 0 100%
app/core/tokens.py 44 0 100%
app/db.py 22 0 100%
app/main.py 107 0 100%
app/models.py 148 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 501 1 99%
app/rag/archive_upload.py 134 0 100%
app/rag/chunker.py 220 4 98%
app/rag/doc_dates.py 18 0 100%
app/rag/folder_summaries.py 134 0 100%
app/rag/git_sources.py 77 0 100%
app/rag/importer.py 865 3 99%
app/rag/llm.py 253 1 99%
app/rag/md_images.py 63 0 100%
app/rag/overview.py 82 0 100%
app/rag/pdf.py 192 0 100%
app/rag/prompts.py 178 0 100%
app/rag/retriever.py 270 3 99%
app/rag/scaffolding.py 55 0 100%
app/rag/source_labels.py 87 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 58 0 100%
app/rag/svg_text.py 111 0 100%
app/rag/verbatim.py 160 0 100%
app/schemas.py 587 0 100%
--------------------------------------------------
TOTAL 7181 16 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
validation OK
@@ -0,0 +1,22 @@
**Task 02 complete — every running-label count pin now asserts `N remaining`.**
**Implemented (test-only + doc hygiene)**
- `tests/e2e/test_sync_label_mobile.py`: `IMPORT_COUNT_RE`/`IMPORT_FULL_RE` + both in-browser JS regexes → `[1-3] remaining` (4-file fixture; `files_done` is the importer's **1-based current file**, so N = 4−done ∈ {3,2,1} and the last file renders no part — the phase doc's "`done = i − 1`" arithmetic was wrong); `IMPORT_MAIN_RE` untouched.
- `tests/e2e/test_sync_upload_progress.py`: the 4 box/title count regexes → `(?: [0-9]+ remaining)?` (optional — see deviations).
- `tests/e2e/test_sync_chunk_progress.py`: JS `count.textContent`, `countText`, the composed A4 string → `1 remaining` (big.md = file 1 of 2).
- `tests/e2e/test_sync_progress_box.py`: the 3 JS waits + 2 Python asserts → `2 remaining` (long file = file 1 of 3).
- `tests/e2e/test_sync_image_progress.py`: the single-file fixture's `(1/1)` pin became the **hidden-at-final-file** arm (`no running label carries " remaining"` + the live-file label still pinned).
- Phase-193 lineage sentence added to each retargeted module docstring; stale `(n/m)` prose fixed in `test_archive_upload_sources.py`, `test_sync_status_freshness.py`, `test_upload_no_scan.py`, `tests/unit/test_sync_button.py`, `tests/unit/test_frontend_sync_upload.py`.
- **Untouched and green**: the `· chunk n/m` / `· image n/m` parts, the summaries `(n/3)` folder position, and every `counts_line` terminal pin (`git_freeze`, `session_reset`, `fast_path_deferred_content`).
- Pin list closed: the remaining hits of the phase's grep are bare `"Syncing…"` asserts and the owner's quoted reports.
**Gates**
- `uv run pytest` → **4792 passed**; `uv run pytest --cov=app --cov-report=term-missing` → **99%** (>90%).
- `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors.
- E2E (`--no-cov`, isolated): `test_sync_label_mobile.py + test_sync_upload_progress.py` 8 passed (the task's combined command); `test_sync_progress_box.py` 8; `test_sync_chunk_progress.py + test_sync_image_progress.py` 4; `+ test_sync_summary_visibility.py` 7; `test_sync_git_freeze.py` 2; `test_archive_upload_sources.py + test_sync_session_reset.py` 9; `test_fast_path_deferred_content.py + test_sync_button.py + test_sync_peak_memory_visible.py` 11; `test_sync_status_freshness.py + test_upload_no_scan.py` in the summary batch.
**Docs**: README's sync running-label paragraph — removed the stray `— and —` the task-01 edit left and re-flowed the wrap; AGENTS.md has no count-part clause (no doc impact for it; task 03 re-verifies).
**Deviations**: (1) the phase's D2 range `[1-4] remaining` is impossible — `files_done` includes the in-flight file, so the count-bearing frames are `[1-3]` (kept **mandatory** where the pin's whole point is "the count is in the text"); (2) the upload suite's 25-file box/title pins make the part **optional**, because its 2 s poll can land exactly on the final file where A1 hides the part — the pattern still rejects a pre-phase `(n/m)` tail.
**Next pending task**: `.agents/phases/todo/193_sync_remaining_count/03_e2e.md`.
@@ -0,0 +1,150 @@
........................................................................ [ 1%]
........................................................................ [ 3%]
........................................................................ [ 4%]
........................................................................ [ 6%]
........................................................................ [ 7%]
........................................................................ [ 9%]
........................................................................ [ 10%]
........................................................................ [ 12%]
........................................................................ [ 13%]
........................................................................ [ 15%]
........................................................................ [ 16%]
........................................................................ [ 18%]
........................................................................ [ 19%]
........................................................................ [ 21%]
........................................................................ [ 22%]
........................................................................ [ 24%]
........................................................................ [ 25%]
........................................................................ [ 27%]
........................................................................ [ 28%]
........................................................................ [ 30%]
........................................................................ [ 31%]
........................................................................ [ 33%]
........................................................................ [ 34%]
........................................................................ [ 36%]
........................................................................ [ 37%]
........................................................................ [ 39%]
........................................................................ [ 40%]
........................................................................ [ 42%]
........................................................................ [ 43%]
........................................................................ [ 45%]
........................................................................ [ 46%]
........................................................................ [ 48%]
........................................................................ [ 49%]
........................................................................ [ 51%]
........................................................................ [ 52%]
........................................................................ [ 54%]
........................................................................ [ 55%]
........................................................................ [ 57%]
........................................................................ [ 58%]
........................................................................ [ 60%]
........................................................................ [ 61%]
........................................................................ [ 63%]
........................................................................ [ 64%]
........................................................................ [ 66%]
........................................................................ [ 67%]
........................................................................ [ 69%]
........................................................................ [ 70%]
........................................................................ [ 72%]
........................................................................ [ 73%]
........................................................................ [ 75%]
........................................................................ [ 76%]
........................................................................ [ 78%]
........................................................................ [ 79%]
........................................................................ [ 81%]
........................................................................ [ 82%]
........................................................................ [ 84%]
........................................................................ [ 85%]
........................................................................ [ 87%]
........................................................................ [ 88%]
........................................................................ [ 90%]
........................................................................ [ 91%]
........................................................................ [ 93%]
........................................................................ [ 94%]
........................................................................ [ 96%]
........................................................................ [ 97%]
........................................................................ [ 99%]
........................................ [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/admin_settings.py 114 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 301 1 99%
app/api/chat_images.py 52 0 100%
app/api/chats.py 111 0 100%
app/api/config.py 13 0 100%
app/api/doc_drafts.py 99 0 100%
app/api/docs.py 239 1 99%
app/api/git_sources.py 259 0 100%
app/api/health.py 10 0 100%
app/api/source_labels.py 52 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 33 0 100%
app/api/sync.py 231 0 100%
app/api/tokens.py 40 0 100%
app/api/ui_settings.py 39 0 100%
app/config.py 332 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 124 0 100%
app/core/compression.py 59 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 43 0 100%
app/core/errors.py 5 0 100%
app/core/http_cache.py 22 0 100%
app/core/logging.py 13 0 100%
app/core/memory.py 32 0 100%
app/core/rate_limit.py 44 0 100%
app/core/response_budget.py 63 0 100%
app/core/security_headers.py 20 0 100%
app/core/settings_catalog.py 18 0 100%
app/core/settings_migrate.py 32 0 100%
app/core/settings_overlay.py 117 0 100%
app/core/theming.py 45 0 100%
app/core/tokens.py 44 0 100%
app/db.py 22 0 100%
app/main.py 107 0 100%
app/models.py 148 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 501 1 99%
app/rag/archive_upload.py 134 0 100%
app/rag/chunker.py 220 4 98%
app/rag/doc_dates.py 18 0 100%
app/rag/folder_summaries.py 134 0 100%
app/rag/git_sources.py 77 0 100%
app/rag/importer.py 865 3 99%
app/rag/llm.py 253 1 99%
app/rag/md_images.py 63 0 100%
app/rag/overview.py 82 0 100%
app/rag/pdf.py 192 0 100%
app/rag/prompts.py 178 0 100%
app/rag/retriever.py 270 3 99%
app/rag/scaffolding.py 55 0 100%
app/rag/source_labels.py 87 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 58 0 100%
app/rag/svg_text.py 111 0 100%
app/rag/verbatim.py 160 0 100%
app/schemas.py 587 0 100%
--------------------------------------------------
TOTAL 7181 16 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
validation OK
@@ -0,0 +1,15 @@
**Task 03 complete — phase 193's own E2E suite is green in isolation; all gates green.**
**Implemented**
- `tests/e2e/test_sync_remaining_count.py` (NEW): a real `file://` git fixture (5 docs, first + last multi-chunk) + the slow-LLM proxy, with a MutationObserver that records EVERY value the one label writer lands (`#sync-progress-text`, button `title`/`aria-label`, `.sync-label-count` visibility). Arms: **(a)** the count-down — grammar `^Syncing… <file> [1-4] remaining(?: · chunk n/m)?$`, rendered `N`s strictly decreasing 4→3→2→1, never 0/≥total, chunk part coexisting (its `m` = the file's real `chunk_document` count), NO count part once the final file is named and never returning, the overview + summaries `(n/m)` phases intact, the terminal `counts_line("5 added")` byte-identical, plus the cross-layer invariant that every rendered `N` equals `files_total − files_done` of the untouched payload; **(c)** the pre-walk `phase:null`/`files_total:0` → bare `Syncing…` and the hidden-at-final edge, pinned on served bodies of the endpoint's unchanged key set; **(b)** the upload arm through the same builder (`Importing <file> N remaining`, hidden at its final file).
- Docs pass: the stale `(n/m)` running-label examples became `N remaining` in `frontend/assets/sources.js` (5 header/comment sites), `frontend/assets/styles.css`, `frontend/index.html`, `frontend/assets/git-sources.js`. `README.md` already carried the phase-193 label (task 01); `AGENTS.md` has no clause naming the running count part — unchanged.
**Gates (final state)** `uv run pytest --cov=app --cov-report=term-missing` → exit 0, `app/` **99%**; `tests/e2e/test_sync_remaining_count.py -v --no-cov` → 3 passed in isolation (twice); regression `test_sync_label_mobile + test_sync_upload_progress + test_sync_chunk_progress + test_sync_summary_visibility` → 13 passed; `ruff check .` clean; `pyright` 0 errors.
**Decisions / deviations**
1. A2's upload arm is not observable live: since phase 90 the upload status answers `null/0/0` for the whole run (asserted against the live endpoint inside that test), so the shared *shape* is pinned through a served status body (the house `page.route` pattern) — the client's `total > 0` / `total − done > 0` guards are what is under test, the server stays untouched.
2. The upload frame can never carry `· chunk n/m` (that branch receives no status object — phase 129/152), so the coexistence pin sits in the sync arms and the upload test asserts the frame stays chunk-free.
3. Count-bearing frames are `1 … N−1` (`files_done` is the 1-based in-flight file) — task 02's finding, kept here.
4. No git commands run and the phase directory was not moved, per the harness instruction — commit + `todo/ → complete/` left to the harness.
**Next task:** none pending in `193_sync_remaining_count` (03 is its last).
@@ -0,0 +1,150 @@
........................................................................ [ 1%]
........................................................................ [ 3%]
........................................................................ [ 4%]
........................................................................ [ 6%]
........................................................................ [ 7%]
........................................................................ [ 9%]
........................................................................ [ 10%]
........................................................................ [ 12%]
........................................................................ [ 13%]
........................................................................ [ 15%]
........................................................................ [ 16%]
........................................................................ [ 18%]
........................................................................ [ 19%]
........................................................................ [ 21%]
........................................................................ [ 22%]
........................................................................ [ 24%]
........................................................................ [ 25%]
........................................................................ [ 27%]
........................................................................ [ 28%]
........................................................................ [ 30%]
........................................................................ [ 31%]
........................................................................ [ 33%]
........................................................................ [ 34%]
........................................................................ [ 36%]
........................................................................ [ 37%]
........................................................................ [ 39%]
........................................................................ [ 40%]
........................................................................ [ 42%]
........................................................................ [ 43%]
........................................................................ [ 45%]
........................................................................ [ 46%]
........................................................................ [ 48%]
........................................................................ [ 49%]
........................................................................ [ 51%]
........................................................................ [ 52%]
........................................................................ [ 54%]
........................................................................ [ 55%]
........................................................................ [ 57%]
........................................................................ [ 58%]
........................................................................ [ 60%]
........................................................................ [ 61%]
........................................................................ [ 63%]
........................................................................ [ 64%]
........................................................................ [ 66%]
........................................................................ [ 67%]
........................................................................ [ 69%]
........................................................................ [ 70%]
........................................................................ [ 72%]
........................................................................ [ 73%]
........................................................................ [ 75%]
........................................................................ [ 76%]
........................................................................ [ 78%]
........................................................................ [ 79%]
........................................................................ [ 81%]
........................................................................ [ 82%]
........................................................................ [ 84%]
........................................................................ [ 85%]
........................................................................ [ 87%]
........................................................................ [ 88%]
........................................................................ [ 90%]
........................................................................ [ 91%]
........................................................................ [ 93%]
........................................................................ [ 94%]
........................................................................ [ 96%]
........................................................................ [ 97%]
........................................................................ [ 99%]
........................................ [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/admin_settings.py 114 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 301 1 99%
app/api/chat_images.py 52 0 100%
app/api/chats.py 111 0 100%
app/api/config.py 13 0 100%
app/api/doc_drafts.py 99 0 100%
app/api/docs.py 239 1 99%
app/api/git_sources.py 259 0 100%
app/api/health.py 10 0 100%
app/api/source_labels.py 52 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 33 0 100%
app/api/sync.py 231 0 100%
app/api/tokens.py 40 0 100%
app/api/ui_settings.py 39 0 100%
app/config.py 332 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 124 0 100%
app/core/compression.py 59 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 43 0 100%
app/core/errors.py 5 0 100%
app/core/http_cache.py 22 0 100%
app/core/logging.py 13 0 100%
app/core/memory.py 32 0 100%
app/core/rate_limit.py 44 0 100%
app/core/response_budget.py 63 0 100%
app/core/security_headers.py 20 0 100%
app/core/settings_catalog.py 18 0 100%
app/core/settings_migrate.py 32 0 100%
app/core/settings_overlay.py 117 0 100%
app/core/theming.py 45 0 100%
app/core/tokens.py 44 0 100%
app/db.py 22 0 100%
app/main.py 107 0 100%
app/models.py 148 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 501 1 99%
app/rag/archive_upload.py 134 0 100%
app/rag/chunker.py 220 4 98%
app/rag/doc_dates.py 18 0 100%
app/rag/folder_summaries.py 134 0 100%
app/rag/git_sources.py 77 0 100%
app/rag/importer.py 865 3 99%
app/rag/llm.py 253 1 99%
app/rag/md_images.py 63 0 100%
app/rag/overview.py 82 0 100%
app/rag/pdf.py 192 0 100%
app/rag/prompts.py 178 0 100%
app/rag/retriever.py 270 3 99%
app/rag/scaffolding.py 55 0 100%
app/rag/source_labels.py 87 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 58 0 100%
app/rag/svg_text.py 111 0 100%
app/rag/verbatim.py 160 0 100%
app/schemas.py 587 0 100%
--------------------------------------------------
TOTAL 7181 16 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
validation OK
+17 -11
View File
@@ -727,12 +727,17 @@ only) runs the whole source refresh in one click:
- States: while polling `GET /api/sync/status` every 2 s, the button - States: while polling `GET /api/sync/status` every 2 s, the button
becomes the spinner (icon only — its label parts are hidden while a becomes the spinner (icon only — its label parts are hidden while a
run is in flight, and its accessible name is the full live label), run is in flight, and its accessible name is the full live label),
and the full live detail — "Syncing… <file> (n/m)" during the import and the full live detail — "Syncing… <file> N remaining" during the
phase (the live file, its `(n/m)` file position, and — while a file's import phase (the live file, its `N remaining` file count — N =
embed batch is in flight — the pinned `· chunk n/m` embed position, total − done, shown while N ≥ 1 and hidden at the final file, since
batch granularity: the first chunk of the in-flight batch over the the owner reads the distance to the end better than the position in
file's total content chunks; and — while a PDF's images are being the whole; the upload's `Importing` label shares the ONE builder and
described — the `· image n/m` vision position: the 1-based image the same shape, though since phase 90 that run reports no counts, so
today it renders bare; and — while a file's embed batch is in flight
— the pinned `· chunk n/m` embed position, batch granularity: the
first chunk of the in-flight batch over the file's total content
chunks; and — while a PDF's images are being described — the
`· image n/m` vision position: the 1-based image
about to be described over the file's total images, ONE continuous about to be described over the file's total images, ONE continuous
counter across the PDF's three vision spans, so a long image pass counter across the PDF's three vision spans, so a long image pass
visibly climbs instead of sitting frozen on the last chunk visibly climbs instead of sitting frozen on the last chunk
@@ -2001,11 +2006,12 @@ to the OPEN document through the ONE shared brand pass (`assets/brand.js`'s
header brand, the footer line and the chat composer placeholder update on the header brand, the footer line and the chat composer placeholder update on the
click, no reload — including a rename of an app whose old name is already on click, no reload — including a rename of an app whose old name is already on
the page. The re-apply runs only when one of those three keys was involved (the the page. The re-apply runs only when one of those three keys was involved (the
other ~72 rows cost no extra request), never stacks a second fetch while one is other 83 rows of the panel's 86-row catalogue cost no extra request), never
in flight, and a failed one leaves the page exactly as it showed — the save's stacks a second fetch while one is in flight, and a failed one leaves the page
own status or error line stays the feedback. The Theme tab runs the same pass exactly as it showed — the save's own status or error line stays the feedback.
after its Save and its Reset; it no longer edits the strings, so that read is The Theme tab runs the same pass after its Save and its Reset; it no longer
what keeps an open tab honest about a name the panel changed meanwhile. edits the strings, so that read is what keeps an open tab honest about a name
the panel changed meanwhile.
The whole contract — the incident regression (a color save no longer shadows a The whole contract — the incident regression (a color save no longer shadows a
later rename) and the live repaint of title / brand / footer / placeholder — is later rename) and the live repaint of title / brand / footer / placeholder — is
pinned in a real browser by `tests/e2e/test_app_name_settings_fix.py` (run in pinned in a real browser by `tests/e2e/test_app_name_settings_fix.py` (run in
+3 -3
View File
@@ -73,7 +73,7 @@
* disabled, title cleared) plus startUploadPolling(): a 2 s poll * disabled, title cleared) plus startUploadPolling(): a 2 s poll
* of GET /api/git-sources/upload/status that renders the bare * of GET /api/git-sources/upload/status that renders the bare
* "Processing…" label for the WHOLE run (phase 90, A2 — the run * "Processing…" label for the WHOLE run (phase 90, A2 — the run
* is unpack + register only: no file, no "(n/m)" counts, no * is unpack + register only: no file, no "N remaining" count, no
* title) and settles it: success → the ready-for-sync line * title) and settles it: success → the ready-for-sync line
* ("Uploaded <name> (<stamp>) — press Sync sources to import * ("Uploaded <name> (<stamp>) — press Sync sources to import
* it." — the phase-154 completion stamp, fmtUploadStamp off the * it." — the phase-154 completion stamp, fmtUploadStamp off the
@@ -1651,7 +1651,7 @@ export async function mount(root) {
* title cleared) + startUploadPolling() own it — a 2 s poll of * title cleared) + startUploadPolling() own it — a 2 s poll of
* GET /api/git-sources/upload/status that renders the bare * GET /api/git-sources/upload/status that renders the bare
* "Processing…" label for the WHOLE run (phase 90, A2 — the unpack * "Processing…" label for the WHOLE run (phase 90, A2 — the unpack
* has no file-level progress: no file, no "(n/m)" counts, no title) * has no file-level progress: no file, no "N remaining" count, no title)
* and settles it: success → the ready-for-sync line ("Uploaded * and settles it: success → the ready-for-sync line ("Uploaded
* <name> (<stamp>) — press Sync sources to import it." — the * <name> (<stamp>) — press Sync sources to import it." — the
* phase-154 completion stamp, fmtUploadStamp off the status's * phase-154 completion stamp, fmtUploadStamp off the status's
@@ -1786,7 +1786,7 @@ export async function mount(root) {
* poll OWNS the button). Each tick fetches * poll OWNS the button). Each tick fetches
* GET /api/git-sources/upload/status: running → the bare * GET /api/git-sources/upload/status: running → the bare
* "Processing…" label for the whole run (phase 90, A2 — the unpack * "Processing…" label for the whole run (phase 90, A2 — the unpack
* has no file-level progress: no file, no "(n/m)" counts, the title * has no file-level progress: no file, no "N remaining" count, the title
* stays clear) + reschedule; success → stop + the ready-for-sync * stays clear) + reschedule; success → stop + the ready-for-sync
* result line + the announcement + the row reload (NO toast — it * result line + the announcement + the row reload (NO toast — it
* fired at the 202, A2); failed → stop + the sanitized server error * fired at the 202, A2); failed → stop + the sanitized server error
+33 -14
View File
@@ -160,7 +160,8 @@
* <source/folder> (n/m)" (the folder part omitted while * <source/folder> (n/m)" (the folder part omitted while
* current_summary is null — the phase's first poll), anything else * current_summary is null — the phase's first poll), anything else
* (the null prelude, "import") → today's "Syncing… <file> (n/m)" * (the null prelude, "import") → today's "Syncing… <file> (n/m)"
* byte-identical. The UPLOAD job (its status has no phase) keeps the * byte-identical (the count part's TEXT is `N remaining` — the distance
* to the end — since phase 193). The UPLOAD job (its status has no phase) keeps the
* bare label. The untruncated label rides the button title (now set * bare label. The untruncated label rides the button title (now set
* for EVERY running label, not just file labels) + #sync-progress as * for EVERY running label, not just file labels) + #sync-progress as
* before (A4). * before (A4).
@@ -309,7 +310,8 @@
* Phase 128 (task 01) — the sync label becomes PARTS: the running * Phase 128 (task 01) — the sync label becomes PARTS: the running
* label's string is split into a shrinks-to-fit MAIN part (prefix + * label's string is split into a shrinks-to-fit MAIN part (prefix +
* the file/folder path — the part CSS may ellipsize) and a pinned * the file/folder path — the part CSS may ellipsize) and a pinned
* COUNT part ("(n/m)" — the position, NEVER clipped), so on a phone * COUNT part ("(n/m)" — the position, NEVER clipped — the text is
* `N remaining` since phase 193), so on a phone
* the file path ellipsizes to fit while the count stays on-screen * the file path ellipsizes to fit while the count stays on-screen
* (the phase-64 bug: with max-width lifted, the inline-block sized to * (the phase-64 bug: with max-width lifted, the inline-block sized to
* its full content and the count ran off-screen). #sync-label now * its full content and the count ran off-screen). #sync-label now
@@ -332,7 +334,8 @@
* the initSyncButton re-attach — goes through setSyncLabel(main) * the initSyncButton re-attach — goes through setSyncLabel(main)
* (count hidden). The full-string shapes are byte-identical to the * (count hidden). The full-string shapes are byte-identical to the
* pre-task labels ("Syncing…", "Syncing… <file>", * pre-task labels ("Syncing…", "Syncing… <file>",
* "Syncing… <file> (n/m)", "Importing", "Writing KB overview…", * "Syncing… <file> (n/m)" — `N remaining` since phase 193, "Importing",
* "Writing KB overview…",
* "Summarizing folders… <folder> (n/m)"); textContent only — no HTML * "Summarizing folders… <folder> (n/m)"); textContent only — no HTML
* strings (the house rule). * strings (the house rule).
* *
@@ -747,7 +750,7 @@ export async function mount(root) {
* tree, in order (startSyncPolling): * tree, in order (startSyncPolling):
* 1. sync running → the phase-aware label (Phase 98 task 02, D2) * 1. sync running → the phase-aware label (Phase 98 task 02, D2)
* — the null-phase prelude + "import" → * — the null-phase prelude + "import" →
* "Syncing… <file> (n/m)" — bare "Syncing…" * "Syncing… <file> N remaining" — bare "Syncing…"
* until the import's first file (clone/pull, * until the import's first file (clone/pull,
* A4, byte-identical); "overview" → "Writing KB * A4, byte-identical); "overview" → "Writing KB
* overview…"; "summaries" → "Summarizing * overview…"; "summaries" → "Summarizing
@@ -813,7 +816,8 @@ export async function mount(root) {
* Elements: #sync-btn (the button), #sync-label (the text — * Elements: #sync-btn (the button), #sync-label (the text —
* Phase 128 (task 01): the PARTS — .sync-label-main, the * Phase 128 (task 01): the PARTS — .sync-label-main, the
* shrinks-to-fit prefix + file/folder, + .sync-label-count, the * shrinks-to-fit prefix + file/folder, + .sync-label-count, the
* pinned "(n/m)" position (hidden when absent); phase 129 (task * pinned count — the phase-193 "N remaining" (hidden when absent);
* phase 129 (task
* 04): + .sync-label-chunk, the pinned "· chunk n/m" embed-batch * 04): + .sync-label-chunk, the pinned "· chunk n/m" embed-batch
* position of the in-flight file (hidden in every state but the * position of the in-flight file (hidden in every state but the
* import phase after the file's first batch); phase 152 (task 03): * import phase after the file's first batch); phase 152 (task 03):
@@ -917,7 +921,7 @@ export async function mount(root) {
* mainText lands in the MAIN part (.sync-label-main — the * mainText lands in the MAIN part (.sync-label-main — the
* shrinks-to-fit prefix + file/folder, the part CSS ellipsizes); * shrinks-to-fit prefix + file/folder, the part CSS ellipsizes);
* countText lands in the COUNT part (.sync-label-count — the pinned * countText lands in the COUNT part (.sync-label-count — the pinned
* "(n/m)" position, NEVER clipped); chunkText (phase 129) lands in * phase-193 "N remaining", NEVER clipped); chunkText (phase 129) lands in
* the CHUNK part (.sync-label-chunk — the pinned "· chunk n/m" * the CHUNK part (.sync-label-chunk — the pinned "· chunk n/m"
* embed-batch position of the in-flight file); imageText (phase * embed-batch position of the in-flight file); imageText (phase
* 152) is the in-flight PDF's "· image n/m" vision-image position — * 152) is the in-flight PDF's "· image n/m" vision-image position —
@@ -1034,9 +1038,20 @@ export async function mount(root) {
* BARE prefix shows during the clone/pull (sync), before any file * BARE prefix shows during the clone/pull (sync), before any file
* is indexed. Phase 90: the upload run is unpack + register only * is indexed. Phase 90: the upload run is unpack + register only
* (no scan), so its status never carries a file or counts — the * (no scan), so its status never carries a file or counts — the
* "Importing" label is always the bare one. The counts — the PINNED * "Importing" label is always the bare one — and when an upload
* count part, never clipped (Phase 128) — appear only once the * status DOES carry counts the upload job shares this builder and
* import has started (total > 0). The builders return the PARTS * the shape (phase 193, A2: the upload's files_total is the
* pre-walk count, the phase-98 contract — the same subtraction
* semantics). The count — the PINNED count part, never clipped
* (Phase 128) — is the phase-193 COUNT-DOWN part (the owner's
* "how many files are left, not how many files out of the
* total"): `N remaining` with N = total - done, shown while
* N >= 1 and HIDDEN at the final file (N == 0 — the main part
* names it; a "0 remaining" next to the last file's name is the
* stale-looking edge); plain integer, no thousands separator. The
* total > 0 guard stays (the phase-64 pre-walk contract — no count
* part before the walk knows the total). The builders return the
* PARTS
* ({ main, count }); setSyncLabel writes them (the untruncated full * ({ main, count }); setSyncLabel writes them (the untruncated full
* string — main + " " + count — rides the button title + * string — main + " " + count — rides the button title +
* #sync-progress, A4). Phase 98 (task 02): the SYNC job's * #sync-progress, A4). Phase 98 (task 02): the SYNC job's
@@ -1053,7 +1068,7 @@ export async function mount(root) {
function fmtSyncLabel(kind, currentFile, done, total) { function fmtSyncLabel(kind, currentFile, done, total) {
const prefix = kind === "upload" ? "Importing" : "Syncing…"; const prefix = kind === "upload" ? "Importing" : "Syncing…";
const main = currentFile ? `${prefix} ${currentFile}` : prefix; const main = currentFile ? `${prefix} ${currentFile}` : prefix;
const count = total > 0 ? `(${done}/${total})` : ""; const count = total > 0 && total - done > 0 ? `${total - done} remaining` : "";
return { main, count }; return { main, count };
} }
@@ -1069,8 +1084,11 @@ export async function mount(root) {
* empty (the summaries phase's own (n/m) is its position — the * empty (the summaries phase's own (n/m) is its position — the
* status's chunk + image fields are cleared at this transition); * status's chunk + image fields are cleared at this transition);
* anything else (the null prelude, "import") → the phase-64 * anything else (the null prelude, "import") → the phase-64
* fmtSyncLabel parts, byte-identical (same builder, same fields, * fmtSyncLabel parts (same builder, same fields, same order —
* same order) + the phase-129 CHUNK part: "· chunk n/m" ONLY while * since phase 193 its count part reads `N remaining`, N =
* total - done, hidden at the final file; the summaries (n/m)
* folder position and the chunk/image parts below stay as they
* are) + the phase-129 CHUNK part: "· chunk n/m" ONLY while
* chunks_total > 0 (a file's embed batch is in flight — n the * chunks_total > 0 (a file's embed batch is in flight — n the
* 1-based first chunk of the batch, m the file's total content * 1-based first chunk of the batch, m the file's total content
* chunks, batch granularity), empty before the first batch (0/0) so * chunks, batch granularity), empty before the first batch (0/0) so
@@ -1099,8 +1117,9 @@ export async function mount(root) {
image: "", // the image position belongs to the import phase only image: "", // the image position belongs to the import phase only
}; };
} }
// null phase (the prelude) or "import" — today's byte-identical // null phase (the prelude) or "import" — the phase-64 label
// sync label (the phase-64 contract, unchanged) + the phase-129 // shape (since phase 193 its count part reads `N remaining`) +
// the phase-129
// chunk part (hidden before the in-flight file's first batch) + // chunk part (hidden before the in-flight file's first batch) +
// the phase-152 image part (hidden before the in-flight PDF's // the phase-152 image part (hidden before the in-flight PDF's
// first vision describe call). // first vision describe call).
+6 -4
View File
@@ -2898,14 +2898,16 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.sync-btn .sync-icon.is-spinning { animation: none; } .sync-btn .sync-icon.is-spinning { animation: none; }
} }
/* Phase 64: the live-file label ("Syncing… <file> (n/m)" / "Importing /* Phase 64: the live-file label ("Syncing… <file> N remaining" /
<file> (n/m)") — a long source/relative/path ellipsizes inside the "Importing <file> N remaining" — phase 193: the file count is the
distance to the end, `total - done`, hidden on the final file) — a
long source/relative/path ellipsizes inside the
pill; the full path lives in the button title + the #sync-progress pill; the full path lives in the button title + the #sync-progress
box (the aria-live announcer — phase 150: the SOLE sync text box (the aria-live announcer — phase 150: the SOLE sync text
surface). Phase 128 (task 02): the label is a FLEX ROW surface). Phase 128 (task 02): the label is a FLEX ROW
of parts — .sync-label-main (the prefix + file/folder: min-width of parts — .sync-label-main (the prefix + file/folder: min-width
0, the shrinkable item whose ellipsis engages) + .sync-label-count 0, the shrinkable item whose ellipsis engages) + .sync-label-count
(the pinned "(n/m)": flex 0 0 auto, NEVER clipped — the (the pinned "N remaining": flex 0 0 auto, NEVER clipped — the
.sync-progress-text mono language, a position reads as data). Phase 129 .sync-progress-text mono language, a position reads as data). Phase 129
(task 04): a THIRD pinned part — .sync-label-chunk (the "· chunk (task 04): a THIRD pinned part — .sync-label-chunk (the "· chunk
n/m" embed-batch position of the in-flight file, the same pinned n/m" embed-batch position of the in-flight file, the same pinned
@@ -2925,7 +2927,7 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
white-space: nowrap; white-space: nowrap;
} }
.sync-label-count { .sync-label-count {
flex: 0 0 auto; /* NEVER clipped — the (n/m) position is the point */ flex: 0 0 auto; /* NEVER clipped — the N remaining count is the point */
white-space: nowrap; white-space: nowrap;
font-family: var(--mono); /* the .sync-progress-text mono language (position reads as data) */ font-family: var(--mono); /* the .sync-progress-text mono language (position reads as data) */
} }
+4 -2
View File
@@ -490,7 +490,9 @@
</p> </p>
</div> </div>
<!-- Phase 149 (task 01): the sync detail box — the full UNTRUNCATED live <!-- Phase 149 (task 01): the sync detail box — the full UNTRUNCATED live
label ("Syncing… <source/rel/path> (n/m) · chunk n/m" — phase 152 label ("Syncing… <source/rel/path> N remaining · chunk n/m" —
phase 193: the file count counts DOWN to the end and hides on
the last file; phase 152
appends "· image n/m" while a PDF's vision span is in flight) appends "· image n/m" while a PDF's vision span is in flight)
in a bordered panel under the button row. Phase 150 (task 01) folded the in a bordered panel under the button row. Phase 150 (task 01) folded the
announcer in: the previous live-region span beside it is retired, announcer in: the previous live-region span beside it is retired,
@@ -774,7 +776,7 @@
inline. Phase 90 (owner-locked A1/A2/A3): the background inline. Phase 90 (owner-locked A1/A2/A3): the background
run is UNPACK + REGISTER ONLY — no model check, no import, run is UNPACK + REGISTER ONLY — no model check, no import,
no overview refresh — so the processing state covers no overview refresh — so the processing state covers
unpack only (no file, no "(n/m)" counts, no title) and unpack only (no file, no "N remaining" count, no title) and
the success line points at the next step: "Uploaded the success line points at the next step: "Uploaded
<name> — press Sync sources to import it." The scan is <name> — press Sync sources to import it." The scan is
the RAG page's Sync button's job (edit the source's the RAG page's Sync button's job (edit the source's
+6 -5
View File
@@ -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 "Uploading… → Processing… → restored" lifecycle is made observable the
old way (the POST is held in the browser via ``page.route``) plus a 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 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 The archives are **built in-test** with Python's ``tarfile`` over
``tmp_path`` fixture files carrying markdown sentinels (``ALPHA-…`` / ``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``); "Uploading…" while the POST is in flight (held via ``page.route``);
at the 202 the "Successfully uploaded — <source>" toast fires; the at the 202 the "Successfully uploaded — <source>" toast fires; the
button then carries the BARE "Processing…" label for the whole 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 so the in-run window outlives the 2 s poll) until the status
``success`` lands: the result line reads "Uploaded e2e-upload ``success`` lands: the result line reads "Uploaded e2e-upload
(<stamp>) — press Sync sources to import it." (the phase-154 (<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 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 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 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 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.""" the hold the in-run window is only the 2 s pre-tick gap."""
state = {"held": False} state = {"held": False}
@@ -449,7 +450,7 @@ def test_upload_registers_without_indexing(
button is disabled and reads "Uploading…"; at the 202 the button is disabled and reads "Uploading…"; at the 202 the
"Successfully uploaded — <source>" toast fires (A2); the button "Successfully uploaded — <source>" toast fires (A2); the button
then carries the BARE "Processing…" label for the whole background 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 status GET); when the status ``success`` lands the result line
reads "Uploaded <source> (<stamp>) — press Sync sources to reads "Uploaded <source> (<stamp>) — press Sync sources to
import it." (A3 — the phase-154 completion stamp), the button 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 # The run is in flight (or just settled) server-side — and the
# label STAYS bare across the whole background run: the first # label STAYS bare across the whole background run: the first
# status GET is held, so the tick that should settle the button is # 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) time.sleep(2.5) # just past the poll's first tick (the fetch is held)
expect(btn).to_have_text("Processing…") expect(btn).to_have_text("Processing…")
+26 -17
View File
@@ -1,7 +1,7 @@
"""Phase 129 task 05 E2E (Playwright, mock-only): while a file imports, """Phase 129 task 05 E2E (Playwright, mock-only): while a file imports,
the sync status (endpoint + button label) shows the file's the sync status (endpoint + button label) shows the file's
embedding-chunk position — ``Syncing… chunkprog/big.md (1/2) · chunk embedding-chunk position — ``Syncing… chunkprog/big.md 1 remaining ·
14/38`` — with ``chunks_total`` the file's TRUE content-chunk count and 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 ``current_chunk`` the 1-based first chunk of the in-flight
token-budgeted batch (batch granularity, assumption A6), climbing 1 → token-budgeted batch (batch granularity, assumption A6), climbing 1 →
N through the file's embed batches, restarting per file, and cleared 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 The dedicated story suite for ``129_sync_chunk_progress`` (A16 — one
Playwright file per phase, run in isolation): the owner's report was 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 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``): 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 pattern; no git anywhere): ``chunkprog/`` with EXACTLY TWO files, both
root-level (no subfolders → the summaries candidate set is just the root-level (no subfolders → the summaries candidate set is just the
source root): ``big.md`` (the ~60 KB file above) + ``small.md`` (one source root): ``big.md`` (the ~60 KB file above) + ``small.md`` (one
tiny chunk). The sorted walk processes ``big.md`` FIRST, so the file tiny chunk). The sorted walk processes ``big.md`` FIRST, so the label's
counter reads ``(1/2)`` during the big span and ``(2/2)`` during the phase-193 count part reads ``1 remaining`` during the big span and is
small one — and the small file's single chunk is the proof that the 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 chunk position RESTARTS per file (the big file ends at its last
batch's first index, the small file reports ``(1, 1)``). 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 own 2 s poll): after clicking **Sync sources**, the page's own
poll renders the import span's ``#sync-label`` parts — poll renders the import span's ``#sync-label`` parts —
``.sync-label-main`` == ``Syncing… chunkprog/big.md``, ``.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 VISIBLE matching ``· chunk \\d+/\\d+`` with the total == the
expected chunk count — and the button ``title`` (and the expected chunk count — and the button ``title`` (and the
``#sync-progress`` box — the aria-live announcer, phase 150: 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). #: the directory's basename is the source name, phase 38).
SOURCE = "chunkprog" SOURCE = "chunkprog"
#: The two fixture files (sorted walk: ``big.md`` FIRST → the file #: The two fixture files (sorted walk: ``big.md`` FIRST → the label's
#: counter reads (1/2) on the big span, (2/2) on the small one). #: 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" BIG_FILE = f"{SOURCE}/big.md"
SMALL_FILE = f"{SOURCE}/small.md" SMALL_FILE = f"{SOURCE}/small.md"
N_FILES = 2 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 #: Test 2, step 1 — the in-flight IMPORT label with the VISIBLE chunk
#: part: the main part carries "Syncing… chunkprog/big.md" (the #: part: the main part carries "Syncing… chunkprog/big.md" (the
#: untruncated A4 text — the big file is the first of the two, so 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 #: "· chunk n/m" with m == the expected chunk count (the arg), and the
#: button title + the #sync-progress box (the aria-live announcer, #: button title + the #sync-progress box (the aria-live announcer,
#: phase 150) carry the UNTRUNCATED concatenation #: phase 150) carry the UNTRUNCATED concatenation
#: (main + " " + count + " " + chunk — A4: the announcer hears the #: (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 = """ _JS_IMPORT_CHUNK_SNAPSHOT = """
(n) => { (n) => {
const label = document.querySelector('#sync-label'); const label = document.querySelector('#sync-label');
@@ -330,7 +338,7 @@ _JS_IMPORT_CHUNK_SNAPSHOT = """
const chunk = label.querySelector('.sync-label-chunk'); const chunk = label.querySelector('.sync-label-chunk');
if (!main || !count || !chunk || count.hidden || chunk.hidden) return null; if (!main || !count || !chunk || count.hidden || chunk.hidden) return null;
if (main.textContent !== 'Syncing… chunkprog/big.md') 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+)$/); const m = chunk.textContent.match(/^· chunk (\\d+)\\/(\\d+)$/);
if (!m || Number(m[2]) !== n) return null; if (!m || Number(m[2]) !== n) return null;
const full = main.textContent + ' ' + count.textContent + ' ' + chunk.textContent; 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) · # 2. UI — the label's chunk part: "Syncing… chunkprog/big.md
# chunk n/m" while a batch is in flight, the untruncated label on # 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 # 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**; """A fresh admin page on ``/sources.html`` clicks **Sync sources**;
the page's OWN 2 s poll renders the import span's ``#sync-label`` the page's OWN 2 s poll renders the import span's ``#sync-label``
as the phase-128/129 PARTS — the main part 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 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 expected chunk count (the big file's span is ~45 s under
the slow proxy — the poll cannot miss it) — with the button 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 _JS_IMPORT_CHUNK_SNAPSHOT, arg=n_chunks, timeout=SETTLE_TIMEOUT_MS
).json_value() ).json_value()
assert snap["mainText"] == f"Syncing… {SOURCE}/big.md", snap 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"]) m = CHUNK_PART_RE.match(snap["chunkText"])
assert m, snap assert m, snap
assert int(m.group(2)) == n_chunks, snap # the TRUE chunk count, not a batch count assert int(m.group(2)) == n_chunks, snap # the TRUE chunk count, not a batch count
assert int(m.group(1)) >= 1, snap assert int(m.group(1)) >= 1, snap
# A4: the untruncated concatenation rides the title AND the # A4: the untruncated concatenation rides the title AND the
# aria-live announcer (the "· chunk" part included). # 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 assert snap["title"] == full and "· chunk" in snap["title"], snap
# The box IS the announcer (phase 150) — it carries the same full # The box IS the announcer (phase 150) — it carries the same full
# string the title does. # string the title does.
+24 -7
View File
@@ -92,7 +92,9 @@ Test → observable mapping (Playwright Mapping Rule):
first ``· image `` label — the two positions are distinct) and first ``· image `` label — the two positions are distinct) and
every image-span label carries the chunk part WITH IT (the last every image-span label carries the chunk part WITH IT (the last
chunk position persists through the file's image span — 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 TRUE chunk count; (c) the button ``title`` + ``aria-label`` carry
the SAME full string as the box on every running sample (A4 — the SAME full string as the box on every running sample (A4 —
asserted per sample, atomically); (d) at settle the box shows the 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 running sync re-enters with the WHOLE status, phase 98) renders
the CURRENT image position with the A4 title/aria — then the run the CURRENT image position with the A4 title/aria — then the run
settles to the counts line. 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 from __future__ import annotations
@@ -169,8 +178,9 @@ SLOW_DELAY_S = "1.5"
#: The local source — the temp directory's basename (``kind=local`` → #: The local source — the temp directory's basename (``kind=local`` →
#: the directory's basename is the source name, phase 38). #: the directory's basename is the source name, phase 38).
SOURCE = "imgpass" SOURCE = "imgpass"
#: The single fixture file (the source is one file — the count part #: The single fixture file (the source is one file, so phase 193 hides
#: reads "(1/1)" throughout the import phase). #: the count part for the whole import span — N = total - done = 0 on
#: the only file there is).
PDF_NAME = "imgpass.pdf" PDF_NAME = "imgpass.pdf"
N_FILES = 1 N_FILES = 1
@@ -739,8 +749,14 @@ def test_label_advances_through_the_image_span(
# sampled) --- # sampled) ---
assert any(t == "Syncing…" for t in seen), f"no bare prelude label: {seen[:6]}" 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… ")] 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), ( assert any(f"Syncing… {SOURCE}/{PDF_NAME}" in t for t in import_labels), (
f"no file count part: {import_labels[:6]}" 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 # --- 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] ns = [n for n, _ in positions]
assert ns == sorted(ns), f"the climb is backwards somewhere: {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}" assert N_IMAGES in ns, f"the climb never reached the total: {positions}"
# The two positions COEXIST (distinct and both may show — "… (1/1) # The two positions COEXIST (distinct and both may show —
# · chunk 19/20 · image 3/4"): every image-span label carries the # "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 # chunk part with it (the last chunk position persists through the
# file's image span — the per-file reset + the phase transitions # file's image span — the per-file reset + the phase transitions
# clear it, not the end of the chunk span). # clear it, not the end of the chunk span).
+50 -29
View File
@@ -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 sync text surface AND the sole aria-live region (the retired
announcer span is gone). Tests 1–3 announcer span is gone). Tests 1–3
therefore pin the BOX: visible while the run is in flight, its text therefore pin the BOX: visible while the run is in flight, its text
the pre-phase byte-identical full string ("Syncing… <full the full label ("Syncing… <full
source/relative/path> (n/4)" / "Summarizing folders… <folder> source/relative/path> N remaining" / "Summarizing folders… <folder>
(n/3)" + the phase-129 "· chunk n/m" while a batch is in flight), (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 inside the viewport at 390 px and 1280 px, the button in its
running state, and the ``title`` carrying the same full string 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 pre-phase counterfactual restores the parts' inline-block display
at matching specificity, last in the cascade. 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``): 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 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 **Sync sources**; while the import phase is running (the page's
own 2 s poll): the detail box (``#sync-progress``) is VISIBLE and own 2 s poll): the detail box (``#sync-progress``) is VISIBLE and
an atomic snapshot of: its text the FULL untruncated label 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 "· 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 text (never clipped — nothing is ellipsized away), the box's
bounding box INSIDE the viewport (left ≥ 0, right ≤ 390 — the bounding box INSIDE the viewport (left ≥ 0, right ≤ 390 — the
bug's direct negation), the button in its running state 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. count span is hidden + empty, and the box hides.
3. ``test_desktop_label_unchanged`` — viewport 1280×800; the same 3. ``test_desktop_label_unchanged`` — viewport 1280×800; the same
sync: during import the box text matches ``Syncing… <full sync: during import the box text matches ``Syncing… <full
source/relative/path> (n/m)`` (+ the phase-129 chunk part while a source/relative/path> N remaining`` (+ the phase-129 chunk part
batch is in flight) — the pre-phase byte-identical FULL string while a batch is in flight) — the FULL string (phase 149 moved it
(phase 149 moved it from the pill's parts to the box — the text from the pill's parts to the box, phase 193 re-shaped the count
is unchanged), the box inside the viewport, the button in its part), the box inside the viewport, the button in its
running state (``is-running``), and the ``title`` carrying the running state (``is-running``), and the ``title`` carrying the
same untruncated string (A4). same untruncated string (A4).
4. ``test_pre_phase_css_reproduces_the_clip`` — the pin check: the 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 #: The in-flight import label's PARTS (the phase-64 byte-identical
#: strings, unit-pinned in tests/unit/test_frontend_sync_upload.py): #: strings, unit-pinned in tests/unit/test_frontend_sync_upload.py):
#: main "Syncing… <full source/relative/path>" + count "(n/4)" — #: main "Syncing… <full source/relative/path>" + count "N remaining"
#: ``done`` starts at 1 (the importer counts the file as current #: (phase 193) — ``files_done`` starts at 1 (the importer counts the
#: BEFORE indexing it), so ``n`` ∈ 1..4. (Test 4 — the pre-phase #: 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.) #: rules' clip reproduction — pins the PARTS' DOM text directly.)
IMPORT_MAIN_RE = re.compile(rf"^Syncing… {FILE_RE}$") 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 #: The pre-phase byte-identical FULL string (main + " " + count — the
#: single span's text before phase 128; the A4 title keeps it), plus #: 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 #: 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 #: 01): the box (#sync-progress) carries this EXACT string while the
#: import is in flight (tests 1 + 3) — the title carries it too #: import is in flight (tests 1 + 3) — the title carries it too
#: (A4 — one source, two surfaces). #: (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): #: The summaries label's FULL string (phase 98 task 02, unit-pinned):
#: "Summarizing folders… [source/folder] (n/3)" (the folder part #: "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 #: Test 1/3 — the in-flight IMPORT detail: the box is VISIBLE and its
#: text is the FULL untruncated label — "Syncing… <full #: 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 #: 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 #: pre-first-batch frame has none; the phase-129 dedicated suite pins
#: it strictly) — the count is IN the text (never clipped — nothing #: it strictly) — the count is IN the text (never clipped — nothing
@@ -296,7 +314,7 @@ _JS_IMPORT_BOX_SNAPSHOT = """
const t = textEl.textContent; const t = textEl.textContent;
const reFull = new RegExp( const reFull = new RegExp(
"^Syncing… synclabel/(?:alpha|bravo)/(?:alpha|bravo)-[ab]\\\\.md " + "^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; if (!reFull.test(t)) return null;
const title = btn.getAttribute('title'); const title = btn.getAttribute('title');
@@ -395,7 +413,7 @@ _JS_PARTS_IMPORT_SNAPSHOT = """
const count = label.querySelector('.sync-label-count'); const count = label.querySelector('.sync-label-count');
if (!main || !count || count.hidden) return null; if (!main || !count || count.hidden) return null;
const reMain = /^Syncing… synclabel\\/(?:alpha|bravo)\\/(?:alpha|bravo)-[ab]\\.md$/; 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; if (!reMain.test(main.textContent) || !reCount.test(count.textContent)) return null;
const chunk = label.querySelector('.sync-label-chunk'); const chunk = label.querySelector('.sync-label-chunk');
const chunkText = (chunk && !chunk.hidden) ? chunk.textContent : ''; 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 box is a block in the 72rem centered container, so it can only
shrink toward the container's content width — it never overflows shrink toward the container's content width — it never overflows
the viewport — and its text is the FULL untruncated label (the 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 on-screen is task 02's overflow-wrap: anywhere, pinned by the
phase-149 dedicated suite). The 1 px slack covers float rounding phase-149 dedicated suite). The 1 px slack covers float rounding
only.""" 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 # 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 (``#sync-progress`` — phase 149: the running detail left the
pill; the button is the spinner) is an atomic snapshot of: pill; the button is the spinner) is an atomic snapshot of:
VISIBLE with the FULL untruncated label ``Syncing… <full VISIBLE with the FULL untruncated label ``Syncing… <full
source/relative/path> (n/4)`` (+ the phase-129 "· chunk n/m" part source/relative/path> N remaining`` (+ the phase-129 "· chunk n/m"
while a batch is in flight — optional) with the ``(n/4)`` count IN part while a batch is in flight — optional) with the
the text (NEVER clipped — nothing is ellipsized away), the box's ``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 bounding box INSIDE the viewport (left ≥ 0, right ≤ 390 — the
bug's direct negation), the button in its running state bug's direct negation), the button in its running state
(``is-running``), and the button ``title`` carrying the SAME full (``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() ).json_value()
# The FULL untruncated label — the complete # 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. # clipped), the optional phase-129 chunk part.
assert IMPORT_FULL_RE.match(snap["boxText"]), snap["boxText"] assert IMPORT_FULL_RE.match(snap["boxText"]), snap["boxText"]
# A4: the button title carries the SAME untruncated label (the # 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; 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 the owner's report was that the full label was clipped in the
pill even on desktop) carries ``Syncing… <full pill even on desktop) carries ``Syncing… <full
source/relative/path> (n/m)`` (+ the phase-129 chunk part while a source/relative/path> N remaining`` (+ the phase-129 chunk part
batch is in flight): the pre-phase byte-identical FULL string while a batch is in flight): the FULL string (phase 149 changed
(phase 149 changed the SURFACE, not the text). The box is inside 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 viewport, the button is in its running state (``is-running``
— the spinner), and the button ``title`` carries the same — the spinner), and the button ``title`` carries the same
untruncated string (A4).""" untruncated string (A4)."""
@@ -832,9 +852,9 @@ def test_desktop_label_unchanged(
_JS_IMPORT_BOX_SNAPSHOT, timeout=SETTLE_TIMEOUT_MS _JS_IMPORT_BOX_SNAPSHOT, timeout=SETTLE_TIMEOUT_MS
).json_value() ).json_value()
# The pre-phase byte-identical FULL string ("Syncing… <full # The FULL string ("Syncing… <full source/relative/path>
# source/relative/path> (n/m)" + " · chunk n/m" while a batch is # N remaining" + " · chunk n/m" while a batch is in flight) — in
# in flight) — now in the box, untruncated and wrapping. # the box, untruncated and wrapping.
assert IMPORT_FULL_RE.match(snap["boxText"]), snap["boxText"] assert IMPORT_FULL_RE.match(snap["boxText"]), snap["boxText"]
assert snap["title"] == snap["boxText"], snap assert snap["title"] == snap["boxText"], snap
# Phase 149: the running button is the spinner (is-running); # 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 # 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 # 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 # 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, ( assert count["right"] > label["right"] + 1, (
f"pre-phase CSS did not clip the count — the pin would be vacuous: " f"pre-phase CSS did not clip the count — the pin would be vacuous: "
f"label={label} count={count}" f"label={label} count={count}"
+23 -14
View File
@@ -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 parts; the spinning icon is the whole control) and the bordered text
box directly under the button row (``#sync-progress``) carries the box directly under the button row (``#sync-progress``) carries the
**EXACT full untruncated live label** — ``Syncing… **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 (the long ``source/relative/path`` that overflows the pill is the
fixture's whole reason to exist). Phase 150 folded the announcer into 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 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 local ``file://`` fixture repo, a real import, the slowed mock LLM so
the in-flight state is observable. 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 — Run in isolation (DB must be up: ``podman compose up -d db``; git on PATH —
a documented environment prerequisite, phase 28): a documented environment prerequisite, phase 28):
@@ -105,7 +111,7 @@ in one JS task):
phase-134 contract unchanged). phase-134 contract unchanged).
3. ``test_box_carries_the_full_untruncated_label`` — during the 3. ``test_box_carries_the_full_untruncated_label`` — during the
in-flight window of the LONG-path file (poll until the box text is 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 pre-phase ellipsis would have truncated it): the box's
``scrollWidth <= clientWidth`` AND the text's ``scrollWidth <= ``scrollWidth <= clientWidth`` AND the text's ``scrollWidth <=
clientWidth`` (the wrap engaged — nothing is clipped), the box IS the 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 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 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> 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 is in flight; the proxy's 1.5 s delay makes the ~15 s window ≫ the
2 s poll). 2 s poll).
4. ``test_mobile_box_full_width`` — a fresh page at the house mobile 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 #: The in-flight label's file part — the status endpoint's
#: ``current_file`` = ``<source>/<rel>`` (the sync's progress hook), #: ``current_file`` = ``<source>/<rel>`` (the sync's progress hook),
#: verbatim. The long file is the FIRST of the three (the sorted walk — #: 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}" LONG_PATH = f"{SOURCE}/{LONG_REL}"
#: The long file's shape (module docstring): an H1 preamble (one chunk) #: 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 #: 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 — #: 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 #: " · chunk n/m" part while an embed batch is in flight — the
#: pre-first-batch frame has none) — the complete #: pre-first-batch frame has none) — the complete
# source/relative/path IN the text (the pre-phase ellipsis would have # 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'); const btn = document.querySelector('#sync-btn');
if (!box || !textEl || !btn || box.hidden) return null; if (!box || !textEl || !btn || box.hidden) return null;
const t = textEl.textContent; const t = textEl.textContent;
const prefix = 'Syncing… ' + longPath + ' ('; const prefix = 'Syncing… ' + longPath + ' ';
if (!t.startsWith(prefix)) return null; if (!t.startsWith(prefix)) return null;
const tail = t.slice(prefix.length); 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); const cs = (el) => getComputedStyle(el);
return { return {
boxText: t, 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 #: 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) · #: embed window: the box text is "Syncing… <full long path>
#: chunk n/m" (the part is only present while an in-flight file's embed #: 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 #: 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 #: long-file window ≫ the 2 s poll, so the retry loop resolves
#: deterministically). #: deterministically).
@@ -418,7 +425,7 @@ _JS_LONG_FILE_CHUNK_SNAPSHOT = """
const textEl = document.querySelector('#sync-progress-text'); const textEl = document.querySelector('#sync-progress-text');
if (!box || !textEl || box.hidden) return null; if (!box || !textEl || box.hidden) return null;
const t = textEl.textContent; 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+)$/); const m = t.match(/· chunk (\\d+)\\/(\\d+)$/);
if (!m) return null; if (!m) return null;
return { return {
@@ -453,7 +460,7 @@ _JS_MOBILE_SNAPSHOT = """
if (!box || !textEl || !btn || !label || !icon || !container || !head) return null; if (!box || !textEl || !btn || !label || !icon || !container || !head) return null;
if (box.hidden) return null; if (box.hidden) return null;
const t = textEl.textContent; 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 cs = (el) => getComputedStyle(el);
const rect = (el) => { const rect = (el) => {
const b = el.getBoundingClientRect(); const b = el.getBoundingClientRect();
@@ -998,7 +1005,7 @@ def test_box_carries_the_full_untruncated_label(
page: Page, app_url: str, db_ready: None page: Page, app_url: str, db_ready: None
) -> None: ) -> None:
"""During the in-flight window of the LONG-path file (poll until the """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 makes the ~15 s span deterministic): the box's text contains the
FULL label shape with the complete ``source/relative/path`` as a FULL label shape with the complete ``source/relative/path`` as a
substring (the pre-phase ellipsis would have truncated it — the 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 # The FULL label shape — the complete long path IN the text (the
# clipping trigger, untruncated). # 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 snap["boxText"].startswith(expected_prefix), snap["boxText"]
assert LONG_PATH in snap["boxText"], snap["boxText"] assert LONG_PATH in snap["boxText"], snap["boxText"]
# The wrap engaged — no horizontal overflow (the whole point of the # 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 # The label wraps — the full long path is IN the box's text (the
# old mobile-stuffing ellipsis is gone). # 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"] assert LONG_PATH in snap["boxText"], snap["boxText"]
# The wrap engaged — no horizontal clipping at the phone width. # The wrap engaged — no horizontal clipping at the phone width.
assert snap["boxScrollWidth"] <= snap["boxClientWidth"], snap assert snap["boxScrollWidth"] <= snap["boxClientWidth"], snap
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -705,8 +705,8 @@ def test_running_upload_line_unchanged(
tarball_c: Path, tarball_c: Path,
) -> None: ) -> None:
"""At the 202 moment the upload run is in flight: the button is """At the 202 moment the upload run is in flight: the button is
the BARE "Processing…" (phase 90, A2 — no file, no "(n/m)" the BARE "Processing…" (phase 90, A2 — no file, no `N remaining`
counts, the title cleared) and ``#archive-upload-result`` is count, the title cleared) and ``#archive-upload-result`` is
HIDDEN — the ready-for-sync line NEVER renders during the run HIDDEN — the ready-for-sync line NEVER renders during the run
(the phase-90 contract: the line appears ONLY on settle). The (the phase-90 contract: the line appears ONLY on settle). The
deterministic in-flight window: the 202 handler synchronously deterministic in-flight window: the 202 handler synchronously
+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``/ 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 ``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 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 sources to import it." result line lands (phase 90 A3 — the phase-154
completion stamp). The scan — with its live completion stamp). The scan — with its live
file label — is the RAG page's **Sync sources** button's job, and the 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 the spinner) for the ``Syncing…`` prefix plus a file path (generous
timeout), which the page's own 2 s poll ticks render. 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 The upload archive is built in-test with Python's ``tarfile`` from
**25 small ``.md`` files** (``e2e-prog.tar.gz`` → source **25 small ``.md`` files** (``e2e-prog.tar.gz`` → source
``e2e-prog``); the sync subjects are the uploaded row itself (the new ``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 the 202; navigating to ``/sources.html`` shows **zero indexed
documents** (the upload unpacked + registered only) and the sync documents** (the upload unpacked + registered only) and the sync
button settled idle with no error UI; clicking **Sync sources** button settled idle with no error UI; clicking **Sync sources**
then imports the uploaded row with the LIVE "Syncing… <file> (n/m)" then imports the uploaded row with the LIVE "Syncing… <file>
label (both layers — the phase-149 detail box carries the full 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 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 + title while a file's embed batch is in flight) and lands the
counts ("N added", the catalog refreshes); counts ("N added", the catalog refreshes);
* **upload processing (phase 90 A2)**: the upload button shows the * **upload processing (phase 90 A2)**: the upload button shows the
BARE "Processing…" for the whole background run (no file, no 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`` running tick the recorder catches carries a null ``current_file``
and 0/0 counts; the terminal status is ``success`` with the no-count and 0/0 counts; the terminal status is ``success`` with the no-count
``{"message": "uploaded"}`` detail and null/0/0 progress; the ``{"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 live file (A4)**: a multi-file local source; clicking
**Sync sources** on ``/sources.html`` shows "Syncing…" (bare, the **Sync sources** on ``/sources.html`` shows "Syncing…" (bare, the
pre-64 click state) in the detail box (phase 149 — the button is 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 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 — file's embed batch is in flight), then the pre-64 success settle —
"Synced HH:MM" + the counts result line — preserved, plus the file "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 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 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 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 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.""" the hold the in-run window is only the 2 s pre-tick gap."""
state = {"held": False} state = {"held": False}
@@ -579,7 +590,7 @@ def test_upload_toast_then_navigate_away(
documents** (phase 90 A1: the upload unpacked + registered only) documents** (phase 90 A1: the upload unpacked + registered only)
and the sync button settled idle with no error UI; then click and the sync button settled idle with no error UI; then click
**Sync sources** (the new leg, phase 90) → the LIVE **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 row (phase 149: the box under the button row — the button is the
spinner — at both layers), then the success settle with the counts spinner — at both layers), then the success settle with the counts
and the catalog refresh (the box hides).""" 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 # 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 # detail box — the page's 2 s poll tick reveals the live file in
# #sync-progress as the FULL untruncated string: "Syncing… # #sync-progress as the FULL untruncated string: "Syncing…
# <source/rel/path> (n/m)" (+ " · chunk n/m" while a file's embed # <source/rel/path> N remaining" (phase 193 — the count part
# batch is in flight — phase 129, batch granularity). The box is # counts DOWN and hides on the FINAL file, so the part is optional
# the visible running surface — nothing is ellipsized away, the # in this wait; the pre-phase "(n/m)" tail can no longer match)
# "(n/m)" count is IN the full string (the never-clipped pin), # (+ " · chunk n/m" while a file's embed batch is in flight —
# and the button title carries the same full string (A4 — one # phase 129, batch granularity). The box is the visible running
# source, two surfaces). # 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( expect(page.locator("#sync-progress-text")).to_have_text(counts_line(
re.compile( re.compile(
rf"^Syncing… {re.escape(UPLOAD_NAME)}/.+\.md \(\d+/{N_FILES}\)" rf"^Syncing… {re.escape(UPLOAD_NAME)}/.+\.md"
r"(?: · chunk \d+/\d+)?$" r"(?: [0-9]+ remaining)?(?: · chunk \d+/\d+)?$"
)), )),
timeout=SETTLE_TIMEOUT_MS, timeout=SETTLE_TIMEOUT_MS,
) )
# The box is the VISIBLE running surface (the count's old # The box is the VISIBLE running surface (the count's old
# "never clipped" pin now lives in its full string — the "(n/m)" # "never clipped" pin now lives in its full string — the
# is in the text above). # "N remaining" part is in the text above).
expect(page.locator("#sync-progress")).to_be_visible( expect(page.locator("#sync-progress")).to_be_visible(
timeout=SETTLE_TIMEOUT_MS timeout=SETTLE_TIMEOUT_MS
) )
expect(btn).to_have_attribute( expect(btn).to_have_attribute(
"title", "title",
re.compile( 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, timeout=SETTLE_TIMEOUT_MS,
) )
@@ -710,7 +724,7 @@ def test_upload_progress_is_bare(
) -> None: ) -> None:
"""Phase 90 A2: the upload's background run has NO file-level """Phase 90 A2: the upload's background run has NO file-level
progress. The button shows the BARE "Processing…" for the whole 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 status GET); every running tick the recorder catches carries a
null ``current_file`` and 0/0 counts; the terminal status is null ``current_file`` and 0/0 counts; the terminal status is
``success`` with the no-count ``{"message": "uploaded"}`` detail ``success`` with the no-count ``{"message": "uploaded"}`` detail
@@ -753,7 +767,7 @@ def test_upload_progress_is_bare(
expect(btn).to_have_attribute("title", "") expect(btn).to_have_attribute("title", "")
# …and it STAYS bare across the whole background run: the first # …and it STAYS bare across the whole background run: the first
# status GET is held, so the settling tick is in flight — no file # 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) time.sleep(2.5)
expect(btn).to_have_text("Processing…") 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 style): on ``/sources.html`` click **Sync sources** → the detail
box (phase 149 — the running label left the pill; the button is box (phase 149 — the running label left the pill; the button is
the spinner) shows "Syncing…" (bare, the pre-64 click state) then 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 ``current_file`` non-null at running ticks; UI layer: the box
poll), then the success settle with the counts result line (the poll), then the success settle with the counts result line (the
box hides) — the pre-phase-64 sync UX's settle is preserved, plus 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 # 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 # detail box — the page's 2 s poll tick reveals the live file in
# #sync-progress as the FULL untruncated string: "Syncing… # #sync-progress as the FULL untruncated string: "Syncing…
# <source/rel/path> (n/m)" (+ " · chunk n/m" while a file's embed # <source/rel/path> N remaining" (phase 193 — the count part
# batch is in flight — phase 129, batch granularity). The box is # counts DOWN and hides on the FINAL file, so the part is optional
# the visible running surface — nothing is ellipsized away, the # in this wait; the pre-phase "(n/m)" tail can no longer match)
# "(n/m)" count is IN the full string (the never-clipped pin), # (+ " · chunk n/m" while a file's embed batch is in flight —
# and the button title carries the same full string (A4 — one # phase 129, batch granularity). The box is the visible running
# source, two surfaces). # 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( expect(page.locator("#sync-progress-text")).to_have_text(counts_line(
re.compile( re.compile(
rf"^Syncing… {re.escape(SYNC_SOURCE_DIR)}/.+\.md \(\d+/{N_FILES}\)" rf"^Syncing… {re.escape(SYNC_SOURCE_DIR)}/.+\.md"
r"(?: · chunk \d+/\d+)?$" r"(?: [0-9]+ remaining)?(?: · chunk \d+/\d+)?$"
)), )),
timeout=SETTLE_TIMEOUT_MS, timeout=SETTLE_TIMEOUT_MS,
) )
# The box is the VISIBLE running surface (the count's old # The box is the VISIBLE running surface (the count's old
# "never clipped" pin now lives in its full string — the "(n/m)" # "never clipped" pin now lives in its full string — the
# is in the text above). # "N remaining" part is in the text above).
expect(page.locator("#sync-progress")).to_be_visible( expect(page.locator("#sync-progress")).to_be_visible(
timeout=SETTLE_TIMEOUT_MS timeout=SETTLE_TIMEOUT_MS
) )
@@ -862,8 +878,8 @@ def test_sync_live_file_label(
"title", "title",
re.compile( re.compile(
"^Syncing… " "^Syncing… "
rf"{re.escape(SYNC_SOURCE_DIR)}/.+\.md \(\d+/{N_FILES}\)" rf"{re.escape(SYNC_SOURCE_DIR)}/.+\.md"
r"(?: · chunk \d+/\d+)?$" r"(?: [0-9]+ remaining)?(?: · chunk \d+/\d+)?$"
), ),
timeout=SETTLE_TIMEOUT_MS, timeout=SETTLE_TIMEOUT_MS,
) )
+1 -1
View File
@@ -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 racing a timer the suite polls the RAG page's settled sync UI
(``#sync-label`` "Synced HH:MM" + the ``#sync-progress`` box's counts (``#sync-label`` "Synced HH:MM" + the ``#sync-progress`` box's counts
line — phase 150: the box is the single sync text surface) with 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. ``test_sync_upload_progress.py`` suite's subject, not this one's.
The archive is **built in-test** with Python's ``tarfile``: The archive is **built in-test** with Python's ``tarfile``:
+172 -29
View File
@@ -79,8 +79,9 @@ its own (n/m) is its position, idle / settle). setSyncLabel(mainText,
countText, chunkText) lands the part under the same present/absent countText, chunkText) lands the part under the same present/absent
contract as the count part, and the UNTRUNCATED full label — main + contract as the count part, and the UNTRUNCATED full label — main +
" " + count + " " + chunk — rides the button title + #sync-progress " " + count + " " + chunk — rides the button title + #sync-progress
(A4 — the in-flight import reads "Syncing… <file> (2/3) · chunk (A4 — the in-flight import reads "Syncing… <file> 1 remaining · chunk
14/38"). The part keeps the pinned mono CSS (flex 0 0 auto, nowrap, 14/38" — phase 193 counts the files DOWN). The part keeps the pinned
mono CSS (flex 0 0 auto, nowrap,
--mono, the rule right after .sync-label-count). A node harness (the --mono, the rule right after .sync-label-count). A node harness (the
test-document-viewer pattern) behaviorally pins the rendered parts + test-document-viewer pattern) behaviorally pins the rendered parts +
the full title across every label state. the full title across every label state.
@@ -141,7 +142,8 @@ the 202/409 click stay bare). setSyncLabel(mainText, countText =
"", chunkText = "", running = false, imageText = "") appends the "", chunkText = "", running = false, imageText = "") appends the
image part LAST to the composed full string — so the box, the image part LAST to the composed full string — so the box, the
button title, and the running aria-label all carry button title, and the running aria-label all carry
"Syncing… <file> (2/3) · chunk 14/38 · image 3/15". The image part "Syncing… <file> 1 remaining · chunk 14/38 · image 3/15" (phase 193).
The image part
gets NO span of its own (the running button is the spinner — its gets NO span of its own (the running button is the spinner — its
parts are hidden, phase 149): it rides ONLY the composed string, parts are hidden, phase 149): it rides ONLY the composed string,
while the chunk part keeps its .sync-label-chunk span; the two while the chunk part keeps its .sync-label-chunk span; the two
@@ -189,6 +191,19 @@ app/ no-touch regression guard). The qualifier's byte-identity
pinned in test_sync_button.py on the shared fixed clock; the four pinned in test_sync_button.py on the shared fixed clock; the four
shapes and the fmtUploadStamp behavior (finished / started / shapes and the fmtUploadStamp behavior (finished / started /
missing / garbage) ride the task-02 pins below. missing / garbage) ride the task-02 pins below.
Phase 193 (task 01, D1) re-points the running file-count part: the
fmtSyncLabel COUNT part is the COUNT-DOWN `N remaining` (N =
total - done) — shown while N >= 1, HIDDEN at the final file (N == 0:
the main part names the file in flight — a "0 remaining" next to the
last file's name is the stale-looking edge the owner's complaint is
about), the total > 0 pre-walk guard KEPT, plain integer (no thousands
separator); the UPLOAD job shares the builder and the shape (A2). The
node harness rows that carried "(2/3)" now carry "1 remaining" (the
same 2/3 fixture — the part renders the distance to the end), the
harness gains the final-file row (the count part hidden), and the
summaries phase's own (n/m) folder position, the "· chunk n/m" /
"· image n/m" parts and the terminal counts line stay byte-identical.
""" """
from __future__ import annotations from __future__ import annotations
@@ -316,6 +331,19 @@ def _tick(js: str) -> str:
return js[tick:end] return js[tick:end]
def _doc_above(js: str, name: str) -> str:
"""The block comment immediately above `function <name>` (the
builder's docstring — _fn starts AT the keyword, so the lineage
paragraphs need this slice)."""
i = js.find(f"function {name}")
assert i != -1, f"{name} must be defined in sources.js"
start = js.rfind("/*", 0, i)
assert start != -1, f"{name} must carry a docstring"
end = js.find("*/", start)
assert start < end < i, f"{name}'s docstring must sit above the function"
return js[start:end]
# ---------- fmtSyncLabel: the live-file label PARTS contract ---------- # ---------- fmtSyncLabel: the live-file label PARTS contract ----------
@@ -328,9 +356,10 @@ def test_fmt_sync_label_returns_the_parts_contract() -> None:
prefix shows during clone/pull, before any file is indexed — A4; prefix shows during clone/pull, before any file is indexed — A4;
phase 90: the upload run never carries a file or counts, so its phase 90: the upload run never carries a file or counts, so its
label is always the bare "Importing"); the COUNT part — only when label is always the bare "Importing"); the COUNT part — only when
total > 0 (the import has started), NO join space (the space is total > 0 (the import has started) — is the phase-193 COUNT-DOWN
the CSS gap between the parts — the full string's space is `N remaining` (N = total - done), NO join space of its own (the
re-joined by setSyncLabel for the title / announcer, A4). File space between the parts is the CSS gap — the full string's space
is re-joined by setSyncLabel for the title / announcer, A4). File
before counts in the full-string order.""" before counts in the full-string order."""
body = _fn(_js(), "fmtSyncLabel") body = _fn(_js(), "fmtSyncLabel")
assert "function fmtSyncLabel(kind, currentFile, done, total)" in body assert "function fmtSyncLabel(kind, currentFile, done, total)" in body
@@ -344,20 +373,95 @@ def test_fmt_sync_label_returns_the_parts_contract() -> None:
# the MAIN part: the file appended only when present # the MAIN part: the file appended only when present
assert "currentFile ?" in body assert "currentFile ?" in body
assert "`${prefix} ${currentFile}`" in body assert "`${prefix} ${currentFile}`" in body
# the COUNT part: only when the import has started (total > 0), # the COUNT part (phase 193, D1): only when the import has
# no join space (the parts are joined by setSyncLabel, A4) # started (the kept total > 0 pre-walk guard) AND the run is not
# on its final file (total - done > 0), no leading join space
# (the parts are joined by setSyncLabel, A4)
assert "total > 0" in body assert "total > 0" in body
assert "`(${done}/${total})`" in body assert "`${total - done} remaining`" in body
assert "` (${done}/${total})`" not in body, ("no space in the count part") assert "(${done}/${total})" not in body, (
"the pre-phase (done/total) shape is gone (phase 193)"
)
i_file = body.find("currentFile ?") i_file = body.find("currentFile ?")
i_counts = body.find("total > 0") i_counts = body.find("total > 0")
assert -1 < i_file < i_counts, "the file lands before the counts" assert -1 < i_file < i_counts, "the file lands before the counts"
# the parts return (the byte-identical full strings hold when # the parts return (the full strings hold when setSyncLabel joins
# setSyncLabel joins them: "Syncing…", "Syncing… <file>", # them: "Syncing…", "Syncing… <file>",
# "Syncing… <file> (n/m)", "Importing") # "Syncing… <file> N remaining", "Importing")
assert "return { main, count }" in body assert "return { main, count }" in body
def test_fmt_sync_label_count_part_is_the_n_remaining_part() -> None:
"""Phase 193 (task 01, D1): the running file-count part counts
DOWN. The fmtSyncLabel count expression is pinned VERBATIM — the
kept `total > 0` pre-walk guard (phase 64: no count part before
the walk knows the total) AND the new `total - done > 0` guard
(the part hides at the final file, ASSUMPTION A1) around the
plain-integer `${total - done} remaining` shape (no thousands
separator — the existing count part's house style). The docstring
names the new part (N = total - done, shown while N >= 1), the
hidden-at-the-final-file edge (N == 0 — the main part names the
file in flight; a "0 remaining" next to the last file's name is
the stale-looking edge), and the upload job sharing the builder
and the shape (A2)."""
js = _js()
body = _fn(js, "fmtSyncLabel")
# the exact expression, byte-for-byte (both guards + the shape +
# the empty-string else)
assert (
"const count = total > 0 && total - done > 0"
" ? `${total - done} remaining` : \"\";"
) in body, "the count expression is the D1 shape"
assert "total > 0 &&" in body, "the total > 0 pre-walk guard stays"
assert "total - done > 0" in body, (
"the part hides at the final file (N == 0, A1)"
)
# plain integer — no separator/format helper joins the number
assert "toLocaleString" not in body and ".toFixed" not in body, (
"plain integer, no thousands separator (the house label style)"
)
doc = _doc_above(js, "fmtSyncLabel")
assert "`N remaining`" in doc and "N = total - done" in doc, (
"the docstring describes the new part"
)
assert "N >= 1" in doc, "shown while N >= 1 (A1)"
assert "HIDDEN at the final file" in doc and "N == 0" in doc, (
"the hidden-at-final-file edge is named in the docstring (A1)"
)
assert "thousands separator" in doc, "the plain-integer clause is named"
assert "pre-walk" in doc, "the phase-64 total > 0 contract is named"
assert "shares this builder" in doc, (
"the upload job sharing the builder and the shape is named (A2)"
)
def test_remaining_part_leaves_the_other_parts_byte_identical() -> None:
"""Phase 193 (task 01): the COUNT-DOWN moved exactly ONE part.
The fmtSyncPhaseLabel import fall-through still builds the
phase-129 "· chunk n/m" and the phase-152 "· image n/m" parts
from the status's own fields, the summaries branch still emits
its FOLDER position `(${summaries_done}/${summaries_total})`,
the bare fmtSyncLabel builder stays free of all three (each part
lives only in its own builder), and the terminal counts line
(fmtSyncResult — a different surface) never grows the word."""
js = _js()
phase = _fn(js, "fmtSyncPhaseLabel")
assert "`· chunk ${status.current_chunk}/${status.chunks_total}`" in phase
assert "`· image ${status.images_done}/${status.images_total}`" in phase
assert (
"count: `(${status.summaries_done}/${status.summaries_total})`"
) in phase, "the summaries folder position keeps its (n/m) shape"
bare = _fn(js, "fmtSyncLabel")
for part in ("chunk", "image", "summaries"):
assert part not in bare, (
f"fmtSyncLabel stays {part}-free — the part lives only in its own builder"
)
assert "remaining" not in _fn(js, "fmtSyncResult"), (
"the terminal result line is untouched (the running count part"
" is the one change)"
)
def test_set_sync_label_is_the_single_parts_writer() -> None: def test_set_sync_label_is_the_single_parts_writer() -> None:
"""setSyncLabel(mainText, countText = "", chunkText = "", """setSyncLabel(mainText, countText = "", chunkText = "",
running = false, imageText = "") — Phase 128 (task 01) + Phase running = false, imageText = "") — Phase 128 (task 01) + Phase
@@ -374,7 +478,7 @@ def test_set_sync_label_is_the_single_parts_writer() -> None:
(countText ? " " + countText : "") + (chunkText ? " " + chunkText (countText ? " " + countText : "") + (chunkText ? " " + chunkText
: "") + (imageText ? " " + imageText : "") — rides the button : "") + (imageText ? " " + imageText : "") — rides the button
title (A4 — the screen reader hears the title (A4 — the screen reader hears the
whole "Syncing… <file> (2/3) · chunk 14/38 · image 3/15" there, whole "Syncing… <file> 1 remaining · chunk 14/38 · image 3/15" there,
and in the box — see below). The image part (phase 152) gets NO and in the box — see below). The image part (phase 152) gets NO
span of its own (the running button is the spinner — its parts span of its own (the running button is the spinner — its parts
are hidden, phase 149): it rides only the composed full string. are hidden, phase 149): it rides only the composed full string.
@@ -868,6 +972,15 @@ enterSyncRunningState("sync", "src/a/b.md", 2, 3, {
current_chunk: 0, chunks_total: 0, current_chunk: 0, chunks_total: 0,
}); });
show("import-prebatch"); show("import-prebatch");
// 2b. Phase 193 (task 01, A1): the FINAL file (files_done ==
// files_total) — the count part HIDES (the main part names the
// file in flight; a "0 remaining" next to it is the stale-looking
// edge):
enterSyncRunningState("sync", "src/a/b.md", 3, 3, {
phase: "import", current_file: "src/a/b.md", files_done: 3, files_total: 3,
current_chunk: 0, chunks_total: 0,
});
show("import-final");
// 3. the prelude (null phase, no file yet): // 3. the prelude (null phase, no file yet):
enterSyncRunningState("sync", null, 0, 0, { enterSyncRunningState("sync", null, 0, 0, {
phase: null, current_file: null, files_done: 0, files_total: 0, phase: null, current_file: null, files_done: 0, files_total: 0,
@@ -957,9 +1070,13 @@ test-document-viewer pattern). The pinned "· chunk n/m" part (the
* import, batch in flight (chunks_total 38 > 0): the chunk part * import, batch in flight (chunks_total 38 > 0): the chunk part
shows "· chunk 14/38" and the UNTRUNCATED title + #sync-progress shows "· chunk 14/38" and the UNTRUNCATED title + #sync-progress
(the sole live region) read exactly (the sole live region) read exactly
"Syncing… src/a/b.md (2/3) · chunk 14/38" (A4); "Syncing… src/a/b.md 1 remaining · chunk 14/38" (A4 — phase
193: the 2/3 fixture now renders the distance to the end);
* import before the first batch (0/0): the chunk part hides and * import before the first batch (0/0): the chunk part hides and
the label stays "Syncing… src/a/b.md (2/3)"; the label stays "Syncing… src/a/b.md 1 remaining";
* import on the FINAL file (phase 193, A1: files_done ==
files_total): the COUNT part hides (no "0 remaining") — the
main part alone names the file in flight;
* every OTHER state — prelude (null phase), the 202/409 click (no * every OTHER state — prelude (null phase), the 202/409 click (no
status), the upload run (no status, D2), overview, summaries (its status), the upload run (no status, D2), overview, summaries (its
own (n/m) is its position), the idle settle — leaves the chunk own (n/m) is its position), the idle settle — leaves the chunk
@@ -982,24 +1099,39 @@ test-document-viewer pattern). The pinned "· chunk n/m" part (the
# untruncated label rides the title + the announcer (A4). # untruncated label rides the title + the announcer (A4).
row = by_tag["import-inflight"] row = by_tag["import-inflight"]
assert row[3] == "Syncing… src/a/b.md", row assert row[3] == "Syncing… src/a/b.md", row
assert row[4] == "(2/3)" and row[5] is False, row assert row[4] == "1 remaining" and row[5] is False, (
"phase 193 (D1): the count part is the count-DOWN (3 − 2 = 1)"
)
assert row[6] == "· chunk 14/38" and row[7] is False, ( assert row[6] == "· chunk 14/38" and row[7] is False, (
"the chunk part shows the batch position (1-based first chunk / file total)" "the chunk part shows the batch position (1-based first chunk / file total)"
) )
assert row[1] == "Syncing… src/a/b.md (2/3) · chunk 14/38", row[1] assert row[1] == "Syncing… src/a/b.md 1 remaining · chunk 14/38", row[1]
assert row[2] == row[1], "the announcer hears the whole thing (A4)" assert row[2] == row[1], "the announcer hears the whole thing (A4)"
# 2. the import before the first batch (0/0) — the part hides, the # 2. the import before the first batch (0/0) — the chunk part
# label is the byte-identical phase-64 string. # hides, the label keeps its phase-64 shape with the phase-193
# count part.
row = by_tag["import-prebatch"] row = by_tag["import-prebatch"]
assert row[1] == "Syncing… src/a/b.md (2/3)", row[1] assert row[1] == "Syncing… src/a/b.md 1 remaining", row[1]
assert row[2] == row[1] assert row[2] == row[1]
assert row[6] == "" and row[7] is True, row assert row[6] == "" and row[7] is True, row
# 2b. the FINAL file (N == 0) — the count part HIDES (phase 193,
# A1): no "0 remaining", the main part names the file, and
# the A4 full label is just the main part.
row = by_tag["import-final"]
assert row[3] == "Syncing… src/a/b.md", row
assert row[4] == "" and row[5] is True, (
"the count part hides at the final file (total - done == 0)"
)
assert row[1] == "Syncing… src/a/b.md", row[1]
assert row[2] == row[1]
# 3. every other RUNNING state — the chunk part is hidden + # 3. every other RUNNING state — the chunk part is hidden +
# empty, the title is the byte-identical pre-phase-129 label, # empty, the title is the byte-identical pre-phase-129 label,
# and the announcer (the box) hears the whole label (A4). # and the announcer (the box) hears the whole label (A4).
for tag, expected_title in ( for tag, expected_title in (
("import-final", "Syncing… src/a/b.md"),
("prelude", "Syncing…"), ("prelude", "Syncing…"),
("click", "Syncing…"), ("click", "Syncing…"),
("upload", "Importing"), ("upload", "Importing"),
@@ -1029,7 +1161,15 @@ test-document-viewer pattern). The pinned "· chunk n/m" part (the
# upload job is NOT cancellable in this phase) and hidden + # upload job is NOT cancellable in this phase) and hidden +
# disabled after the real settle (settleSyncButton always hides # disabled after the real settle (settleSyncButton always hides
# it — every terminal + idle passes through it). # it — every terminal + idle passes through it).
for tag in ("import-inflight", "import-prebatch", "prelude", "click", "overview", "summaries"): for tag in (
"import-inflight",
"import-prebatch",
"import-final",
"prelude",
"click",
"overview",
"summaries",
):
row = by_tag[tag] row = by_tag[tag]
assert row[8] is False and row[9] is False, ( assert row[8] is False and row[9] is False, (
f"{tag}: the Cancel control must be shown + enabled while a sync run is in flight" f"{tag}: the Cancel control must be shown + enabled while a sync run is in flight"
@@ -1053,14 +1193,15 @@ def test_image_part_renders_only_in_the_import_phase_under_node() -> None:
WITH the last chunk position still on the status (125/126): WITH the last chunk position still on the status (125/126):
the composed full string carries BOTH parts — the box, the the composed full string carries BOTH parts — the box, the
button title, and the running aria-label all read exactly button title, and the running aria-label all read exactly
"Syncing… src/a/b.md (2/3) · chunk 125/126 · image 3/15" "Syncing… src/a/b.md 1 remaining · chunk 125/126 · image 3/15"
(the owner's stuck label, now moving); the chunk part KEEPS (the owner's stuck label, now moving; phase 193: the count part
is the count-DOWN); the chunk part KEEPS
its span ("· chunk 125/126" visible) and the image part gets its span ("· chunk 125/126" visible) and the image part gets
NO span of its own (it rides the composed string only — the NO span of its own (it rides the composed string only — the
running button is the spinner, phase 149); running button is the spinner, phase 149);
* import, a purely pictorial PDF (the chunk position never left * import, a purely pictorial PDF (the chunk position never left
0/0) in its vision span: the image part shows ALONE — 0/0) in its vision span: the image part shows ALONE —
"Syncing… src/a/b.md (2/3) · image 7/7" — and the chunk part "Syncing… src/a/b.md 1 remaining · image 7/7" — and the chunk part
stays hidden + empty (the two parts are distinct and each stays hidden + empty (the two parts are distinct and each
disappears independently on its own 0/0); disappears independently on its own 0/0);
* every OTHER running state (the pre-batch import, the prelude, * every OTHER running state (the pre-batch import, the prelude,
@@ -1082,7 +1223,7 @@ def test_image_part_renders_only_in_the_import_phase_under_node() -> None:
# (the image part LAST, after the chunk part), on the box, the # (the image part LAST, after the chunk part), on the box, the
# title, and the running aria-label (A4) — the chunk span # title, and the running aria-label (A4) — the chunk span
# keeps its own part. # keeps its own part.
full = "Syncing… src/a/b.md (2/3) · chunk 125/126 · image 3/15" full = "Syncing… src/a/b.md 1 remaining · chunk 125/126 · image 3/15"
row = by_tag["import-image-span"] row = by_tag["import-image-span"]
assert row[1] == full, row[1] assert row[1] == full, row[1]
assert row[2] == full, "the announcer (the box) hears the whole thing (A4)" assert row[2] == full, "the announcer (the box) hears the whole thing (A4)"
@@ -1095,7 +1236,7 @@ def test_image_part_renders_only_in_the_import_phase_under_node() -> None:
# 1c. the image part alone (the chunk position never left 0/0 — # 1c. the image part alone (the chunk position never left 0/0 —
# the two parts are independent). # the two parts are independent).
row = by_tag["import-image-only"] row = by_tag["import-image-only"]
assert row[1] == "Syncing… src/a/b.md (2/3) · image 7/7", row[1] assert row[1] == "Syncing… src/a/b.md 1 remaining · image 7/7", row[1]
assert row[2] == row[1] and row[14] == row[1], row assert row[2] == row[1] and row[14] == row[1], row
assert row[6] == "" and row[7] is True, ( assert row[6] == "" and row[7] is True, (
"the chunk part stays hidden + empty (its own 0/0)" "the chunk part stays hidden + empty (its own 0/0)"
@@ -1106,8 +1247,9 @@ def test_image_part_renders_only_in_the_import_phase_under_node() -> None:
# hides on its own 0/0 — the overview/summaries transitions # hides on its own 0/0 — the overview/summaries transitions
# clear the position server-side, the bare builders carry none). # clear the position server-side, the bare builders carry none).
for tag, expected_title in ( for tag, expected_title in (
("import-inflight", "Syncing… src/a/b.md (2/3) · chunk 14/38"), ("import-inflight", "Syncing… src/a/b.md 1 remaining · chunk 14/38"),
("import-prebatch", "Syncing… src/a/b.md (2/3)"), ("import-prebatch", "Syncing… src/a/b.md 1 remaining"),
("import-final", "Syncing… src/a/b.md"),
("prelude", "Syncing…"), ("prelude", "Syncing…"),
("click", "Syncing…"), ("click", "Syncing…"),
("upload", "Importing"), ("upload", "Importing"),
@@ -1176,6 +1318,7 @@ def test_progress_box_and_running_button_contract_under_node() -> None:
for tag in ( for tag in (
"import-inflight", "import-inflight",
"import-prebatch", "import-prebatch",
"import-final",
"prelude", "prelude",
"click", "click",
"upload", "upload",
+4 -3
View File
@@ -268,8 +268,9 @@ def test_sync_button_has_icon_and_label_parts() -> None:
Phase 128 (task 01): #sync-label is PARTS — the MAIN span Phase 128 (task 01): #sync-label is PARTS — the MAIN span
(.sync-label-main) carries the idle text (the pre-JS label renders (.sync-label-main) carries the idle text (the pre-JS label renders
from the static markup; the part CSS may ellipsize it), the from the static markup; the part CSS may ellipsize it), the
COUNT span (.sync-label-count — the pinned "(n/m)" position, COUNT span (.sync-label-count — the pinned phase-193
never clipped) ships hidden + empty (there is no count at idle), "N remaining" position, never clipped) ships hidden + empty
(there is no count at idle),
and — phase 129 (task 04) — the CHUNK span (.sync-label-chunk — and — phase 129 (task 04) — the CHUNK span (.sync-label-chunk —
the pinned "· chunk n/m" embed-batch position) ships hidden + the pinned "· chunk n/m" embed-batch position) ships hidden +
empty too (no chunk position at idle — the part renders only empty too (no chunk position at idle — the part renders only
@@ -516,7 +517,7 @@ def test_fmt_sync_phase_label_image_part_is_pinned_source_level() -> None:
`running` flag); `running` flag);
* ``setSyncLabel``'s composed ``full`` string appends the image * ``setSyncLabel``'s composed ``full`` string appends the image
part LAST (after the chunk part) — so a status carrying both part LAST (after the chunk part) — so a status carrying both
positions reads exactly "Syncing… <file> (2540/2540) · chunk positions reads exactly "Syncing… <file> 1 remaining · chunk
125/126 · image 3/15" on the box, the title, and the running 125/126 · image 3/15" on the box, the title, and the running
aria-label (byte-for-byte; the docstring's worked example aria-label (byte-for-byte; the docstring's worked example
carries the shape). carries the shape).