phase: 154_sync_status_freshness
Build and Push Containers / build-and-push-app (push) Successful in 2m26s
Build and Push Containers / build-and-push-db (push) Successful in 16s

All 4 task files exist in `complete/`, and only `00_phase.md` remains in `todo/`. This is the final verification pass. I'll read through the task files and check the current state of the code.
This commit is contained in:
2026-10-04 14:24:33 -04:00
parent 966c4b0540
commit c275735db4
36 changed files with 2526 additions and 99 deletions
@@ -0,0 +1 @@
All 4 task files exist in `complete/`, and only `00_phase.md` remains in `todo/`. This is the final verification pass. I'll read through the task files and check the current state of the code.
@@ -0,0 +1,121 @@
........................................................................ [ 2%]
........................................................................ [ 4%]
........................................................................ [ 6%]
........................................................................ [ 8%]
........................................................................ [ 10%]
........................................................................ [ 12%]
........................................................................ [ 14%]
........................................................................ [ 16%]
........................................................................ [ 18%]
........................................................................ [ 20%]
........................................................................ [ 22%]
........................................................................ [ 24%]
........................................................................ [ 26%]
........................................................................ [ 28%]
........................................................................ [ 30%]
........................................................................ [ 32%]
........................................................................ [ 34%]
........................................................................ [ 36%]
........................................................................ [ 38%]
........................................................................ [ 40%]
........................................................................ [ 42%]
........................................................................ [ 44%]
........................................................................ [ 46%]
........................................................................ [ 48%]
........................................................................ [ 50%]
........................................................................ [ 52%]
........................................................................ [ 54%]
........................................................................ [ 56%]
........................................................................ [ 58%]
........................................................................ [ 60%]
........................................................................ [ 62%]
........................................................................ [ 64%]
........................................................................ [ 66%]
........................................................................ [ 68%]
........................................................................ [ 70%]
........................................................................ [ 72%]
........................................................................ [ 75%]
........................................................................ [ 77%]
........................................................................ [ 79%]
........................................................................ [ 81%]
........................................................................ [ 83%]
........................................................................ [ 85%]
........................................................................ [ 87%]
........................................................................ [ 89%]
........................................................................ [ 91%]
........................................................................ [ 93%]
........................................................................ [ 95%]
........................................................................ [ 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/auth.py 52 0 100%
app/api/chat.py 270 1 99%
app/api/chat_images.py 50 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 13 0 100%
app/api/doc_drafts.py 99 0 100%
app/api/docs.py 196 1 99%
app/api/git_sources.py 259 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 33 0 100%
app/api/sync.py 204 0 100%
app/api/tokens.py 40 0 100%
app/api/ui_settings.py 55 0 100%
app/config.py 266 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/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 20 0 100%
app/core/theming.py 38 0 100%
app/core/tokens.py 44 0 100%
app/db.py 22 0 100%
app/main.py 68 0 100%
app/models.py 135 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 431 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 764 4 99%
app/rag/llm.py 246 1 99%
app/rag/md_images.py 63 0 100%
app/rag/overview.py 71 0 100%
app/rag/pdf.py 192 0 100%
app/rag/prompts.py 155 0 100%
app/rag/retriever.py 248 3 99%
app/rag/scaffolding.py 55 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/schemas.py 443 0 100%
--------------------------------------------------
TOTAL 5719 17 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,12 @@
Everything is green. Final verification of the complete gate sequence was already done: full suite (unit + integration) 3546 passed, ruff clean, pyright 0 errors, app/ coverage 99%.
**Report — Phase 154, Task 01 (RAG sync badge date-qualified)**
- `frontend/assets/sources.js::fmtSyncTime` contract widened to the A1 date-qualifier (name kept, `applySyncSuccess` shape unchanged): same local day → `HH:MM` (byte-identical to pre-phase), same year → `M/D, h:MM AM/PM`, other year → `M/D/YYYY, h:MM AM/PM`; 12-hour without leading zero (`12:05 AM` edge), parse guard unchanged (`garbage`/missing → `""` → bare "Synced")
- Parts-writer comment block (L821+) names the qualified stamp where the label parts are listed (cited, not restated); the five other stale `Synced HH:MM` comment cites in the file updated for accuracy
- No CSS change (longer stamp rides the same ellipsized label part)
- Unit pins in `tests/unit/test_sync_button.py`: source-level qualifier pins + the real `fmtSyncTime` run under node with a FAKE clock (patched `globalThis.Date`, fixed now Oct 3 2026 18:30 local; all ISOs designator-less — the wall clock never appears in an assertion): same-day/midnight/end-of-day, yesterday (3:14 PM, 12:05 AM, 12:07 PM, 11:58 PM), last-year (10/3/2025, 9:12 PM; 1/3/2025, 9:12 AM), garbage/empty/undefined → `""`; the `null`-epoch edge pinned TZ-independently (null ≡ epoch-ISO, dated+year form); settle-string test: `Synced 16:06` (byte-identical), `Synced 10/2, 3:14 PM`, bare `Synced` fallback
- Results: `uv run pytest` → **3546 passed** (exit 0); `uv run pytest --cov=app` → TOTAL **99%** (>90%); `uv run ruff check . && uv run pyright` → clean (0 errors); `node --check sources.js` OK
- Docs: README.md sync-settle paragraph now names the date-qualified stamp; AGENTS.md had no mention — no change
- Notable: zero server change (frontend-only per phase design); pre-existing TODO.md working-tree change left untouched (owner's file swap)
- Next pending task: `.agents/phases/todo/154_sync_status_freshness/02_upload_line_stamp.md`
@@ -0,0 +1,121 @@
........................................................................ [ 2%]
........................................................................ [ 4%]
........................................................................ [ 6%]
........................................................................ [ 8%]
........................................................................ [ 10%]
........................................................................ [ 12%]
........................................................................ [ 14%]
........................................................................ [ 16%]
........................................................................ [ 18%]
........................................................................ [ 20%]
........................................................................ [ 22%]
........................................................................ [ 24%]
........................................................................ [ 26%]
........................................................................ [ 28%]
........................................................................ [ 30%]
........................................................................ [ 32%]
........................................................................ [ 34%]
........................................................................ [ 36%]
........................................................................ [ 38%]
........................................................................ [ 40%]
........................................................................ [ 42%]
........................................................................ [ 44%]
........................................................................ [ 46%]
........................................................................ [ 48%]
........................................................................ [ 50%]
........................................................................ [ 52%]
........................................................................ [ 54%]
........................................................................ [ 56%]
........................................................................ [ 58%]
........................................................................ [ 60%]
........................................................................ [ 62%]
........................................................................ [ 64%]
........................................................................ [ 67%]
........................................................................ [ 69%]
........................................................................ [ 71%]
........................................................................ [ 73%]
........................................................................ [ 75%]
........................................................................ [ 77%]
........................................................................ [ 79%]
........................................................................ [ 81%]
........................................................................ [ 83%]
........................................................................ [ 85%]
........................................................................ [ 87%]
........................................................................ [ 89%]
........................................................................ [ 91%]
........................................................................ [ 93%]
........................................................................ [ 95%]
........................................................................ [ 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/auth.py 52 0 100%
app/api/chat.py 270 1 99%
app/api/chat_images.py 50 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 13 0 100%
app/api/doc_drafts.py 99 0 100%
app/api/docs.py 196 1 99%
app/api/git_sources.py 259 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 33 0 100%
app/api/sync.py 204 0 100%
app/api/tokens.py 40 0 100%
app/api/ui_settings.py 55 0 100%
app/config.py 266 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/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 20 0 100%
app/core/theming.py 38 0 100%
app/core/tokens.py 44 0 100%
app/db.py 22 0 100%
app/main.py 68 0 100%
app/models.py 135 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 431 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 764 4 99%
app/rag/llm.py 246 1 99%
app/rag/md_images.py 63 0 100%
app/rag/overview.py 71 0 100%
app/rag/pdf.py 192 0 100%
app/rag/prompts.py 155 0 100%
app/rag/retriever.py 248 3 99%
app/rag/scaffolding.py 55 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/schemas.py 443 0 100%
--------------------------------------------------
TOTAL 5719 17 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,9 @@
Task 02 complete — all gates green. Report:
**Phase 154, task 02 — upload-line completion stamp (frontend-only, zero server change)**
- `git-sources.js`: `fmtUploadResult(detail, name, when)` — 4 shapes: `Uploaded <name> (<stamp>) — …`, `Uploaded <name> — …`, `Uploaded <stamp> — …`, `Uploaded — …`; `when === ""` renders byte-identical pre-phase strings; non-`uploaded` branch ("The upload finished.") unchanged. New `fmtUploadStamp(status)`: finished_at first, started_at fallback (corrupt finished degrades to started stamp), `""` when neither parses — A1 qualifier duplicated from sources.js's `fmtSyncTime` (cited, not restated). Both render sites (poll settle + boot re-attach) pass `fmtUploadStamp(status)`; header + section comments updated to the stamped shapes.
- Unit pins (`test_frontend_sync_upload.py`): 3-arg signature + all 4 shape templates; ok-list/boot-reattach regex asserts for the 3-arg call; new `fmtUploadStamp` source pin (finished-before-started, skips, `|| {}` guard, full A1 structure) + real-node behavioral tests — 4 shapes (incl. legacy 2-arg call → pre-phase string) and fixed-clock stamp (finished wins / started fallback / `""` cases / corrupt-finish degrade / same-day, yesterday, last-year, `12:05 AM` + end-of-day edges).
- 8 stale E2E files (exact-string `to_have_text` / `==` pins) updated to same-day `HH:MM` regex helpers (incl. nameless boot re-render, self-dating) — verified in isolation: sync_upload_progress 4✓, upload_no_scan 3✓, archive_upload_sources 5✓, answer_images 5✓.
- Gates: full unit+integration **3549 passed**; `ruff` clean; `pyright` 0/0; `--cov=app` **99%**; `app/` untouched (no server change, per design).
- Docs: README/AGENTS.md don't document the result-line text — **no doc change needed**. Next pending task: `03_unit_pins.md`.
@@ -0,0 +1,121 @@
........................................................................ [ 2%]
........................................................................ [ 4%]
........................................................................ [ 6%]
........................................................................ [ 8%]
........................................................................ [ 10%]
........................................................................ [ 12%]
........................................................................ [ 14%]
........................................................................ [ 16%]
........................................................................ [ 18%]
........................................................................ [ 20%]
........................................................................ [ 22%]
........................................................................ [ 24%]
........................................................................ [ 26%]
........................................................................ [ 28%]
........................................................................ [ 30%]
........................................................................ [ 32%]
........................................................................ [ 34%]
........................................................................ [ 36%]
........................................................................ [ 38%]
........................................................................ [ 40%]
........................................................................ [ 42%]
........................................................................ [ 44%]
........................................................................ [ 46%]
........................................................................ [ 48%]
........................................................................ [ 50%]
........................................................................ [ 52%]
........................................................................ [ 54%]
........................................................................ [ 56%]
........................................................................ [ 58%]
........................................................................ [ 60%]
........................................................................ [ 62%]
........................................................................ [ 64%]
........................................................................ [ 66%]
........................................................................ [ 68%]
........................................................................ [ 71%]
........................................................................ [ 73%]
........................................................................ [ 75%]
........................................................................ [ 77%]
........................................................................ [ 79%]
........................................................................ [ 81%]
........................................................................ [ 83%]
........................................................................ [ 85%]
........................................................................ [ 87%]
........................................................................ [ 89%]
........................................................................ [ 91%]
........................................................................ [ 93%]
........................................................................ [ 95%]
........................................................................ [ 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/auth.py 52 0 100%
app/api/chat.py 270 1 99%
app/api/chat_images.py 50 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 13 0 100%
app/api/doc_drafts.py 99 0 100%
app/api/docs.py 196 1 99%
app/api/git_sources.py 259 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 33 0 100%
app/api/sync.py 204 0 100%
app/api/tokens.py 40 0 100%
app/api/ui_settings.py 55 0 100%
app/config.py 266 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/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 20 0 100%
app/core/theming.py 38 0 100%
app/core/tokens.py 44 0 100%
app/db.py 22 0 100%
app/main.py 68 0 100%
app/models.py 135 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 431 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 764 4 99%
app/rag/llm.py 246 1 99%
app/rag/md_images.py 63 0 100%
app/rag/overview.py 71 0 100%
app/rag/pdf.py 192 0 100%
app/rag/prompts.py 155 0 100%
app/rag/retriever.py 248 3 99%
app/rag/scaffolding.py 55 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/schemas.py 443 0 100%
--------------------------------------------------
TOTAL 5719 17 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,12 @@
Task 03 complete — all gates green.
**Report — Phase 154, Task 03 (unit pins: qualifier + the four stamp shapes)**
- `tests/unit/test_sync_button.py`: `PRE_PHASE_FMT_SYNC_TIME_JS` — the pre-phase `fmtSyncTime` pinned **verbatim** from git history (name renamed, body byte-for-byte) + `test_sources_js_fmt_sync_time_same_day_is_byte_identical_to_pre_phase`: real vs reference side-by-side on the shared fixed clock — same-day + fallbacks byte-identical (incl. the audit `16:06` settle), the divergent set (yesterday/last-year/epoch) byte-different, reference self-checked against the verbatim old strings (assertable, not assumed)
- `tests/unit/test_frontend_sync_upload.py`: `PRE_PHASE_FMT_UPLOAD_RESULT_JS` (pre-phase 2-arg pinned verbatim) + verbatim old-line constants + `test_upload_result_no_stamp_is_byte_identical_to_pre_phase` — the two no-stamp fallbacks + the legacy 2-arg call byte-identical to the reference/verbatim strings; non-`uploaded` branch unchanged on both sides; module docstring gains the task-03 note
- `fmtUploadStamp` (finished/started/missing/garbage) + the four shapes: already pinned by task 02's `test_upload_stamp_behavior_fixed_clock` / `test_upload_result_four_shapes_under_node` — verified green, additions only
- Payload pin (work item 2): `test_idle_upload_status_pins_full_shape` (phase-64 full-dict pin) **unchanged and green** (42/42 in `test_git_sources_upload.py`); `git diff -- app/` empty — the no-server-change regression guard holds
- Defect fixed: the dev DB was stale (alembic 0003 vs head 0026) breaking ~30 DB-backed tests — applied the documented `uv run alembic upgrade head`
- Results: `uv run pytest tests/unit -q` → 2803 passed (exit 0); `uv run pytest -q` (unit+integration) → **3563 passed** (exit 0); `uv run pytest --cov=app` → **99%** (>90%); `uv run ruff check . && uv run pyright` → clean (0/0/0)
- Docs: no doc impact (unit pins only; README updated by task 01, AGENTS.md doesn't describe the stamps)
- Next pending task: `.agents/phases/todo/154_sync_status_freshness/04_e2e_status_freshness.md`
@@ -0,0 +1,121 @@
........................................................................ [ 2%]
........................................................................ [ 4%]
........................................................................ [ 6%]
........................................................................ [ 8%]
........................................................................ [ 10%]
........................................................................ [ 12%]
........................................................................ [ 14%]
........................................................................ [ 16%]
........................................................................ [ 18%]
........................................................................ [ 20%]
........................................................................ [ 22%]
........................................................................ [ 24%]
........................................................................ [ 26%]
........................................................................ [ 28%]
........................................................................ [ 30%]
........................................................................ [ 32%]
........................................................................ [ 34%]
........................................................................ [ 36%]
........................................................................ [ 38%]
........................................................................ [ 40%]
........................................................................ [ 42%]
........................................................................ [ 44%]
........................................................................ [ 46%]
........................................................................ [ 48%]
........................................................................ [ 50%]
........................................................................ [ 52%]
........................................................................ [ 54%]
........................................................................ [ 56%]
........................................................................ [ 58%]
........................................................................ [ 60%]
........................................................................ [ 62%]
........................................................................ [ 64%]
........................................................................ [ 66%]
........................................................................ [ 68%]
........................................................................ [ 70%]
........................................................................ [ 72%]
........................................................................ [ 75%]
........................................................................ [ 77%]
........................................................................ [ 79%]
........................................................................ [ 81%]
........................................................................ [ 83%]
........................................................................ [ 85%]
........................................................................ [ 87%]
........................................................................ [ 89%]
........................................................................ [ 91%]
........................................................................ [ 93%]
........................................................................ [ 95%]
........................................................................ [ 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/auth.py 52 0 100%
app/api/chat.py 270 1 99%
app/api/chat_images.py 50 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 13 0 100%
app/api/doc_drafts.py 99 0 100%
app/api/docs.py 196 1 99%
app/api/git_sources.py 259 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 33 0 100%
app/api/sync.py 204 0 100%
app/api/tokens.py 40 0 100%
app/api/ui_settings.py 55 0 100%
app/config.py 266 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/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 20 0 100%
app/core/theming.py 38 0 100%
app/core/tokens.py 44 0 100%
app/db.py 22 0 100%
app/main.py 68 0 100%
app/models.py 135 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 431 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 764 4 99%
app/rag/llm.py 246 1 99%
app/rag/md_images.py 63 0 100%
app/rag/overview.py 71 0 100%
app/rag/pdf.py 192 0 100%
app/rag/prompts.py 155 0 100%
app/rag/retriever.py 248 3 99%
app/rag/scaffolding.py 55 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/schemas.py 443 0 100%
--------------------------------------------------
TOTAL 5719 17 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,13 @@
All gates green. Task 04 complete.
**Report**
- **Implemented:** `tests/e2e/test_sync_status_freshness.py` — the phase-154 dedicated Playwright suite (mock-only, no `slow_llm` needed):
1. `test_fresh_sync_settles_same_day_shape` — real mock sync settles `#sync-btn` to `^Synced \d{2}:\d{2}$` (shape pin, never a wall-clock comparison) + real-run proof (counts line, terminal `finished_at` non-null)
2. `test_upload_line_fresh_settle_carrys_name_and_stamp` — real archive upload → 202 toast → settle line `Uploaded <name> (<stamp>) — press Sync sources to import it.` (stamp capture asserted non-empty; terminal no-count payload + non-null `finished_at`; row + host folder; zero docs indexed)
3. `test_upload_line_boot_rerender_stamped_nameless` — **the phase's fix**: settle, then reload → `initUploadStatus` success branch renders the NAMELESS line WITH the stamp (from the server payload); run identity pinned on `started_at` (no second upload); page boots honest (no toast/error, button restored)
4. `test_running_upload_line_unchanged` — regression guard: deterministic in-flight observation (the 202 handler's ≥2 s `Processing…` window before the first poll tick) asserts the bare label + hidden result line; line appears ONLY on settle (distinct safe name proves the stale boot line was replaced)
- **Gates:** `uv run pytest tests/e2e/test_sync_status_freshness.py -v --no-cov` → **4 passed** (run 4×, no flake); `uv run pytest` (unit+integration) → **exit 0, 100% green**; `uv run pytest --cov=app` → **app/ 99%** (>90%); `uv run ruff check .` → clean; `uv run pyright` → **0 errors**; cross-check `test_upload_no_scan.py` in isolation → 3 passed. `app/` untouched (phase is frontend-only).
- **Docs:** README "Archive upload sources" gained the ready-for-sync line with the date-qualified completion stamp + both render paths (the sync badge was already named by tasks 01/02); AGENTS.md — no impact (no new commands/env/rules).
- **Decisions:** sync label pinned exactly as the task specifies (`Synced \d{2}:\d{2}`); upload stamp uses the task's dual shape regex (same-day + date-qualified, midnight-safe); no delay proxy — the upload's 2 s poll window makes the in-flight observation deterministic; test-4 pre-upload hidden-assertion removed (shared module app's boot re-render of the prior test's line is the intended phase-154 behavior, not a regression).
- **Next pending task:** none — 154 is fully complete (01–04); phase can move to `complete/`.
@@ -0,0 +1,121 @@
........................................................................ [ 2%]
........................................................................ [ 4%]
........................................................................ [ 6%]
........................................................................ [ 8%]
........................................................................ [ 10%]
........................................................................ [ 12%]
........................................................................ [ 14%]
........................................................................ [ 16%]
........................................................................ [ 18%]
........................................................................ [ 20%]
........................................................................ [ 22%]
........................................................................ [ 24%]
........................................................................ [ 26%]
........................................................................ [ 28%]
........................................................................ [ 30%]
........................................................................ [ 32%]
........................................................................ [ 34%]
........................................................................ [ 36%]
........................................................................ [ 38%]
........................................................................ [ 40%]
........................................................................ [ 42%]
........................................................................ [ 44%]
........................................................................ [ 46%]
........................................................................ [ 48%]
........................................................................ [ 50%]
........................................................................ [ 52%]
........................................................................ [ 54%]
........................................................................ [ 56%]
........................................................................ [ 58%]
........................................................................ [ 60%]
........................................................................ [ 62%]
........................................................................ [ 64%]
........................................................................ [ 66%]
........................................................................ [ 68%]
........................................................................ [ 70%]
........................................................................ [ 72%]
........................................................................ [ 75%]
........................................................................ [ 77%]
........................................................................ [ 79%]
........................................................................ [ 81%]
........................................................................ [ 83%]
........................................................................ [ 85%]
........................................................................ [ 87%]
........................................................................ [ 89%]
........................................................................ [ 91%]
........................................................................ [ 93%]
........................................................................ [ 95%]
........................................................................ [ 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/auth.py 52 0 100%
app/api/chat.py 270 1 99%
app/api/chat_images.py 50 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 13 0 100%
app/api/doc_drafts.py 99 0 100%
app/api/docs.py 196 1 99%
app/api/git_sources.py 259 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 33 0 100%
app/api/sync.py 204 0 100%
app/api/tokens.py 40 0 100%
app/api/ui_settings.py 55 0 100%
app/config.py 266 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/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 20 0 100%
app/core/theming.py 38 0 100%
app/core/tokens.py 44 0 100%
app/db.py 22 0 100%
app/main.py 68 0 100%
app/models.py 135 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 431 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 764 4 99%
app/rag/llm.py 246 1 99%
app/rag/md_images.py 63 0 100%
app/rag/overview.py 71 0 100%
app/rag/pdf.py 192 0 100%
app/rag/prompts.py 155 0 100%
app/rag/retriever.py 248 3 99%
app/rag/scaffolding.py 55 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/schemas.py 443 0 100%
--------------------------------------------------
TOTAL 5719 17 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
+16 -3
View File
@@ -306,6 +306,15 @@ first).
- Re-uploading the same filename replaces the source **in place** (no
second folder, no duplicate row).
- After the 202 the form settles into the ready-for-sync line
"Uploaded <source> (<stamp>) — press Sync sources to import it." —
the completion stamp (phase 154: the same date-qualified freshness
rule as the sync badge — `HH:MM` on the same local day,
`M/D, h:MM AM/PM` otherwise, `M/D/YYYY` across years) rides the
line on EVERY render path, so a line that outlives its upload is
self-dating: after a reload the source name is gone (it was
page-local) but the stamp survives, coming from the server
payload's `finished_at`.
- Zip-bomb guard: absolute member paths, `..` traversal, and symlinks
escaping the unpack folder are rejected.
- Archives unpack under `BOR_UPLOAD_DIR` (default `~/bor-sources/uploads`).
@@ -358,9 +367,13 @@ whole git-source refresh in one click:
pre-phase result-line announcer into the box). At settle the box
STAYS visible to carry the result line — the counts line
("N added · …") after a success, "Sync cancelled" after a cancel —
and the button leaves the running state (the "Synced HH:MM" pill
after a success, the retry-ready "Sync sources" pill after a
failure); the box hides only when its text is empty (a failed sync
and the button leaves the running state (the "Synced <stamp>" pill
after a success — the date-qualified freshness stamp (phase 154):
`HH:MM` on the same local day, `M/D, h:MM AM/PM` otherwise
(`M/D/YYYY` across years — a badge that tells the owner *when*),
the bare "Synced" when the stamp is missing — the retry-ready
"Sync sources" pill after a failure); the box hides only when its
text is empty (a failed sync
— the banner + modal carry the error — and the true idle). No
client-side timeout — a clone + embed can legitimately take minutes.
- One sync at a time: a second trigger while a run is in flight gets a
+91 -18
View File
@@ -75,8 +75,12 @@
* "Processing…" label for the WHOLE run (phase 90, A2 — the run
* is unpack + register only: no file, no "(n/m)" counts, no
* title) and settles it: success → the ready-for-sync line
* ("Uploaded <name> — press Sync sources to import it.",
* fmtUploadResult off the status's {"message": "uploaded"}
* ("Uploaded <name> (<stamp>) — press Sync sources to import
* it." — the phase-154 completion stamp, fmtUploadStamp off the
* status's finished_at, the A1 date-qualified form cited in
* sources.js's fmtSyncTime, not restated; a running / partial
* payload without timestamps renders the byte-identical pre-phase
* line — fmtUploadResult off the status's {"message": "uploaded"}
* detail — the role=status result line) + the "Archive uploaded
* — press Sync sources to import it." announce + loadSources
* (the new/updated row lands with the Local badge; a re-upload
@@ -93,8 +97,10 @@
* (a reload mid-run re-attaches — no second upload), a terminal
* run re-renders its result line / error banner (the safe name
* was page-local — lastUploadName is null after a reload — so the
* re-rendered line is the nameless "Uploaded — press Sync sources
* to import it.").
* re-rendered line is the nameless "Uploaded <stamp> — press
* Sync sources to import it." — the stamp comes from the SERVER
* payload, so the line is self-dating even with the name
* unknown, phase 154).
* • remove — a row's Remove button opens the page-local
* confirmation modal (#remove-confirm-dialog, a real
* role="alertdialog" — the native confirm() retired, phase 69):
@@ -1096,7 +1102,10 @@ export async function mount(root) {
* "Processing…" label for the WHOLE run (phase 90, A2 — the unpack
* has no file-level progress: no file, no "(n/m)" counts, no title)
* and settles it: success → the ready-for-sync line ("Uploaded
* <name> — press Sync sources to import it.", fmtUploadResult off
* <name> (<stamp>) — press Sync sources to import it." — the
* phase-154 completion stamp, fmtUploadStamp off the status's
* finished_at; the A1 date-qualified form is cited in
* fmtUploadResult's comment, not restated — fmtUploadResult off
* the status's {"message": "uploaded"} detail) in the role=status
* result line + the "Archive uploaded — press Sync sources to
* import it." announce + loadSources (NO second toast — it already
@@ -1117,23 +1126,75 @@ export async function mount(root) {
* (The phase-49 synchronous 200 paragraph is superseded by phase 64;
* the phase-64 scan counts are superseded by phase 90.) */
/* The result line's text (phase 90, A2 — the no-count contract): the
/* The result line's text (phase 90, A2 — the no-count contract,
extended by phase 154 task 02 with the completion stamp): the
status success detail is exactly {"message": "uploaded"} — the
sync-style counts the phase-64 line rendered are gone (the scan —
and its counts — belong to the Sync button, which renders them on
the RAG page). `name` is the accepted 202's safe source name
(lastUploadName) when the run started on this page; it is null
after a reload or on the 409 re-attach (the line still points at
the next step, only without the name). */
function fmtUploadResult(detail, name) {
the next step, only without the name). `when` is the PRECOMPUTED
completion stamp (fmtUploadStamp(status) — the phase-154 A1
date-qualified form, the rule cited in sources.js's fmtSyncTime,
not restated); "" (a running / legacy / partial payload without
timestamps) renders the byte-identical pre-phase-154 strings. */
function fmtUploadResult(detail, name, when) {
if (detail && detail.message === "uploaded") {
return name
? `Uploaded ${name} — press Sync sources to import it.`
if (name) {
return when
? `Uploaded ${name} (${when}) — press Sync sources to import it.`
: `Uploaded ${name} — press Sync sources to import it.`;
}
return when
? `Uploaded ${when} — press Sync sources to import it.`
: "Uploaded — press Sync sources to import it.";
}
return "The upload finished.";
}
/* Phase 154 (task 02): the upload line's completion stamp —
* PRECOMPUTED from the /upload/status payload (the per-page
* duplication idiom: the SAME date-qualifier as sources.js's
* fmtSyncTime, duplicated, not shared — the A1 rule — same local
* day `HH:MM`, same year `M/D, h:MM AM/PM`, across years
* `M/D/YYYY, h:MM AM/PM` — is cited there, not restated). Reads
* status.finished_at first (the success state always has it on the
* server), falls back to status.started_at (the defensive guard
* for a legacy / partial payload — a corrupt finished_at degrades
* to the started_at stamp rather than to no stamp), and yields
* "" when both are missing/absent or every ISO is unparseable
* (the pre-phase-154 byte-identical fallback — the line renders
* the pre-stamp copy). */
function fmtUploadStamp(status) {
const s = status || {};
let d = null;
for (const iso of [s.finished_at, s.started_at]) {
if (!iso) continue;
const t = new Date(iso);
if (!Number.isNaN(t.getTime())) {
d = t;
break;
}
}
if (d === null) return "";
const now = new Date();
const pad = (n) => String(n).padStart(2, "0");
if (
d.getFullYear() === now.getFullYear() &&
d.getMonth() === now.getMonth() &&
d.getDate() === now.getDate()
)
return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
const h12 = d.getHours() % 12 || 12;
const ampm = d.getHours() < 12 ? "AM" : "PM";
const date =
d.getFullYear() === now.getFullYear()
? `${d.getMonth() + 1}/${d.getDate()}`
: `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`;
return `${date}, ${h12}:${pad(d.getMinutes())} ${ampm}`;
}
/* Upload-success toast (phase 64 task 05, A2 — owner-locked): the
* "successfully uploaded" confirmation, the phase-55 share-toast
* pattern (frontend/assets/app.js) made page-local. A SINGLE node —
@@ -1237,11 +1298,16 @@ export async function mount(root) {
if (status.state === "success") {
// The run finished (unpack + register only — phase 90): the
// ready-for-sync line (fmtUploadResult reads the no-count
// detail), the announcement, the row lands. NO toast here — it
// detail + fmtUploadStamp's completion stamp — phase 154),
// the announcement, the row lands. NO toast here — it
// already fired at the 202 (A2).
const detail = status.detail || {};
if (uploadResult) {
uploadResult.textContent = fmtUploadResult(detail, lastUploadName);
uploadResult.textContent = fmtUploadResult(
detail,
lastUploadName,
fmtUploadStamp(status)
);
uploadResult.hidden = false;
}
announce("Archive uploaded — press Sync sources to import it.");
@@ -1276,10 +1342,11 @@ export async function mount(root) {
* no second upload, no error); a finished run re-renders its result
* line ONLY (no announce, no toast — the toast fired at the 202, A2;
* the name is unknown after a reload — lastUploadName is null — so
* the line is the nameless "Uploaded — press Sync sources to import
* it.", phase 90); a failed run re-renders its error banner; idle
* does nothing (and a blip is a no-op — the page boots honest
* either way). */
* the line is the nameless "Uploaded <stamp> — press Sync sources
* to import it." — the stamp from the SERVER payload, so the line
* is self-dating even with the name unknown, phase 154; phase 90);
* a failed run re-renders its error banner; idle does nothing (and a
* blip is a no-op — the page boots honest either way). */
async function initUploadStatus() {
if (!uploadBtn) return;
let status;
@@ -1299,9 +1366,15 @@ export async function mount(root) {
if (status.state === "success") {
// The last run's result line only — no announce, no toast (A2).
// The safe name was page-local (lastUploadName is null after a
// reload) — the line still points at the next step (phase 90).
// reload) — the line still points at the next step (phase 90),
// self-dating through fmtUploadStamp's completion stamp
// (phase 154 — the stamp comes from the server payload).
if (uploadResult) {
uploadResult.textContent = fmtUploadResult(status.detail, lastUploadName);
uploadResult.textContent = fmtUploadResult(
status.detail,
lastUploadName,
fmtUploadStamp(status)
);
uploadResult.hidden = false;
}
return;
+49 -7
View File
@@ -321,7 +321,9 @@
* title + #sync-progress (A4 — the screen reader hears the whole
* thing; the visible span carries main + count, the space between
* them is the CSS gap). Every other #sync-label writer — the idle /
* settle labels ("Sync sources", "Synced HH:MM"), the 202/409 click,
* settle labels ("Sync sources", "Synced <stamp>" — the date-
* qualified stamp from fmtSyncTime, phase 154: `HH:MM` on the same
* local day, the dated form otherwise), the 202/409 click,
* the initSyncButton re-attach — goes through setSyncLabel(main)
* (count hidden). The full-string shapes are byte-identical to the
* pre-task labels ("Syncing…", "Syncing… <file>",
@@ -385,7 +387,8 @@
* every settle). The `is-running`
* class travels with the spin: added in enterSyncRunningState, removed
* in settleSyncButton (every terminal + both idles pass through it).
* The settle rendering ("Synced HH:MM" + the counts line), the Cancel
* The settle rendering ("Synced <stamp>" — the date-qualified stamp,
* phase 154 — + the counts line), the Cancel
* control, the 409 double-trigger, the error banner/modal, and the
* sync status API are all UNCHANGED.
*
@@ -404,7 +407,8 @@
* writes
* the SAME untruncated full label through setSyncProgress, ONLY
* while running (the idle / settle button labels — "Sync sources",
* "Synced HH:MM" — must not fill the panel; they stay on the
* "Synced <stamp>" (the date-qualified stamp, phase 154) — must
* not fill the panel; they stay on the
* button, A2);
* • result mode — the settle paths write the result line through
* setSyncProgress: the counts line (fmtSyncResult — "N added ·
@@ -811,11 +815,40 @@ export async function mount(root) {
}
}
/* Phase 154 (task 01): the contract widens to the DATE-QUALIFIER
* (the phase design's A1 rule — owner-locked; the stamp is
* computed from the ISO's LOCAL calendar date, the page's clock —
* the table `Updated` columns already render local dates):
* • same local calendar day as now → `HH:MM` (byte-identical to
* the pre-phase output — a same-day badge is unchanged);
* • same local year, different day → `M/D, h:MM AM/PM`
* (e.g. `10/2, 3:14 PM`);
* • different local year → `M/D/YYYY, h:MM AM/PM`
* (e.g. `1/3/2025, 9:12 PM`).
* `h:MM` is the 12-hour form WITHOUT a leading zero on the hour
* (the `12:05 AM` edge), month/day without leading zero (the table
* dates' `M/D/YYYY` / `5:34:06 PM` family, the seconds dropped).
* The parse guard is UNCHANGED — an unparseable/missing ISO still
* yields "" (the bare "Synced" settle — e.g. the app-restart case,
* A12). */
function fmtSyncTime(iso) {
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return "";
const now = new Date();
const pad = (n) => String(n).padStart(2, "0");
return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
if (
d.getFullYear() === now.getFullYear() &&
d.getMonth() === now.getMonth() &&
d.getDate() === now.getDate()
)
return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
const h12 = d.getHours() % 12 || 12;
const ampm = d.getHours() < 12 ? "AM" : "PM";
const date =
d.getFullYear() === now.getFullYear()
? `${d.getMonth() + 1}/${d.getDate()}`
: `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`;
return `${date}, ${h12}:${pad(d.getMinutes())} ${ampm}`;
}
/* Phase 128 (task 01) + Phase 129 (task 04) + Phase 152 (task 03):
@@ -861,7 +894,14 @@ export async function mount(root) {
* live label (the title already carries it for hover); every
* non-running call restores the ship value "Sync sources" (the
* static HTML attribute is the idle truth — the JS only
* overwrites/restores it, never removes it). */
* overwrites/restores it, never removes it).
* Phase 154 (task 01): the settle badge among these label parts is
* date-qualified — `Synced <stamp>`, where `<stamp>` is produced
* by fmtSyncTime (its contract widened to the phase-154 A1 rule —
* same-day stays `HH:MM`, non-same-day carries the date; the rule
* is cited there, not restated). The parts themselves are
* UNCHANGED — the longer stamp is just more text in the same part
* (the label part ellipsizes as before). */
function setSyncLabel(mainText, countText = "", chunkText = "", running = false, imageText = "") {
if (syncLabelMain) syncLabelMain.textContent = mainText;
if (syncLabelCount) {
@@ -1106,7 +1146,8 @@ export async function mount(root) {
// the running state.
syncBtn.classList.remove("is-running");
// Phase 128 (task 01): the idle / settle labels ("Sync sources",
// "Synced HH:MM") go through the parts writer — count hidden
// "Synced <stamp>" — the date-qualified stamp, phase 154) go
// through the parts writer — count hidden
// (absent), the label's full string rides the title (A4, as with
// every label state). Phase 150 (task 02): the default
// `running = false` leaves the detail box alone in every
@@ -1196,7 +1237,8 @@ export async function mount(root) {
settleSyncButton(time ? `Synced ${time}` : "Synced");
// Phase 150 (task 02): the settle's counts line IS the box's
// result line (the sole live region) — the string builder is
// unchanged (A2: the "Synced HH:MM" label stays on the button).
// unchanged (A2: the "Synced <stamp>" label stays on the button;
// the stamp is date-qualified, phase 154).
setSyncProgress(fmtSyncResult(status.detail));
hideSyncError();
emitSyncStatus(status);
+17 -3
View File
@@ -335,10 +335,24 @@ def _offered_tool_names(body: dict[str, Any]) -> list[str]:
return [t["function"]["name"] for t in (body.get("tools") or [])]
def _ready_for_sync_line(source: str) -> re.Pattern[str]:
"""Phase 154 (task 02): the ready-for-sync line carrying the
completion stamp — "Uploaded <source> (<stamp>) — press Sync
sources to import it." (the fresh 202 settle). In E2E the stamp
is ALWAYS the same-day ``HH:MM`` form (the run settles within
seconds; the date-qualified forms are unit-pinned on a fixed
clock — E2E never depends on the wall clock)."""
return re.compile(
rf"^Uploaded {re.escape(source)} \(\d{{1,2}}:\d{{2}}\)"
r" — press Sync sources to import it\.$"
)
def _upload_zip(page: Page, app_url: str, archive: Path) -> None:
"""The real admin upload flow: form login → the git-sources page →
pick the archive → submit → the terminal result line (phase 90:
"Uploaded <source> — press Sync sources to import it.")."""
pick the archive → submit → the terminal result line (phase 90 A3,
phase 154 task 02: "Uploaded <source> (<stamp>) — press Sync
sources to import it." — the completion stamp)."""
login(page, app_url, next=GIT_SOURCES_URL)
expect(page).to_have_url(app_url + GIT_SOURCES_URL, timeout=30_000)
expect(page.locator("#sign-out-btn")).to_be_visible(timeout=15_000)
@@ -347,7 +361,7 @@ def _upload_zip(page: Page, app_url: str, archive: Path) -> None:
result = page.locator("#archive-upload-result")
expect(result).to_be_visible(timeout=UPLOAD_TIMEOUT_MS)
expect(result).to_have_text(
f"Uploaded {SOURCE_NAME} — press Sync sources to import it.",
_ready_for_sync_line(SOURCE_NAME),
timeout=UPLOAD_TIMEOUT_MS,
)
+28 -14
View File
@@ -20,7 +20,8 @@ server-side behind ``GET /api/git-sources/upload/status`` (the
phase-32 ``SyncStatus`` pattern; the phase-64 key set with
``current_file``/``files_done``/``files_total`` null/0/0 for the whole
run — phase 90 A2). The settled result line points at the next step:
"Uploaded <name> — press Sync sources to import it." (phase 90 A3) —
"Uploaded <name> (<stamp>) — press Sync sources to import it."
(phase 90 A3, the phase-154 completion stamp) —
the scan is the RAG page's **Sync sources** button's job, so this
suite asserts **zero indexed documents** after every upload.
@@ -66,8 +67,9 @@ Contract under test:
button then carries the BARE "Processing…" label for the whole
background run (no file, no "(n/m)" — the first status GET is held
so the in-run window outlives the 2 s poll) until the status
``success`` lands: the result line reads "Uploaded e2e-upload —
press Sync sources to import it.", the button restores ("Upload"),
``success`` lands: the result line reads "Uploaded e2e-upload
(<stamp>) — press Sync sources to import it." (the phase-154
completion stamp), the button restores ("Upload"),
the file input clears, the list gains exactly one row for
``e2e-upload`` with the **Local** badge and its "Ignore paths"
control (the phase-89 editor the deferral exists for), the terminal
@@ -139,6 +141,19 @@ SOURCES_URL = "/sources.html"
#: filename minus the archive suffix (the phase's locked naming rule).
SOURCE_NAME = "e2e-upload"
def _ready_for_sync_line(name: str) -> re.Pattern[str]:
"""Phase 154 (task 02): the ready-for-sync line carrying the
completion stamp — "Uploaded <name> (<stamp>) — press Sync sources
to import it." (the fresh 202 settle, the page-local name). In
E2E the stamp is ALWAYS the same-day ``HH:MM`` form — the run
settles within seconds (the date-qualified forms are unit-pinned
on a fixed clock — E2E never depends on the wall clock)."""
return re.compile(
rf"^Uploaded {re.escape(name)} \(\d{{1,2}}:\d{{2}}\)"
r" — press Sync sources to import it\.$"
)
#: v1: two sentinel docs. v2 (same filename): alpha CHANGED, beta DROPPED,
#: gamma ADDED — the in-place-replace subject (the on-disk folder swap).
ALPHA_SENTINEL_V1 = "ALPHA-TOKEN-v1-7f31"
@@ -436,8 +451,9 @@ def test_upload_registers_without_indexing(
then carries the BARE "Processing…" label for the whole background
run (no file, no "(n/m)" — phase 90 A2, proven across a held first
status GET); when the status ``success`` lands the result line
reads "Uploaded <source> — press Sync sources to import it."
(A3), the button restores ("Upload"), the file input clears, the
reads "Uploaded <source> (<stamp>) — press Sync sources to
import it." (A3 — the phase-154 completion stamp), the button
restores ("Upload"), the file input clears, the
list gains exactly ONE row for ``e2e-upload`` (Local badge, the
"Ignore paths" control), the terminal status carries the no-count
``{"message": "uploaded"}`` detail with null/0/0 progress — and
@@ -490,9 +506,7 @@ def test_upload_registers_without_indexing(
# …release: the status success lands → the ready-for-sync line +
# the never-stale restore (input cleared).
expect(result).to_be_visible(timeout=UPLOAD_TIMEOUT_MS)
expect(result).to_have_text(
f"Uploaded {SOURCE_NAME} — press Sync sources to import it."
)
expect(result).to_have_text(_ready_for_sync_line(SOURCE_NAME))
expect(btn).to_be_enabled()
expect(btn).to_have_text("Upload")
expect(page.locator("#archive-upload-file")).to_have_value("")
@@ -560,8 +574,8 @@ def test_reupload_replaces_in_place(
# Baseline: v1 through the page (202 → the ready-for-sync line,
# one row, no index).
assert _upload_via_page(page, tarball_v1) == (
f"Uploaded {SOURCE_NAME} — press Sync sources to import it."
assert _ready_for_sync_line(SOURCE_NAME).fullmatch(
_upload_via_page(page, tarball_v1)
)
expect(page.locator("#git-sources-tbody tr", has_text=SOURCE_NAME)).to_have_count(1)
folder = upload_dir / SOURCE_NAME
@@ -570,8 +584,8 @@ def test_reupload_replaces_in_place(
# Re-upload v2 — SAME basename, different parent dir (the file
# input's selection is replaced wholesale).
assert _upload_via_page(page, tarball_v2) == (
f"Uploaded {SOURCE_NAME} — press Sync sources to import it."
assert _ready_for_sync_line(SOURCE_NAME).fullmatch(
_upload_via_page(page, tarball_v2)
)
# No duplicate: exactly ONE row for that source (and one row total).
@@ -635,8 +649,8 @@ def test_bad_file_inline_error(
# The form is not wedged: a good upload right after still works —
# and indexes nothing (phase 90 A1).
assert _upload_via_page(page, tarball_v1) == (
f"Uploaded {SOURCE_NAME} — press Sync sources to import it."
assert _ready_for_sync_line(SOURCE_NAME).fullmatch(
_upload_via_page(page, tarball_v1)
)
expect(error).to_be_hidden()
expect(page.locator("#git-sources-tbody tr", has_text=SOURCE_NAME)).to_have_count(1)
+17 -3
View File
@@ -296,10 +296,24 @@ def _health_db(app_url: str) -> bool:
return False
def _ready_for_sync_line(source: str) -> re.Pattern[str]:
"""Phase 154 (task 02): the ready-for-sync line carrying the
completion stamp — "Uploaded <source> (<stamp>) — press Sync
sources to import it." (the fresh 202 settle). In E2E the stamp
is ALWAYS the same-day ``HH:MM`` form (the run settles within
seconds; the date-qualified forms are unit-pinned on a fixed
clock — E2E never depends on the wall clock)."""
return re.compile(
rf"^Uploaded {re.escape(source)} \(\d{{1,2}}:\d{{2}}\)"
r" — press Sync sources to import it\.$"
)
def _upload_zip(page: Page, app_url: str, archive: Path, source: str) -> None:
"""The real admin upload flow: form login → the git-sources page →
pick the archive → submit → the terminal result line (phase 90:
"Uploaded <source> — press Sync sources to import it.")."""
pick the archive → submit → the terminal result line (phase 90 A3,
phase 154 task 02: "Uploaded <source> (<stamp>) — press Sync
sources to import it." — the completion stamp)."""
login(page, app_url, next=GIT_SOURCES_URL)
expect(page).to_have_url(app_url + GIT_SOURCES_URL, timeout=30_000)
expect(page.locator("#sign-out-btn")).to_be_visible(timeout=15_000)
@@ -308,7 +322,7 @@ def _upload_zip(page: Page, app_url: str, archive: Path, source: str) -> None:
result = page.locator("#archive-upload-result")
expect(result).to_be_visible(timeout=UPLOAD_TIMEOUT_MS)
expect(result).to_have_text(
f"Uploaded {source} — press Sync sources to import it.",
_ready_for_sync_line(source),
timeout=UPLOAD_TIMEOUT_MS,
)
+17 -3
View File
@@ -233,10 +233,24 @@ def _health_db(app_url: str) -> bool:
return False
def _ready_for_sync_line(source: str) -> re.Pattern[str]:
"""Phase 154 (task 02): the ready-for-sync line carrying the
completion stamp — "Uploaded <source> (<stamp>) — press Sync
sources to import it." (the fresh 202 settle). In E2E the stamp
is ALWAYS the same-day ``HH:MM`` form (the run settles within
seconds; the date-qualified forms are unit-pinned on a fixed
clock — E2E never depends on the wall clock)."""
return re.compile(
rf"^Uploaded {re.escape(source)} \(\d{{1,2}}:\d{{2}}\)"
r" — press Sync sources to import it\.$"
)
def _upload_zip(page: Page, app_url: str, archive: Path) -> None:
"""The real admin upload flow: form login → the git-sources page →
pick the archive → submit → the terminal result line (phase 90:
"Uploaded <source> — press Sync sources to import it.")."""
pick the archive → submit → the terminal result line (phase 90 A3,
phase 154 task 02: "Uploaded <source> (<stamp>) — press Sync
sources to import it." — the completion stamp)."""
login(page, app_url, next=GIT_SOURCES_URL)
expect(page).to_have_url(app_url + GIT_SOURCES_URL, timeout=30_000)
expect(page.locator("#sign-out-btn")).to_be_visible(timeout=15_000)
@@ -245,7 +259,7 @@ def _upload_zip(page: Page, app_url: str, archive: Path) -> None:
result = page.locator("#archive-upload-result")
expect(result).to_be_visible(timeout=UPLOAD_TIMEOUT_MS)
expect(result).to_have_text(
f"Uploaded {SOURCE_NAME} — press Sync sources to import it.",
_ready_for_sync_line(SOURCE_NAME),
timeout=UPLOAD_TIMEOUT_MS,
)
+17 -3
View File
@@ -320,10 +320,24 @@ def _health_db(app_url: str) -> bool:
return False
def _ready_for_sync_line(source: str) -> re.Pattern[str]:
"""Phase 154 (task 02): the ready-for-sync line carrying the
completion stamp — "Uploaded <source> (<stamp>) — press Sync
sources to import it." (the fresh 202 settle). In E2E the stamp
is ALWAYS the same-day ``HH:MM`` form (the run settles within
seconds; the date-qualified forms are unit-pinned on a fixed
clock — E2E never depends on the wall clock)."""
return re.compile(
rf"^Uploaded {re.escape(source)} \(\d{{1,2}}:\d{{2}}\)"
r" — press Sync sources to import it\.$"
)
def _upload_zip(page: Page, app_url: str, archive: Path, source: str) -> None:
"""The real admin upload flow: form login → the git-sources page →
pick the archive → submit → the terminal result line (phase 90:
"Uploaded <source> — press Sync sources to import it.")."""
pick the archive → submit → the terminal result line (phase 90 A3,
phase 154 task 02: "Uploaded <source> (<stamp>) — press Sync
sources to import it." — the completion stamp)."""
login(page, app_url, next="/git-sources.html")
expect(page).to_have_url(app_url + "/git-sources.html", timeout=30_000)
expect(page.locator("#sign-out-btn")).to_be_visible(timeout=15_000)
@@ -332,7 +346,7 @@ def _upload_zip(page: Page, app_url: str, archive: Path, source: str) -> None:
result = page.locator("#archive-upload-result")
expect(result).to_be_visible(timeout=UPLOAD_TIMEOUT_MS)
expect(result).to_have_text(
f"Uploaded {source} — press Sync sources to import it.",
_ready_for_sync_line(source),
timeout=UPLOAD_TIMEOUT_MS,
)
+17 -3
View File
@@ -379,10 +379,24 @@ def _health_db(app_url: str) -> bool:
return False
def _ready_for_sync_line(source: str) -> re.Pattern[str]:
"""Phase 154 (task 02): the ready-for-sync line carrying the
completion stamp — "Uploaded <source> (<stamp>) — press Sync
sources to import it." (the fresh 202 settle). In E2E the stamp
is ALWAYS the same-day ``HH:MM`` form (the run settles within
seconds; the date-qualified forms are unit-pinned on a fixed
clock — E2E never depends on the wall clock)."""
return re.compile(
rf"^Uploaded {re.escape(source)} \(\d{{1,2}}:\d{{2}}\)"
r" — press Sync sources to import it\.$"
)
def _upload_zip(page: Page, app_url: str, archive: Path, source_name: str) -> None:
"""The real admin upload flow: form login → the git-sources page →
pick the archive → submit → the terminal result line (phase 90:
"Uploaded <source> — press Sync sources to import it.")."""
pick the archive → submit → the terminal result line (phase 90 A3,
phase 154 task 02: "Uploaded <source> (<stamp>) — press Sync
sources to import it." — the completion stamp)."""
login(page, app_url, next="/git-sources.html")
expect(page).to_have_url(app_url + "/git-sources.html", timeout=30_000)
expect(page.locator("#sign-out-btn")).to_be_visible(timeout=15_000)
@@ -391,7 +405,7 @@ def _upload_zip(page: Page, app_url: str, archive: Path, source_name: str) -> No
result = page.locator("#archive-upload-result")
expect(result).to_be_visible(timeout=UPLOAD_TIMEOUT_MS)
expect(result).to_have_text(
f"Uploaded {source_name} — press Sync sources to import it.",
_ready_for_sync_line(source_name),
timeout=UPLOAD_TIMEOUT_MS,
)
+17 -3
View File
@@ -278,10 +278,24 @@ def _health_db(app_url: str) -> bool:
return False
def _ready_for_sync_line(source: str) -> re.Pattern[str]:
"""Phase 154 (task 02): the ready-for-sync line carrying the
completion stamp — "Uploaded <source> (<stamp>) — press Sync
sources to import it." (the fresh 202 settle). In E2E the stamp
is ALWAYS the same-day ``HH:MM`` form (the run settles within
seconds; the date-qualified forms are unit-pinned on a fixed
clock — E2E never depends on the wall clock)."""
return re.compile(
rf"^Uploaded {re.escape(source)} \(\d{{1,2}}:\d{{2}}\)"
r" — press Sync sources to import it\.$"
)
def _upload_zip(page: Page, app_url: str, archive: Path) -> None:
"""The real admin upload flow: form login → the git-sources page →
pick the archive → submit → the terminal result line (phase 90:
"Uploaded <source> — press Sync sources to import it.")."""
pick the archive → submit → the terminal result line (phase 90 A3,
phase 154 task 02: "Uploaded <source> (<stamp>) — press Sync
sources to import it." — the completion stamp)."""
login(page, app_url, next="/git-sources.html")
expect(page).to_have_url(app_url + "/git-sources.html", timeout=30_000)
expect(page.locator("#sign-out-btn")).to_be_visible(timeout=15_000)
@@ -290,7 +304,7 @@ def _upload_zip(page: Page, app_url: str, archive: Path) -> None:
result = page.locator("#archive-upload-result")
expect(result).to_be_visible(timeout=UPLOAD_TIMEOUT_MS)
expect(result).to_have_text(
f"Uploaded {SOURCE_NAME} — press Sync sources to import it.",
_ready_for_sync_line(SOURCE_NAME),
timeout=UPLOAD_TIMEOUT_MS,
)
+17 -3
View File
@@ -427,10 +427,24 @@ def _clean_kb(app_url: str) -> Iterator[None]:
_truncate_all()
def _ready_for_sync_line(source: str) -> re.Pattern[str]:
"""Phase 154 (task 02): the ready-for-sync line carrying the
completion stamp — "Uploaded <source> (<stamp>) — press Sync
sources to import it." (the fresh 202 settle). In E2E the stamp
is ALWAYS the same-day ``HH:MM`` form (the run settles within
seconds; the date-qualified forms are unit-pinned on a fixed
clock — E2E never depends on the wall clock)."""
return re.compile(
rf"^Uploaded {re.escape(source)} \(\d{{1,2}}:\d{{2}}\)"
r" — press Sync sources to import it\.$"
)
def _upload_tar(page: Page, app_url: str, archive: Path) -> None:
"""The real admin upload flow: form login → the git-sources page →
pick the archive → submit → the terminal result line (phase 90:
"Uploaded <source> — press Sync sources to import it.")."""
pick the archive → submit → the terminal result line (phase 90 A3,
phase 154 task 02: "Uploaded <source> (<stamp>) — press Sync
sources to import it." — the completion stamp)."""
login(page, app_url, next="/git-sources.html")
expect(page).to_have_url(app_url + "/git-sources.html", timeout=30_000)
expect(page.locator("#sign-out-btn")).to_be_visible(timeout=15_000)
@@ -439,7 +453,7 @@ def _upload_tar(page: Page, app_url: str, archive: Path) -> None:
result = page.locator("#archive-upload-result")
expect(result).to_be_visible(timeout=UPLOAD_TIMEOUT_MS)
expect(result).to_have_text(
f"Uploaded {SOURCE_NAME} — press Sync sources to import it.",
_ready_for_sync_line(SOURCE_NAME),
timeout=UPLOAD_TIMEOUT_MS,
)
+769
View File
@@ -0,0 +1,769 @@
"""Phase 154 task 04 E2E (Playwright, mock-only): the two visible
FRESHNESS-STAMP contracts on the live UI (the phase-154 date-qualified
stamps — the same-day shape is pinned here; the non-same-day shapes
are unit-pinned on a fixed clock, so this suite never depends on the
wall clock beyond "the run finishes today").
1. **The RAG sync badge settles same-day-shaped**: a fresh mock sync
(a small registered local source, the mock LLM keeps it short)
settles the ``#sync-btn`` label to ``Synced HH:MM`` — the
DATE-QUALIFIER (phase 154, task 01, ``fmtSyncTime``'s widened
contract) must yield the same-day ``HH:MM`` shape for a run that
finished "now". The assertion is a SHAPE pin (the regex
``^Synced \\d{2}:\\d{2}$`` — the zero-padded 24-hour form
``fmtSyncTime`` produces for the same local calendar day), never a
comparison against the wall clock's actual value (the
date-qualified forms are unit-pinned on a fixed clock, phase 154,
task 03 — the E2E never depends on the wall clock beyond "the
run finishes today").
2. **The upload line's fresh settle carries the name AND the stamp**:
a real small fixture archive through the page's upload form → the
202 "Successfully uploaded — <source>" toast → the 2 s poll's
settle renders the ready-for-sync line
``Uploaded <name> (<stamp>) — press Sync sources to import it.``
(the phase-154 task 02 shape, the page-local safe name +
``fmtUploadStamp``'s completion stamp off the server payload's
``finished_at`` — the terminal status pin carries the non-null
``finished_at`` the stamp is computed from). The stamp matches the
shape pin (same-day ``HH:MM`` in practice; the shape regex also
covers the date-qualified forms, so the pin never breaks across a
midnight boundary).
3. **The boot re-render is NAMELESS but still STAMPED** (the phase's
actual fix — the owner's "the line can sit there indefinitely and
mislead"): after the settled fresh line, a page reload boots
``initUploadStatus``'s success branch — the safe name was
page-local (``lastUploadName`` is null after a reload — the
phase-90 contract), so the line is the NAMELESS
``Uploaded <stamp> — press Sync sources to import it.`` — but the
stamp SURVIVES because it comes from the SERVER payload
(``finished_at``), not from page state. The run's identity pin
(``started_at`` unchanged across the reload) proves the re-render
is the same run's line, not a second upload's.
4. **A running upload never renders the ready-for-sync line** (the
phase-90 regression guard, extended): at the 202 moment the button
is the BARE ``Processing…`` (phase 90, A2 — no file, no counts,
title cleared) and ``#archive-upload-result`` is HIDDEN — the line
appears ONLY on settle (the poll's success tick). The
deterministic in-flight window: the 202 handler synchronously
enters the processing state and schedules the first poll tick at
+2 s, so the ``Processing…`` state holds for ≥ 2 s regardless of
how fast the sub-second background run finishes — the
in-flight observation needs no delay proxy (the upload makes zero
LLM calls; the sync leg of test 1 is 2 files and settles within
the generous budget).
Run in isolation (DB must be up: ``podman compose up -d db``):
uv run pytest tests/e2e/test_sync_status_freshness.py -v --no-cov
MOCK-ONLY suite: ``E2E_REAL_LLM=1`` is not supported — the whole
point is the deterministic mock (no real-LLM dependency), and the
time assertions are shape pins, never wall-clock value comparisons.
Per-module app env (the conftest pattern — mirroring
``test_upload_no_scan.py``'s local helpers, NOT importing that
module): ``BOR_UPLOAD_DIR`` points at a scratch dir (the app runs on
the same host), ``BOR_GIT_SOURCES`` is forced empty (this suite's
own registered rows / upload rows are the only sync sources),
``BOR_LLM_BASE_URL`` is the mock (the sync leg's embeds + overview),
the retry knobs pinned (the sync leg never burns a 5 s retry wait),
and the autouse ``_clean`` truncates the shared Postgres (plus waits
for no running background job — a leaked run must not survive into
the next test's truncated KB).
"""
from __future__ import annotations
import io
import os
import re
import subprocess
import sys
import tarfile
import time
from collections.abc import Iterator
from pathlib import Path
from typing import Any
import httpx
import pytest
from playwright.sync_api import Page, expect
from sqlalchemy import text
from app.config import Settings as _Settings
from app.db import SessionLocal
from e2e.auth_helpers import login
from e2e.conftest import (
ADMIN_PASSWORD,
SESSION_SECRET,
USE_REAL_LLM,
_wait_http,
)
REPO = Path(__file__).resolve().parents[2]
# Phase 79 (task 04, full inventory): in a combined session run the
# conftest session app already owns its port — a second uvicorn on it
# dies on bind and this suite would silently drive the wrong server.
# The module app binds its own port instead (env-overridable).
APP_PORT = int(os.environ.get("E2E_APP_PORT_SYNCFRESH", "8172"))
APP_URL = f"http://127.0.0.1:{APP_PORT}"
SOURCES_URL = "/sources.html"
GIT_SOURCES_URL = "/git-sources.html"
#: The sync-leg fixture source (a host temp dir, ``kind=local``) —
#: exactly two small markdown files, so the counts line is "2 added"
#: and the run is short against the fast mock LLM.
SYNC_SOURCE = "freshness-sync"
SYNC_FILES: dict[str, str] = {
"alpha.md": (
"# Freshness alpha\n"
"\n"
"A small note that exists only to give the phase-154 sync\n"
"fixture a real import.\n"
"\n"
"Marker: FRESHNESS-SYNC-ALPHA-7f31\n"
),
"beta.md": (
"# Freshness beta\n"
"\n"
"The second small note — the counts line's denominator.\n"
"\n"
"Marker: FRESHNESS-SYNC-BETA-2c90\n"
),
}
#: The upload-leg archive basenames (one per upload test — distinct
#: names keep the in-place-replace identity out of the assertions).
UPLOAD_A = "e2e-freshness-a"
UPLOAD_B = "e2e-freshness-b"
UPLOAD_C = "e2e-freshness-c"
def _targz_files(name: str) -> dict[str, str]:
"""One small deterministic markdown file per archive (a valid
replacement archive — what the KB indexes with it is the sync's
call, phase 90)."""
return {
"note.md": (
f"# Freshness {name}\n"
"\n"
"A small note carried by this suite's archive.\n"
f"\nMarker: FRESHNESS-{name.upper()}-9d16\n"
)
}
# ---------------------------------------------------------------------------
# Shape pins (the task's contract: shapes, never wall-clock values)
# ---------------------------------------------------------------------------
#: The date-qualified stamp's shape (phase 154, A1 rule): the
#: same-day ``HH:MM`` (zero-padded — ``fmtSyncTime`` /
#: ``fmtUploadStamp`` pad both fields) OR the date-qualified forms
#: (``M/D(, YYYY), h:MM AM/PM`` — no leading zero on month/day/hour).
#: In practice a run that settles "now" always lands on the same-day
#: branch; the shape regex covers both so the pin never breaks across
#: a midnight boundary. The date-qualified VALUES are unit-pinned on a
#: fixed clock (phase 154, task 03) — this suite never compares a
#: stamp against the actual time.
STAMP = (
r"\d{2}:\d{2}"
r"|\d{1,2}/\d{1,2}(?:/\d{4})?, \d{1,2}:\d{2} [AP]M"
)
#: ``Synced HH:MM`` — the same-day settled sync label (task 01: the
#: qualifier yields the zero-padded ``HH:MM`` for a run that finished
#: on the same local calendar day — byte-identical to the pre-phase
#: shape). The date-qualified ``M/D(, YYYY), h:MM AM/PM`` forms are
#: unit-pinned on a fixed clock (phase 154, task 03) — this pin is
#: the same-day shape the task pins; a run that settles "now" always
#: lands on the same-day branch.
SYNCED_SAME_DAY = re.compile(r"^Synced \d{2}:\d{2}$")
#: The fresh-settle ready-for-sync line WITH the safe name and the
#: stamp (phase 154, task 02 — the page-local ``lastUploadName`` +
#: ``fmtUploadStamp(status)`` off the server payload's
#: ``finished_at``).
def _ready_line(name: str) -> re.Pattern[str]:
return re.compile(
rf"^Uploaded {re.escape(name)} \(({STAMP})\)"
r" — press Sync sources to import it\.$"
)
#: The boot re-render ready-for-sync line — NAMELESS (the safe name
#: was page-local, ``lastUploadName`` is null after a reload — the
#: phase-90 contract) but STAMPED (the stamp comes from the SERVER
#: payload — the phase-154 fix: a line that outlives its upload is
#: self-dating).
NAMELESS_READY_LINE = re.compile(
rf"^Uploaded ({STAMP}) — press Sync sources to import it\.$"
)
#: Generous settle budgets: the upload run settles in milliseconds
#: (phase 90 — unpack + register only, zero LLM calls) and the UI's
#: 2 s poll lands one tick after; the sync leg is 2 files against the
#: fast mock LLM and may settle between the 2 s poll ticks — the
#: settled-state assertions retry until then.
UPLOAD_TIMEOUT_MS = 30_000
SYNC_TIMEOUT_MS = 90_000
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
def _build_targz(path: Path, files: dict[str, str]) -> Path:
"""A deterministic ``.tar.gz`` (mtime 0) over the given files."""
with tarfile.open(path, "w:gz") as tf:
for rel, content in files.items():
data = content.encode("utf-8")
info = tarfile.TarInfo(rel)
info.size = len(data)
info.mtime = 0
tf.addfile(info, io.BytesIO(data))
return path
@pytest.fixture(scope="module")
def sync_dir(tmp_path_factory: pytest.TempPathFactory) -> Path:
"""The sync-leg's local source (``kind=local`` — the directory's
basename is the source name, phase 38): EXACTLY the two fixture
files (the counts line reads ``2 added``)."""
root = tmp_path_factory.mktemp("bor_freshness_sync")
src = root / SYNC_SOURCE
src.mkdir()
for rel, content in SYNC_FILES.items():
(src / rel).write_text(content, encoding="utf-8")
assert len(list(src.rglob("*"))) == len(SYNC_FILES)
return src
@pytest.fixture(scope="module")
def upload_dir(tmp_path_factory: pytest.TempPathFactory) -> Path:
"""The app's ``BOR_UPLOAD_DIR`` for this suite — a scratch dir
the host-side assertions inspect (the app server runs on the same
machine). The app creates it on the first upload."""
return tmp_path_factory.mktemp("bor_freshness_uploads") / "uploads"
@pytest.fixture(scope="module")
def tarball_a(tmp_path_factory: pytest.TempPathFactory) -> Path:
root = tmp_path_factory.mktemp("bor_freshness_a")
return _build_targz(root / f"{UPLOAD_A}.tar.gz", _targz_files(UPLOAD_A))
@pytest.fixture(scope="module")
def tarball_b(tmp_path_factory: pytest.TempPathFactory) -> Path:
root = tmp_path_factory.mktemp("bor_freshness_b")
return _build_targz(root / f"{UPLOAD_B}.tar.gz", _targz_files(UPLOAD_B))
@pytest.fixture(scope="module")
def tarball_c(tmp_path_factory: pytest.TempPathFactory) -> Path:
root = tmp_path_factory.mktemp("bor_freshness_c")
return _build_targz(root / f"{UPLOAD_C}.tar.gz", _targz_files(UPLOAD_C))
@pytest.fixture(scope="module")
def app_server(
mock_llm: int,
upload_dir: Path,
tmp_path_factory: pytest.TempPathFactory,
) -> Iterator[str]:
"""The real app under test — per-module env: uploads unpack into
a scratch dir, the env git list is forced empty (this suite's own
rows are the only sync sources), and the LLM base URL is the mock
(the upload makes zero LLM calls — phase 90; the mock serves the
sync leg's embeds + overview). No ``slow_llm`` proxy: the upload
run is sub-second and the 2 s poll window is what the in-flight
assertion leans on (see the module docstring's test 4 paragraph).
Skips the module (clear instructions) when Postgres is down —
the sync needs the KB tables."""
env = dict(os.environ)
env.pop("DEBUGPY", None)
env["BOR_ENVIRONMENT"] = "e2e"
env["BOR_STATIC_DIR"] = str(REPO / "frontend")
env["BOR_LLM_BASE_URL"] = (
"https://aipi.reeseapps.com/v1"
if USE_REAL_LLM
else f"http://127.0.0.1:{mock_llm}/v1"
)
# Mock-calibrated threshold (conftest pattern): this suite never
# asks the chat model anything — the gate is never on a path.
env["BOR_RELEVANCE_THRESHOLD"] = "0.30"
# Phase 67: instant retry waits + the code-default budget (the
# conftest leak-guard pattern — the sync leg never burns a 5 s
# retry wait).
env["BOR_LLM_RETRY_DELAY"] = "0"
env["BOR_LLM_RETRIES"] = str(_Settings.model_fields["llm_retries"].default)
env.setdefault(
"BOR_DATABASE_URL",
"postgresql+psycopg://reese:reese@localhost:5432/brain_of_reese",
)
# Phase 16: admin auth must be set or create_app() refuses to boot.
env["BOR_ADMIN_PASSWORD"] = ADMIN_PASSWORD
env["BOR_SESSION_SECRET"] = SESSION_SECRET
# The repo's .env file carries the owner's BOR_GIT_SOURCES (the app
# reads it from cwd) — override it with an EMPTY value (the env var
# beats the .env file): the registry must hold EXACTLY this
# suite's own rows.
env["BOR_GIT_SOURCES"] = ""
# Phase 49: unpack uploads into the suite's scratch dir (host-
# inspectable) and keep the (unused) git checkouts out of the dev
# location.
env["BOR_UPLOAD_DIR"] = str(upload_dir)
env["BOR_SOURCES_DIR"] = str(tmp_path_factory.mktemp("bor_checkouts"))
proc = subprocess.Popen(
[sys.executable, "-m", "uvicorn", "app.main:app",
"--host", "127.0.0.1", "--port", str(APP_PORT), "--log-level", "warning"],
cwd=REPO,
env=env,
)
try:
_wait_http(f"{APP_URL}/api/health")
body = httpx.get(f"{APP_URL}/api/health", timeout=5).json()
if body["db"] != "up":
pytest.skip("Postgres not reachable — run `podman compose up -d db` first")
yield APP_URL
finally:
proc.terminate()
try:
proc.wait(timeout=10)
except subprocess.TimeoutExpired:
proc.kill()
@pytest.fixture(scope="module")
def app_url(app_server: str) -> str:
return app_server
def _truncate_all() -> None:
"""Fresh registry + KB per test (the E2E isolation pattern): the
row-count and doc-list assertions must be this test's own doing.
The E2E suites share one Postgres, and a leftover git_sources row
or document would corrupt them (a leftover row would be a SECOND
sync source, skewing the sync's counts). ``sources_meta`` (the KB
generation counter) resets with the KB — the sync re-creates the
row; ``folder_summaries`` / ``steering_notes`` ride along (the
phase-96 pattern)."""
with SessionLocal() as db:
db.execute(
text(
"TRUNCATE chunks, documents, query_log, steering_notes, "
"kb_overview, git_sources, sources_meta, folder_summaries"
)
)
db.commit() # without the commit the TRUNCATE rolls back (the house pattern)
def _wait_no_running_jobs(app_url: str) -> None:
"""No background job may leak across tests (the run states live in
the app's memory, and a still-running sync would keep importing
into the NEXT test's truncated KB, and a still-running upload
would 409 the next test's upload): wait for BOTH status endpoints
to be non-running BEFORE the truncate. Own admin session (the
endpoints are admin-only) — usually a no-op: the tests settle
only after their terminal states."""
with httpx.Client(timeout=5.0) as client:
client.post(f"{app_url}/api/login", json={"password": ADMIN_PASSWORD})
for path in ("/api/sync/status", "/api/git-sources/upload/status"):
body: dict[str, Any] = {}
deadline = time.monotonic() + 90
while time.monotonic() < deadline:
r = client.get(f"{app_url}{path}")
if r.status_code == 200:
body = r.json()
if body["state"] != "running":
break
time.sleep(0.2)
assert body["state"] != "running", (
f"a background job was still running at test boundary: {path} {body}"
)
@pytest.fixture(autouse=True)
def _clean(app_url: str, db_ready: None) -> Iterator[None]:
_wait_no_running_jobs(app_url)
_truncate_all()
yield
_truncate_all()
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _register_local_source(base_url: str, path: str) -> None:
"""Register the temp directory through the authenticated API (the
``test_local_directory_sources.py`` / phase-94 pattern — a plain
directory is an API/DB-only operation)."""
with httpx.Client(base_url=base_url, timeout=30.0) as client:
r = client.post("/api/login", json={"password": ADMIN_PASSWORD})
assert r.status_code == 204, r.text
r = client.post(
"/api/git-sources", json={"kind": "local", "path": path}
)
assert r.status_code == 201, r.text
def _status(page: Page, app_url: str, path: str) -> dict[str, Any]:
"""The status endpoint as the signed-in page (admin cookie)."""
r = page.request.get(f"{app_url}{path}")
assert r.status == 200, r.text
return r.json()
def _admin_git_sources_page(page: Page, app_url: str) -> None:
"""Real form login landing on the git sources page (admin settled:
Sign out visible, the manager revealed by the page module)."""
login(page, app_url, next=GIT_SOURCES_URL)
expect(page).to_have_url(app_url + GIT_SOURCES_URL, timeout=30_000)
expect(page.locator("#sign-out-btn")).to_be_visible(timeout=15_000)
expect(page.locator("#git-sources-gate")).to_be_hidden()
expect(page.locator("#git-sources-content")).to_be_visible()
def _upload_via_page(page: Page, archive: Path) -> None:
"""Pick the archive and submit the form (the 202 path: the toast
fires at the 202, then the button's status polling settles the
line — the caller asserts the line's shape). Every upload in this
suite succeeds (the failure path is another suite's subject), so
any non-202 outcome here is a test error."""
page.set_input_files("#archive-upload-file", str(archive))
page.click("#archive-upload-btn")
toast = page.locator(".toast")
expect(toast).to_have_count(1, timeout=UPLOAD_TIMEOUT_MS)
expect(toast).to_have_class(re.compile(r"\bis-visible\b"))
# ---------------------------------------------------------------------------
# 1. The RAG sync badge settles in the same-day shape (the qualifier's
# E2E half — the date-qualified values are unit-pinned, the shape
# is pinned here on a run that finished "now")
# ---------------------------------------------------------------------------
def test_fresh_sync_settles_same_day_shape(
page: Page, app_url: str, db_ready: None, sync_dir: Path
) -> None:
"""A fresh mock sync (the small registered local source — the mock
LLM keeps the run short) settles the RAG ``#sync-btn`` label to
the same-day ``Synced HH:MM`` shape (phase 154, task 01 — the
qualifier must yield ``HH:MM`` for a run that finished on the
same local calendar day — the SHAPE pin, never a wall-clock value
comparison). The run is real (the counts line ``2 added`` + the
terminal status's non-null ``finished_at`` — the ISO the
badge's stamp is computed from — + 2 of 2 files), the settled
button is re-enabled with the ship aria value restored, and no
error UI is present."""
page.set_default_timeout(30_000)
_register_local_source(app_url, str(sync_dir))
login(page, app_url, next=SOURCES_URL)
expect(page).to_have_url(app_url + SOURCES_URL, timeout=30_000)
btn = page.locator("#sync-btn")
expect(btn).to_be_visible(timeout=30_000)
# The settled boot label — the fresh app's idle state (this is the
# module's first test; the app's in-memory sync state is idle).
expect(page.locator("#sync-label")).to_have_text("Sync sources")
expect(btn).to_be_enabled()
expect(page.locator("#sync-error-banner")).to_be_hidden()
btn.click()
# The click's immediate state (the 202 moment — the bare prefix in
# the box — phase 149: the running label rides the box, the
# button is the spinner).
expect(btn).to_be_disabled()
expect(btn).to_have_attribute("aria-busy", "true")
expect(btn.locator(".sync-icon")).to_have_class(re.compile(r"\bis-spinning\b"))
expect(page.locator("#sync-progress")).to_have_text("Syncing…")
# The settle (phase 154, task 01 — the SAME-DAY SHAPE pin):
# "Synced HH:MM" — the date-qualified ``M/D(, YYYY), h:MM AM/PM``
# forms are unit-pinned on a fixed clock (phase 154, task 03);
# this run finished "now", so the qualifier must yield the
# same-day shape (the shape is asserted; the actual time value is
# never compared against the wall clock).
expect(page.locator("#sync-label")).to_have_text(
SYNCED_SAME_DAY, timeout=SYNC_TIMEOUT_MS
)
# The run was real (the counts line + the terminal status agree —
# the status is the source of truth for the settle; the UI's 2 s
# poll renders it at most one tick later).
expect(page.locator("#sync-progress-text")).to_have_text(
f"{len(SYNC_FILES)} added", timeout=SYNC_TIMEOUT_MS
)
btn = page.locator("#sync-btn")
expect(btn).to_be_enabled()
expect(btn).not_to_have_attribute("aria-busy")
# Phase 150: the settle restores the ship aria value (the running
# full label is gone — the badge is the pill's text).
assert btn.get_attribute("aria-label") == "Sync sources", (
btn.get_attribute("aria-label")
)
expect(page.locator("#sync-error-banner")).to_be_hidden()
terminal = _status(page, app_url, "/api/sync/status")
assert terminal["state"] == "success", terminal
# The run was a fresh import of exactly the two fixture files —
# and its ``finished_at`` (non-null) is the ISO the badge's stamp
# is computed from (the stamp's server-side half).
assert terminal["detail"]["added"] == len(SYNC_FILES), terminal
assert terminal["detail"]["pruned"] == 0, terminal
assert terminal["files_done"] == len(SYNC_FILES), terminal
assert terminal["files_total"] == len(SYNC_FILES), terminal
assert terminal["finished_at"], terminal
# ---------------------------------------------------------------------------
# 2. The upload line's fresh settle carries the name AND the stamp
# (phase 154, task 02 — the phase-90 no-count contract extended)
# ---------------------------------------------------------------------------
def test_upload_line_fresh_settle_carrys_name_and_stamp(
page: Page,
app_url: str,
db_ready: None,
tarball_a: Path,
upload_dir: Path,
) -> None:
"""One real upload through the page: the 202 fires the
"Successfully uploaded — <source>" toast (phase 64, A2); the
sub-second background run (unpack + register only — phase 90)
settles at the next 2 s poll tick into the ready-for-sync line
``Uploaded <name> (<stamp>) — press Sync sources to import it.``
— the SAFE name (the 202's ``UploadAccepted.name`` — the
phase-90 contract) + the phase-154 COMPLETION STAMP
(``fmtUploadStamp`` off the server payload's ``finished_at`` —
the shape pin: same-day ``HH:MM`` in practice, the date-qualified
forms covered by the shape regex). The never-stale restore
(button + input) lands with the line, the terminal status is the
no-count payload (``{"message": "uploaded"}``, null/0/0 — the
phase-90 A2 key set) with a NON-NULL ``finished_at`` (the stamp's
source), the source row is present (Local badge), and the
unpacked folder exists on the host (the run did its unpack +
register work — zero documents indexed: the scan is the Sync
button's job, phase 90, A1)."""
page.set_default_timeout(30_000)
_admin_git_sources_page(page, app_url)
expect(page.locator("#git-sources-tbody tr")).to_have_count(0)
btn = page.locator("#archive-upload-btn")
expect(btn).to_be_enabled()
expect(btn).to_have_text("Upload")
_upload_via_page(page, tarball_a)
# The 202's toast names the safe source name (phase 64, A2)…
toast = page.locator(".toast")
expect(toast).to_have_attribute("role", "status")
expect(toast).to_have_text(f"Successfully uploaded — {UPLOAD_A}")
# …and the settle: the ready-for-sync line WITH the name and the
# completion stamp (phase 154, task 02 — the shape pin).
result = page.locator("#archive-upload-result")
expect(result).to_be_visible(timeout=UPLOAD_TIMEOUT_MS)
expect(result).to_have_text(_ready_line(UPLOAD_A))
# The stamp is present and non-empty (the regex's capture is the
# stamp itself — the pre-phase string has no "(…)" at all, so a
# match IS the stamp's proof).
m = _ready_line(UPLOAD_A).fullmatch(result.text_content() or "")
assert m is not None, "the settled line did not match the named + stamped shape"
assert m.group(1), "the stamp is empty in the settled line"
# The never-stale restore (the poll's success tick owns it)…
expect(btn).to_be_enabled()
expect(btn).to_have_text("Upload")
expect(page.locator("#archive-upload-file")).to_have_value("")
# …and the terminal status: the no-count payload (phase 90, A2)
# with the NON-NULL finished_at the stamp is computed from.
status = _status(page, app_url, "/api/git-sources/upload/status")
assert status["state"] == "success", status
assert status["detail"] == {"message": "uploaded"}, status
assert status["current_file"] is None, status
assert status["files_done"] == 0 and status["files_total"] == 0, status
assert status["finished_at"], status
assert status["started_at"], status
# The row landed (Local badge) and the folder is on the host —
# but the upload indexed NOTHING (phase 90, A1 — the scan is the
# Sync button's job).
row = page.locator("#git-sources-tbody tr", has_text=UPLOAD_A)
expect(row).to_have_count(1, timeout=30_000)
expect(row.locator("span.git-source-kind")).to_have_text("Local")
assert (upload_dir / UPLOAD_A).is_dir(), (
f"the unpacked folder is missing: {upload_dir / UPLOAD_A}"
)
r = page.request.get(f"{app_url}/api/docs")
assert r.status == 200, r.text
assert r.json()["documents"] == []
# ---------------------------------------------------------------------------
# 3. The boot re-render is nameless but STAMPED — the phase's actual
# fix (the stale "Uploaded" line is self-dating after a reload)
# ---------------------------------------------------------------------------
def test_upload_line_boot_rerender_stamped_nameless(
page: Page,
app_url: str,
db_ready: None,
tarball_b: Path,
) -> None:
"""Seed a settled upload (the fresh named line — the run's
identity pinned on the status's ``started_at``), then RELOAD the
page: the boot re-attach (``initUploadStatus``'s success branch —
phase 64, task 05) re-renders the ready-for-sync line — the SAFE
NAME IS GONE (``lastUploadName`` was page-local, null after a
reload — the phase-90 contract) but the STAMP SURVIVES
(``fmtUploadStamp`` reads the SERVER payload's ``finished_at`` —
phase 154, task 02 — the line is self-dating even with the name
unknown; THIS is the regression the phase fixes: a line that
outlives its upload can no longer masquerade as fresh). The page
boots honest (no error banner, no toast — it fired at the 202 on
the previous document life — the button restored), the list shows
exactly one row for the archive, and the terminal status is the
SAME run (the ``started_at`` is unchanged — no second upload)."""
page.set_default_timeout(30_000)
_admin_git_sources_page(page, app_url)
# The seed: a settled upload (the fresh named line with the
# stamp — test 2's shape, re-proven here as the pre-reload state).
_upload_via_page(page, tarball_b)
expect(page.locator(".toast")).to_have_text(
f"Successfully uploaded — {UPLOAD_B}"
)
result = page.locator("#archive-upload-result")
expect(result).to_be_visible(timeout=UPLOAD_TIMEOUT_MS)
expect(result).to_have_text(_ready_line(UPLOAD_B))
# The run's identity (a second upload would reset it — the
# single-run claim is pinned on it).
before = _status(page, app_url, "/api/git-sources/upload/status")
assert before["state"] == "success", before
started_at = before["started_at"]
assert started_at, before
# The reload (the session cookie survives — the boot re-attach is
# the admin's).
page.reload()
expect(page).to_have_url(app_url + GIT_SOURCES_URL, timeout=30_000)
expect(page.locator("#sign-out-btn")).to_be_visible(timeout=15_000)
expect(page.locator("#git-sources-content")).to_be_visible()
# The boot re-render: the NAMELESS line WITH the stamp (phase
# 154, task 02 — the stamp from the server payload). The
# nameless shape has NO "(…)" and NO <name> — the regex's match
# IS the nameless+stamped proof (the pre-phase nameless string
# "Uploaded — press …" has no stamp at all).
expect(result).to_have_text(NAMELESS_READY_LINE, timeout=UPLOAD_TIMEOUT_MS)
m = NAMELESS_READY_LINE.fullmatch(result.text_content() or "")
assert m is not None, "the boot re-render line did not match the nameless + stamped shape"
assert m.group(1), (
"the stamp is empty in the boot re-render line"
)
# The page boots honest: no error banner, no toast at boot (A2 —
# the toast fired at the 202 on the previous document life), the
# never-stale restore (button re-enabled / "Upload").
expect(page.locator("#archive-upload-error")).to_be_hidden()
expect(page.locator(".toast")).to_have_count(0)
expect(page.locator("#archive-upload-btn")).to_be_enabled()
expect(page.locator("#archive-upload-btn")).to_have_text("Upload")
# The list shows exactly one row for the archive (the in-place
# identity survived the reload).
expect(page.locator("#git-sources-tbody tr")).to_have_count(1)
row = page.locator("#git-sources-tbody tr", has_text=UPLOAD_B)
expect(row).to_have_count(1)
expect(row.locator("span.git-source-kind")).to_have_text("Local")
# No second upload: the terminal status is the SAME run — its
# started_at is the one from before the reload.
terminal = _status(page, app_url, "/api/git-sources/upload/status")
assert terminal["state"] == "success", terminal
assert terminal["started_at"] == started_at, (
f"the run's identity changed (a second upload ran): {terminal['started_at']}"
)
assert terminal["detail"] == {"message": "uploaded"}, terminal
assert terminal["finished_at"], terminal
# ---------------------------------------------------------------------------
# 4. A running upload never renders the ready-for-sync line (the
# phase-90 regression guard, extended — the line appears ONLY on
# settle)
# ---------------------------------------------------------------------------
def test_running_upload_line_unchanged(
page: Page,
app_url: str,
db_ready: None,
tarball_c: Path,
) -> None:
"""At the 202 moment the upload run is in flight: the button is
the BARE "Processing…" (phase 90, A2 — no file, no "(n/m)"
counts, the title cleared) and ``#archive-upload-result`` is
HIDDEN — the ready-for-sync line NEVER renders during the run
(the phase-90 contract: the line appears ONLY on settle). The
deterministic in-flight window: the 202 handler synchronously
enters the processing state and schedules the first poll tick at
+2 s, so the "Processing…" state holds for ≥ 2 s regardless of
how fast the sub-second background run finishes — no delay proxy
is needed (the upload makes zero LLM calls). Then the poll's
success tick settles the line (the NAMED shape with the
completion stamp — the line appears only on settle), with the
never-stale restore and no error UI throughout."""
page.set_default_timeout(30_000)
_admin_git_sources_page(page, app_url)
btn = page.locator("#archive-upload-btn")
expect(btn).to_be_enabled()
expect(btn).to_have_text("Upload")
expect(page.locator("#archive-upload-error")).to_be_hidden()
# The 202 moment (the toast fires at the 202 — the run is in
# flight from here until the poll's success tick). The submit
# handler hid the result line before the fetch (a new attempt
# starts clean), so at the in-flight moment the line is hidden
# even though the shared module app's boot re-render may have
# re-shown the PREVIOUS test's settled line at page load (the
# phase-154 boot re-attach — the line's self-dating contract,
# test 3's subject — not a partial-state regression).
_upload_via_page(page, tarball_c)
expect(page.locator(".toast")).to_have_text(
f"Successfully uploaded — {UPLOAD_C}"
)
# The IN-FLIGHT state (the ≥ 2 s window — deterministic): the
# bare "Processing…" label (phase 90, A2), the button disabled
# (the poll owns it — PLAN §7.4), the title cleared, and the
# ready-for-sync line HIDDEN (the line appears ONLY on settle).
expect(btn).to_be_disabled()
expect(btn).to_have_text("Processing…")
expect(btn).to_have_attribute("title", "")
expect(page.locator("#archive-upload-result")).to_be_hidden()
expect(page.locator("#archive-upload-error")).to_be_hidden()
# The in-flight status: the bare payload (phase 90, A2 — the
# run's unpack + register is in flight or just finished; either
# way the line is the poll tick's job, not the in-flight state's).
running = _status(page, app_url, "/api/git-sources/upload/status")
assert running["state"] in ("running", "success"), running
# The SETTLE (the poll's success tick — the line appears ONLY
# here, and the line is THIS run's — the named shape with the
# completion stamp; the distinct safe name also proves the
# previous test's boot re-render line was replaced, not carried
# through) + the never-stale restore, no error UI.
expect(page.locator("#archive-upload-result")).to_have_text(
_ready_line(UPLOAD_C), timeout=UPLOAD_TIMEOUT_MS
)
expect(btn).to_be_enabled()
expect(btn).to_have_text("Upload")
expect(page.locator("#archive-upload-error")).to_be_hidden()
status = _status(page, app_url, "/api/git-sources/upload/status")
assert status["state"] == "success", status
assert status["detail"] == {"message": "uploaded"}, status
assert status["finished_at"], status
+34 -5
View File
@@ -16,8 +16,9 @@ unpack → swap → row upsert continues server-side behind
pattern; the phase-64 key set with ``current_file``/``files_done``/
``files_total`` null/0/0 for the whole run — phase 90 A2), and the
upload's UI processing state is the BARE "Processing…" (no file, no
"(n/m)") until the no-count "Uploaded <name> — press Sync sources to
import it." result line lands (phase 90 A3). The scan — with its live
"(n/m)") until the no-count "Uploaded <name> (<stamp>) — press Sync
sources to import it." result line lands (phase 90 A3 — the phase-154
completion stamp). The scan — with its live
file label — is the RAG page's **Sync sources** button's job, and the
suite proves the new loop: upload → nothing indexed → **the sync that
follows the upload shows its live file label and lands the counts**.
@@ -159,6 +160,31 @@ SLOW_DELAY_S = "0.15"
#: The uploaded archive: 25 small docs under ``docs/``.
UPLOAD_NAME = "e2e-prog"
def _ready_for_sync_line(name: str) -> re.Pattern[str]:
"""Phase 154 (task 02): the ready-for-sync line carrying the
completion stamp — "Uploaded <name> (<stamp>) — press Sync sources
to import it." (the fresh 202 settle, the page-local name). In
E2E the stamp is ALWAYS the same-day ``HH:MM`` form — the run
settles within seconds (the date-qualified forms are unit-pinned
on a fixed clock — E2E never depends on the wall clock)."""
return re.compile(
rf"^Uploaded {re.escape(name)} \(\d{{1,2}}:\d{{2}}\)"
r" — press Sync sources to import it\.$"
)
def _nameless_ready_for_sync_line() -> re.Pattern[str]:
"""Phase 154 (task 02): the boot re-render line — nameless (the
safe name was page-local — ``lastUploadName`` is null after a
reload, phase 90) but SELF-DATING through the completion stamp
(``fmtUploadStamp`` from the server payload): "Uploaded <stamp> —
press Sync sources to import it." (the same-day ``HH:MM`` form in
E2E)."""
return re.compile(
r"^Uploaded \d{1,2}:\d{2} — press Sync sources to import it\.$"
)
UPLOAD_ARCHIVE = f"{UPLOAD_NAME}.tar.gz"
N_FILES = 25
UPLOAD_FILES: dict[str, str] = {
@@ -747,7 +773,7 @@ def test_upload_progress_is_bare(
# the archive — and the KB empty (no scan, phase 90 A1).
result = page.locator("#archive-upload-result")
expect(result).to_have_text(
f"Uploaded {UPLOAD_NAME} — press Sync sources to import it.",
_ready_for_sync_line(UPLOAD_NAME),
timeout=30_000,
)
expect(btn).to_be_enabled()
@@ -915,9 +941,12 @@ def test_upload_reattach_after_reload(
expect(page.locator("#archive-upload-error")).to_be_hidden()
expect(page.locator(".toast")).to_have_count(0)
# …and the settled result line: the nameless variant (the safe
# name was page-local — lastUploadName is null after a reload).
# name was page-local — lastUploadName is null after a reload),
# self-dating through the completion stamp (phase 154 — the stamp
# comes from the server payload, so the line is still self-dating
# with the name unknown).
expect(page.locator("#archive-upload-result")).to_have_text(
"Uploaded — press Sync sources to import it.",
_nameless_ready_for_sync_line(),
timeout=SETTLE_TIMEOUT_MS,
)
expect(page.locator("#archive-upload-btn")).to_be_enabled()
+27 -13
View File
@@ -54,8 +54,9 @@ Contract under test:
* **test 1 — upload does not scan**: the "Upload" button (phase 90 A3)
→ the archive → the 202 "Successfully uploaded — <source>" toast →
the settled result line "Uploaded <source> — press Sync sources to
import it." with the terminal no-count status payload
the settled result line "Uploaded <source> (<stamp>) — press Sync
sources to import it." (the phase-154 completion stamp) with the
terminal no-count status payload
(``{"message": "uploaded"}``, null/0/0 progress — phase 90 A2); the
source row is present (Local badge + the phase-89 "Ignore paths"
control) and its folder exists on the host under ``BOR_UPLOAD_DIR``
@@ -127,6 +128,19 @@ SOURCES_URL = "/sources.html"
#: filename minus the archive suffix (the phase-49 naming rule).
SOURCE_NAME = "e2e-upload-no-scan"
def _ready_for_sync_line(name: str) -> re.Pattern[str]:
"""Phase 154 (task 02): the ready-for-sync line carrying the
completion stamp — "Uploaded <name> (<stamp>) — press Sync sources
to import it." (the fresh 202 settle, the page-local name). In
E2E the stamp is ALWAYS the same-day ``HH:MM`` form — the run
settles within seconds (the date-qualified forms are unit-pinned
on a fixed clock — E2E never depends on the wall clock)."""
return re.compile(
rf"^Uploaded {re.escape(name)} \(\d{{1,2}}:\d{{2}}\)"
r" — press Sync sources to import it\.$"
)
#: v1: two sentinel docs + the notes/ file the phase-89 ignore entry
#: (``notes``) excludes from the sync. v2 (same basename): beta CHANGED,
#: alpha DROPPED, gamma ADDED — the in-place-replace subject (the
@@ -394,7 +408,8 @@ def test_upload_does_not_scan(
"""One real upload through the page: the button reads **Upload**
(phase 90 A3); the 202 fires the "Successfully uploaded — <source>"
toast; the settled result line points at the next step — "Uploaded
<source> — press Sync sources to import it." (A3) — with the
<source> (<stamp>) — press Sync sources to import it." (A3 — the
phase-154 completion stamp) — with the
terminal no-count status payload (``{"message": "uploaded"}``,
null/0/0 progress — A2). The source row is present (Local badge +
the phase-89 "Ignore paths" control) and its folder exists on the
@@ -428,9 +443,7 @@ def test_upload_does_not_scan(
# never-stale restore (button + input)…
result = page.locator("#archive-upload-result")
expect(result).to_be_visible(timeout=UPLOAD_TIMEOUT_MS)
expect(result).to_have_text(
f"Uploaded {SOURCE_NAME} — press Sync sources to import it."
)
expect(result).to_have_text(_ready_for_sync_line(SOURCE_NAME))
expect(btn).to_be_enabled()
expect(btn).to_have_text("Upload")
expect(page.locator("#archive-upload-file")).to_have_value("")
@@ -507,9 +520,10 @@ def test_ignore_list_then_sync_scans(
page.set_default_timeout(30_000)
_admin_git_sources_page(page, app_url)
# Upload: the ready-for-sync line, and still nothing indexed.
assert _upload_via_page(page, tarball_v1) == (
f"Uploaded {SOURCE_NAME} — press Sync sources to import it."
# Upload: the ready-for-sync line (the phase-154 completion
# stamp), and still nothing indexed.
assert _ready_for_sync_line(SOURCE_NAME).fullmatch(
_upload_via_page(page, tarball_v1)
)
assert _docs(page, app_url) == []
expect(page.locator("#git-sources-tbody tr", has_text=SOURCE_NAME)).to_have_count(1)
@@ -622,8 +636,8 @@ def test_reupload_replaces_without_scan(
# Baseline: v1 through the page (202 → the ready-for-sync line,
# one row, no index).
assert _upload_via_page(page, tarball_v1) == (
f"Uploaded {SOURCE_NAME} — press Sync sources to import it."
assert _ready_for_sync_line(SOURCE_NAME).fullmatch(
_upload_via_page(page, tarball_v1)
)
expect(page.locator("#git-sources-tbody tr", has_text=SOURCE_NAME)).to_have_count(1)
folder = upload_dir / SOURCE_NAME
@@ -632,8 +646,8 @@ def test_reupload_replaces_without_scan(
# Re-upload v2 — SAME basename, different parent dir (the file
# input's selection is replaced wholesale).
assert _upload_via_page(page, tarball_v2) == (
f"Uploaded {SOURCE_NAME} — press Sync sources to import it."
assert _ready_for_sync_line(SOURCE_NAME).fullmatch(
_upload_via_page(page, tarball_v2)
)
# No duplicate: exactly ONE row for that source (and one row
+425 -18
View File
@@ -149,6 +149,46 @@ positions are distinct and each disappears independently. The node
harness gains the two in-flight image rows (both parts at once —
the owner's stuck label — and the image-only pictorial PDF) and
pins the composed full string byte-for-byte.
Phase 154 (task 02) extends the Sources-page upload line with the
COMPLETION STAMP (the phase's date-qualified freshness): the settled
line reads "Uploaded <name> (<stamp>) — press Sync sources to import
it." (the fresh 202 settle, the page-local name) or the nameless
"Uploaded <stamp> — press Sync sources to import it." (the boot
re-render — the stamp comes from the SERVER payload, so the line is
self-dating even with the name unknown); a running / legacy / partial
payload without timestamps renders the byte-identical pre-phase-154
strings (the nameful / nameless "Uploaded … — press Sync sources to
import it." lines). The stamp is PRECOMPUTED by the page-local
fmtUploadStamp(status) (the per-page duplication idiom — the same A1
date-qualifier as sources.js's fmtSyncTime, duplicated, not shared;
the rule's boundary set is pinned in test_sync_button.py on a fixed
clock): it reads status.finished_at first (the success state always
has it on the server), falls back to status.started_at (a defensive
guard — a corrupt finished_at degrades to the started_at stamp rather
than to no stamp), and yields "" when both are absent or every ISO is
unparseable. Both render sites — the poll's success settle and the
boot re-attach — pass fmtUploadStamp(status) as the third argument;
the NO server change (the /upload/status payload is untouched —
started_at / finished_at already shipped, phase 64).
Phase 154 (task 03) pins the BYTE-IDENTITY of the no-stamp fallbacks
against the VERBATIM pre-phase-154 strings: the old 2-arg
fmtUploadResult (git history) is pinned in this file as a reference
implementation (the name renamed, the body byte-for-byte) and run
side by side with the real 3-arg function under node — the no-stamp
(nameful / nameless) outputs + the legacy 2-arg call are asserted
byte-identical to the reference's output, and the reference's
output to the verbatim old line strings (assertable, not assumed).
Clock-free: `when` is a precomputed argument, so the no-stamp
shapes need no clock at all. The /upload/status full-dict pin
(tests/integration/test_git_sources_upload.py, the phase-64
precedent) stays UNCHANGED — this phase adds no server keys (the
app/ no-touch regression guard). The qualifier's byte-identity
(same-day + fallbacks vs the verbatim pre-phase fmtSyncTime) is
pinned in test_sync_button.py on the shared fixed clock; the four
shapes and the fmtUploadStamp behavior (finished / started /
missing / garbage) ride the task-02 pins below.
"""
from __future__ import annotations
@@ -1639,23 +1679,42 @@ def test_processing_state_is_bare_for_the_whole_run() -> None:
def test_fmt_upload_result_is_the_ready_for_sync_line() -> None:
"""Phase 90 (A2/A3): the result line reads the no-count
{"message": "uploaded"} detail and renders "Uploaded <name> —
press Sync sources to import it." (the name from the accepted
202); without a name (a reload re-render, the 409 re-attach) it
renders the nameless variant; the old sync-style count keys
"""Phase 90 (A2/A3) extended by Phase 154 (task 02): the result
line reads the no-count {"message": "uploaded"} detail and renders
the ready-for-sync line in the FOUR phase-154 shapes —
"Uploaded <name> (<stamp>) — press Sync sources to import it." (the
name + the completion stamp), "Uploaded <name> — press Sync sources
to import it." (name, no stamp — BYTE-IDENTICAL to the pre-phase
string), "Uploaded <stamp> — press Sync sources to import it." (no
name + stamp — the boot re-render, self-dating with the name
unknown), "Uploaded — press Sync sources to import it." (no name,
no stamp — BYTE-IDENTICAL to the pre-phase string; the running /
legacy / partial payload). The non-uploaded detail keeps the
unchanged "The upload finished."; the old sync-style count keys
(added / updated / unchanged / pruned) are no longer read at all.
The 202 branch records the safe name (lastUploadName); the poll's
settle and the boot re-attach both render the line from
(detail, lastUploadName); the button's idle label is "Upload"."""
settle and the boot re-attach both render the line from (detail,
lastUploadName, fmtUploadStamp(status)); the button's idle label
is "Upload"."""
js = _gjs()
body = _gfn(js, "fmtUploadResult")
assert "function fmtUploadResult(detail, name)" in body
assert 'detail.message === "uploaded"' in body
assert "Uploaded ${name} — press Sync sources to import it." in body
assert "Uploaded — press Sync sources to import it." in body, (
"the nameless variant (reload / 409 re-attach)"
assert "function fmtUploadResult(detail, name, when)" in body, (
"phase 154: the precomputed completion stamp is the third parameter"
)
assert 'detail.message === "uploaded"' in body
# The four shapes (name×stamp, name×no-stamp, no-name×stamp,
# no-name×no-stamp).
assert "Uploaded ${name} (${when}) — press Sync sources to import it." in body
assert "Uploaded ${name} — press Sync sources to import it." in body, (
"the no-stamp nameful variant — byte-identical to pre-phase"
)
assert "Uploaded ${when} — press Sync sources to import it." in body, (
"the nameless + stamp variant (the boot re-render — self-dating)"
)
assert "Uploaded — press Sync sources to import it." in body, (
"the nameless no-stamp variant — byte-identical to pre-phase"
)
assert '"The upload finished."' in body, ("the non-uploaded branch is unchanged")
for key in ("added", "updated", "unchanged", "pruned"):
assert key not in body, f"the sync-style count {key!r} is gone"
# The 202 branch records the safe name for the settled line.
@@ -1664,9 +1723,19 @@ def test_fmt_upload_result_is_the_ready_for_sync_line() -> None:
i409 = sub.find("r.status === 409")
assert "lastUploadName = name" in sub[i202:i409]
assert "let lastUploadName = null" in js, "page-local (null after a reload)"
# Both render sites pass (detail, lastUploadName).
assert "fmtUploadResult(detail, lastUploadName)" in _utick(js)
assert "fmtUploadResult(status.detail, lastUploadName)" in _gfn(js, "initUploadStatus")
# Both render sites pass (…, lastUploadName, fmtUploadStamp(status)).
tick = _utick(js)
assert re.search(
r"uploadResult\.textContent = fmtUploadResult\(\s*detail,\s*"
r"lastUploadName,\s*fmtUploadStamp\(status\)\s*\)",
tick,
), "the poll's success settle passes the completion stamp (phase 154)"
init = _gfn(js, "initUploadStatus")
assert re.search(
r"uploadResult\.textContent = fmtUploadResult\(\s*status\.detail,\s*"
r"lastUploadName,\s*fmtUploadStamp\(status\)\s*\)",
init,
), "the boot re-attach passes the completion stamp (phase 154)"
# The button's idle label (static + the poll's restores).
restore = _gfn(js, "restoreUploadButton")
assert 'uploadBtn.textContent = "Upload"' in restore
@@ -1751,7 +1820,7 @@ def test_upload_polling_decision_tree() -> None:
# next-step announce + reload, NO toast.
ok = tick[i_ok:i_fail]
for line in (
"fmtUploadResult(detail, lastUploadName)",
"fmtUploadResult(",
"uploadResult.hidden = false",
'announce("Archive uploaded — press Sync sources to import it.")',
'uploadFileInput.value = ""',
@@ -1759,6 +1828,11 @@ def test_upload_polling_decision_tree() -> None:
"loadSources()",
):
assert line in ok, f"the success settle must carry {line!r}"
assert re.search(
r"uploadResult\.textContent = fmtUploadResult\(\s*detail,\s*"
r"lastUploadName,\s*fmtUploadStamp\(status\)\s*\)",
ok,
), "the success settle passes the completion stamp (phase 154)"
assert "showUploadToast" not in tick, "no toast in the poll — it fired at the 202 (A2)"
# failed: the sanitized error banner + reload.
fail = tick[i_fail:]
@@ -1815,8 +1889,14 @@ def test_boot_reattach_branches() -> None:
assert "uploadError" not in run and "showUploadToast" not in run
ok = body[i_ok:i_fail]
# The last result line — the nameless variant after a reload
# (lastUploadName is null: the safe name was page-local, phase 90).
assert "fmtUploadResult(status.detail, lastUploadName)" in ok
# (lastUploadName is null: the safe name was page-local, phase 90),
# self-dating through the completion stamp (phase 154 — the stamp
# comes from the server payload).
assert re.search(
r"uploadResult\.textContent = fmtUploadResult\(\s*status\.detail,\s*"
r"lastUploadName,\s*fmtUploadStamp\(status\)\s*\)",
ok,
), "the boot re-attach passes the completion stamp (phase 154)"
assert "uploadResult.hidden = false" in ok
assert "announce(" not in ok, "no announce at boot (A2)"
assert "showUploadToast" not in ok, "no toast at boot (A2)"
@@ -1860,3 +1940,330 @@ def test_git_sources_html_comment_documents_the_202_contract() -> None:
assert "Phase 90" in comment, "the unpack-only rework is documented"
assert "UNPACK + REGISTER ONLY" in comment
assert "Sync sources" in comment, "the result line points at the RAG page's button"
# =====================================================================
# Phase 154 (task 02) — the Sources upload line's completion stamp
# =====================================================================
def test_fmt_upload_stamp_is_the_duplicated_qualifier() -> None:
"""Phase 154 (task 02): ``fmtUploadStamp(status)`` — the page-local
DUPLICATE of sources.js's ``fmtSyncTime`` date-qualifier (the
per-page duplication idiom — not shared). It reads
``status.finished_at`` first (the success state always has it on
the server), falls back to ``status.started_at`` (the defensive
guard — a corrupt ``finished_at`` degrades to the ``started_at``
stamp rather than to no stamp), and yields ``""`` when both are
missing/absent or every ISO is unparseable (the byte-identical
pre-phase-154 fallback). The qualifier is the SAME A1 structure as
the phase-154 task-01 pin in test_sync_button.py — local calendar
dates, ``HH:MM`` same day, ``M/D, h:MM AM/PM`` same year,
``M/D/YYYY, h:MM AM/PM`` across years, the 12-hour form WITHOUT a
leading zero on the hour, month/day without leading zeros — cited
in sources.js, not restated. The ``status || {}`` guard defends a
missing status object."""
js = _gjs()
body = _gfn(js, "fmtUploadStamp")
assert "function fmtUploadStamp(status)" in body
assert "const s = status || {};" in body, (
"defensive: a missing / null status object yields ''"
)
assert "s.finished_at" in body and "s.started_at" in body
i_fin = body.find("s.finished_at")
i_st = body.find("s.started_at")
assert -1 < i_fin < i_st, "finished_at is read first — the started_at fallback"
assert "if (!iso) continue;" in body, (
"absent / null timestamps are skipped (the fallback reads)"
)
assert "if (!Number.isNaN(t.getTime()))" in body, (
"an unparseable ISO is skipped — a corrupt finished_at degrades"
" to started_at, '' when neither parses"
)
assert 'if (d === null) return "";' in body, (
"both absent (or unparseable) → '' — the byte-identical pre-"
"phase-154 fallback"
)
# The duplicated A1 qualifier (cited in sources.js's fmtSyncTime,
# not restated) — the same structure as the task-01 pin.
assert "const now = new Date();" in body, "'now' is the page's clock"
assert (
"d.getFullYear() === now.getFullYear() &&" in body
and "d.getMonth() === now.getMonth()" in body
and "d.getDate() === now.getDate()" in body
), "same local calendar day as now → HH:MM (byte-identical to pre-phase)"
assert "d.getHours() % 12 || 12" in body, (
"12-hour WITHOUT leading zero on the hour (the 12 o'clock edge)"
)
assert '"AM"' in body and '"PM"' in body, "the AM/PM family"
assert "pad(d.getMinutes())" in body, "MM zero-padded in every form"
assert "`${d.getMonth() + 1}/${d.getDate()}`" in body, (
"M/D, no leading zeros (same year, different day)"
)
assert "`${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`" in body, (
"M/D/YYYY across years"
)
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_upload_result_four_shapes_under_node() -> None:
"""Phase 154 (task 02): the REAL git-sources.js ``fmtUploadResult``
under node — the four shapes, ``when`` PRECOMPUTED by the caller
(so the shape itself is clock-free):
* name + stamp: ``Uploaded docs.zip (21:14) — press Sync sources
to import it.`` (the fresh 202 settle, the page-local name);
* name, no stamp (``""``): ``Uploaded docs.zip — press Sync
sources to import it.`` — BYTE-IDENTICAL to the pre-phase-154
string (the running / legacy / partial payload);
* no name + stamp: ``Uploaded 10/2, 3:14 PM — press Sync sources
to import it.`` (the boot re-render — self-dating with the name
unknown);
* no name, no stamp (``""``): ``Uploaded — press Sync sources to
import it.`` — BYTE-IDENTICAL to the pre-phase-154 string;
* a legacy 2-arg call (``when`` undefined) renders the pre-phase
nameful string (the fallback is falsy-driven);
* the non-``uploaded`` detail and a missing detail keep the
unchanged ``The upload finished.``."""
js = _gjs()
fn = _gfn(js, "fmtUploadResult")
script = (
fn
+ "\n"
'const S = { message: "uploaded" };\n'
'const O = { message: "other" };\n'
"console.log(JSON.stringify([\n"
' fmtUploadResult(S, "docs.zip", "21:14"),\n'
' fmtUploadResult(S, "docs.zip", ""),\n'
' fmtUploadResult(S, null, "10/2, 3:14 PM"),\n'
' fmtUploadResult(S, null, ""),\n'
' fmtUploadResult(S, "docs.zip"),\n'
' fmtUploadResult(O, "docs.zip", "21:14"),\n'
" fmtUploadResult(null, \"docs.zip\", \"21:14\"),\n"
"]));\n"
)
rows = json.loads(_run_node(script))
assert rows == [
"Uploaded docs.zip (21:14) — press Sync sources to import it.",
# byte-identical to pre-phase (the no-stamp nameful variant):
"Uploaded docs.zip — press Sync sources to import it.",
"Uploaded 10/2, 3:14 PM — press Sync sources to import it.",
# byte-identical to pre-phase (the no-stamp nameless variant):
"Uploaded — press Sync sources to import it.",
# the legacy 2-arg call (when undefined → falsy → pre-phase):
"Uploaded docs.zip — press Sync sources to import it.",
"The upload finished.",
"The upload finished.",
], rows
# =====================================================================
# Phase 154 (task 03) — the byte-identity pins (the pre-phase
# strings, pinned verbatim — assertable, not assumed)
# =====================================================================
#: The pre-phase-154 fmtUploadResult, pinned VERBATIM from git
#: history (the 2-arg no-count formatter — the phase-90 contract
#: pre-stamp). Only the function NAME is renamed (so the real + the
#: reference run side by side under node); the body is the old code
#: byte-for-byte — the two old line strings + "The upload finished."
#: live in this constant, so the byte-identity below is assertable,
#: not assumed.
PRE_PHASE_FMT_UPLOAD_RESULT_JS = """
function prePhaseUploadResult(detail, name) {
if (detail && detail.message === "uploaded") {
return name
? `Uploaded ${name} — press Sync sources to import it.`
: "Uploaded — press Sync sources to import it.";
}
return "The upload finished.";
}
"""
#: The two pre-phase-154 result-line strings, pinned verbatim as
#: constants (the byte-identity targets — the task-03 completion
#: criterion). The nameful one is the old template instantiated with
#: the test's input name.
PRE_PHASE_UPLOAD_LINE_NAMEFUL = "Uploaded docs.zip — press Sync sources to import it."
PRE_PHASE_UPLOAD_LINE_NAMELESS = "Uploaded — press Sync sources to import it."
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_upload_result_no_stamp_is_byte_identical_to_pre_phase() -> None:
"""Phase 154 (task 03): the two BYTE-IDENTITY fallbacks — the
REAL 3-arg ``fmtUploadResult`` and the VERBATIM pre-phase 2-arg
formatter (pinned above) run side by side under node (clock-free
— ``when`` is precomputed):
* name + ``when === ""`` → the pre-phase nameful string,
byte-identical (the running / legacy / partial payload);
* no name + ``when === ""`` → the pre-phase nameless string,
byte-identical (the same payload, the name unknown);
* the legacy 2-arg call (``when`` undefined → falsy) renders the
SAME two byte-identical strings (the old call shape is
unchanged in output);
* the non-``uploaded`` detail (and a missing detail) keep
``The upload finished.`` on BOTH sides (the branch the phase
never touched).
The reference implementation's own output is asserted against the
verbatim old strings FIRST (a self-check on the pinned constant —
a typo in the pinned function fails here, not silently), so the
real-vs-reference identity is an assertion against the verbatim
pre-phase strings (the completion criterion), not an
assumption."""
# Source-level self-check: the pinned constant carries the old
# strings verbatim (both line templates + the non-uploaded
# branch).
assert (
"return name\n"
" ? `Uploaded ${name} — press Sync sources to import it.`\n"
' : "Uploaded — press Sync sources to import it.";'
in PRE_PHASE_FMT_UPLOAD_RESULT_JS
), "the old nameful + nameless line templates are pinned verbatim"
assert '"The upload finished."' in PRE_PHASE_FMT_UPLOAD_RESULT_JS, (
"the non-uploaded branch is pinned verbatim"
)
fn = _gfn(_gjs(), "fmtUploadResult")
script = (
fn
+ "\n"
+ PRE_PHASE_FMT_UPLOAD_RESULT_JS
+ "\n"
'const S = { message: "uploaded" };\n'
'const O = { message: "other" };\n'
"console.log(JSON.stringify({\n"
" REAL: [\n"
' fmtUploadResult(S, "docs.zip", ""),\n'
" fmtUploadResult(S, null, \"\"),\n"
' fmtUploadResult(S, "docs.zip"),\n' # legacy 2-arg call
" fmtUploadResult(S, null),\n" # legacy 2-arg call
' fmtUploadResult(O, "docs.zip", "21:14"),\n'
' fmtUploadResult(null, "docs.zip", "21:14"),\n'
" ],\n"
" PRE: [\n"
' prePhaseUploadResult(S, "docs.zip"),\n'
" prePhaseUploadResult(S, null),\n"
' prePhaseUploadResult(O, "docs.zip"),\n'
' prePhaseUploadResult(null, "docs.zip"),\n'
" ]\n"
"}));\n"
)
rows = json.loads(_run_node(script))
# 1. the pinned reference reproduces the verbatim old strings
# (self-check on the constant — assertable, not assumed).
assert rows["PRE"] == [
PRE_PHASE_UPLOAD_LINE_NAMEFUL,
PRE_PHASE_UPLOAD_LINE_NAMELESS,
"The upload finished.",
"The upload finished.",
], rows
# 2. the no-stamp + legacy shapes are byte-identical to the
# reference (i.e. to the verbatim old strings).
assert rows["REAL"][:4] == [
PRE_PHASE_UPLOAD_LINE_NAMEFUL, # name + when === ""
PRE_PHASE_UPLOAD_LINE_NAMELESS, # no name + when === ""
PRE_PHASE_UPLOAD_LINE_NAMEFUL, # legacy 2-arg call
PRE_PHASE_UPLOAD_LINE_NAMELESS, # legacy 2-arg call
], rows
assert rows["REAL"][0] == rows["PRE"][0], (
"the nameful no-stamp shape is byte-identical to pre-phase"
)
assert rows["REAL"][1] == rows["PRE"][1], (
"the nameless no-stamp shape is byte-identical to pre-phase"
)
# 3. the non-uploaded branch is unchanged on both sides.
assert rows["REAL"][4] == rows["PRE"][2] == "The upload finished."
assert rows["REAL"][5] == rows["PRE"][3] == "The upload finished."
#: Phase 154 (task 02): the fixed clock for the stamp's boundary set —
#: the SAME fixed now as test_sync_button.py's task-01 pin (Oct 3,
#: 2026, 18:30 LOCAL). The test body never calls `new Date()` for its
#: own timing: the fake clock is installed in the driver (global Date
#: patched BEFORE the call — `new Date()` → the fixed now,
#: `new Date(iso)` → the real parse), and every ISO is
#: designator-less (local) so the comparison is
#: timezone-independent.
_UPLOAD_STAMP_NOW_JS = "new RealDate(2026, 9, 3, 18, 30, 0)"
def _upload_stamp_driver(fn_src: str, expr: str) -> str:
"""Run `expr` under node with the REAL git-sources.js function +
the fake clock (the idiom: the Date patch precedes the call)."""
driver = (
f"{fn_src}\n"
"const RealDate = Date;\n"
f"const NOW = {_UPLOAD_STAMP_NOW_JS};\n"
"globalThis.Date = function (...args) {\n"
" return args.length === 0 ? NOW : new RealDate(...args);\n"
"};\n"
f"{expr}\n"
)
proc = subprocess.run(
["node", "-e", driver], capture_output=True, text=True, timeout=60
)
assert proc.returncode == 0, f"node failed: {proc.stderr}"
return proc.stdout
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_upload_stamp_behavior_fixed_clock() -> None:
"""Phase 154 (task 02): the REAL git-sources.js
``fmtUploadStamp`` under node (fake clock, the fixed now Oct 3
2026 18:30 local — the same clock as test_sync_button.py's
task-01 pin): finished wins over started; started when finished is
missing/null; ``""`` when both are absent (or the status object is
missing — the defensive guard); a garbage ISO → ``""`` with no
fallback (and a corrupt finished_at degrades to the started_at
stamp when a started_at is present — the defensive guard); the A1
date-qualifier's boundary set (same day ``HH:MM`` — the audit's
own ``21:14`` case + the end-of-day edge; yesterday
``M/D, h:MM AM/PM`` with the ``12:05 AM`` edge; last year
``M/D/YYYY, h:MM AM/PM``). The wall clock never appears in an
assertion."""
fn = _gfn(_gjs(), "fmtUploadStamp")
def _lit(status: object) -> str:
if status is None:
return "null"
assert isinstance(status, dict)
parts = []
for k, v in status.items():
if v is None:
parts.append(f"{k}: null")
else:
parts.append(f'{k}: "{v}"')
return "{" + ", ".join(parts) + "}"
# (status, expected) — the expected strings are the pins.
cases = [
(
{"finished_at": "2026-10-03T16:06:00", "started_at": "2026-10-03T16:05:00"},
"16:06", # finished wins over started
),
({"started_at": "2026-10-02T15:14:00"}, "10/2, 3:14 PM"), # no finished → started
(
{"finished_at": None, "started_at": "2025-10-03T21:12:00"},
"10/3/2025, 9:12 PM", # null finished → started, last year
),
({"finished_at": "garbage"}, ""), # unparseable, no fallback
(
{"finished_at": "garbage", "started_at": "2026-10-03T16:06:00"},
"16:06", # corrupt finished degrades to the started_at stamp
),
({"finished_at": None, "started_at": None}, ""), # both absent
({}, ""), # both missing
(None, ""), # the status object itself missing (the guard)
({"finished_at": "2026-10-02T00:05:00"}, "10/2, 12:05 AM"), # 12:05 AM edge
({"finished_at": "2026-10-03T23:59:00"}, "23:59"), # same day, end-of-day
]
driver_expr = (
"const CASES = [" + ", ".join(_lit(s) for s, _ in cases) + "];\n"
"const STAMPS = CASES.map((c) => fmtUploadStamp(c));\n"
"console.log(JSON.stringify(STAMPS));"
)
out = json.loads(_upload_stamp_driver(fn, driver_expr))
assert out == [expected for _, expected in cases], out
+316
View File
@@ -164,8 +164,11 @@ tests/unit/test_frontend_sync_upload.py's node check).
from __future__ import annotations
import asyncio
import json
import logging
import re
import shutil
import subprocess
import threading
import time
from collections.abc import Callable, Iterator, Sequence
@@ -198,6 +201,8 @@ RETIRED_ID = "-".join(("sync", "result")) # built at runtime: 3.12+ folds
# adjacent/"+" constant strings at compile time, which would leave the
# retired name in the .pyc — the phase gate greps even the bytecode.
HAVE_NODE = shutil.which("node") is not None
def _text(path: Path) -> str:
assert path.is_file(), f"missing frontend file: {path}"
@@ -563,6 +568,10 @@ def test_sources_js_terminal_states() -> None:
success = _body(js, "applySyncSuccess")
assert '"Synced"' in success and "fmtSyncTime(status.finished_at)" in success
assert 'settleSyncButton(time ? `Synced ${time}` : "Synced")' in success, (
"the settle string — phase 154: the qualified stamp rides the SAME"
" shape (the qualifier lives inside the formatter)"
)
failure = _body(js, "applySyncFailure")
assert 'settleSyncButton("Sync sources")' in failure # retry-ready
@@ -578,6 +587,313 @@ def test_sources_js_terminal_states() -> None:
time = _body(js, "fmtSyncTime")
assert "getHours()" in time and "getMinutes()" in time, "local HH:MM of finished_at"
assert 'if (Number.isNaN(d.getTime())) return "";' in time, (
"the parse guard is UNCHANGED — unparseable ISO → "" (the bare"
" 'Synced' settle)"
)
# Phase 154 (task 01): the DATE-QUALIFIER (A1 rule — the LOCAL
# calendar dates, the page's clock; the table dates' family).
assert "const now = new Date();" in time, "'now' is the page's clock"
assert (
"d.getFullYear() === now.getFullYear() &&" in time
and "d.getMonth() === now.getMonth()" in time
and "d.getDate() === now.getDate()" in time
), "same local calendar day as now → HH:MM (byte-identical to pre-phase)"
assert "d.getHours() % 12 || 12" in time, (
"12-hour WITHOUT leading zero on the hour (the 12 o'clock edge)"
)
assert '"AM"' in time and '"PM"' in time, "the AM/PM family"
assert "pad(d.getMinutes())" in time, "MM zero-padded in every form"
assert "`${d.getMonth() + 1}/${d.getDate()}`" in time, (
"M/D, no leading zeros (same year, different day)"
)
assert "`${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`" in time, (
"M/D/YYYY across years"
)
# Phase 154 (task 01) — the date-qualifier's boundary set, behavioral
# under node with a FIXED clock. The test body never calls `new
# Date()` for its own timing: the fake clock is installed in the
# driver (global Date patched BEFORE the call — `new Date()` → the
# fixed now, `new Date(iso)` → the real parse), and every ISO is
# designator-less (local) so the comparison is timezone-independent.
# The fixed now is Oct 3, 2026, 18:30 LOCAL.
_STAMP_NOW_JS = "new RealDate(2026, 9, 3, 18, 30, 0)"
def _stamp_driver(fn_src: str, expr: str) -> str:
"""Run `expr` under node with the REAL sources.js function + the
fake clock (the idiom: the Date patch precedes the call)."""
driver = (
f"{fn_src}\n"
"const RealDate = Date;\n"
f"const NOW = {_STAMP_NOW_JS};\n"
"globalThis.Date = function (...args) {\n"
" return args.length === 0 ? NOW : new RealDate(...args);\n"
"};\n"
f"{expr}\n"
)
proc = subprocess.run(
["node", "-e", driver], capture_output=True, text=True, timeout=60
)
assert proc.returncode == 0, f"node failed: {proc.stderr}"
return proc.stdout
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_sources_js_fmt_sync_time_qualifier_boundaries_fixed_clock() -> None:
"""The REAL sources.js ``fmtSyncTime`` under node (fake clock, the
fixed now Oct 3 2026 18:30 local) — the A1 boundary set:
same-day → ``HH:MM`` (byte-identical to the pre-phase output —
the audit's own ``16:06`` case, plus the midnight / end-of-day
edges); yesterday (same year) → ``M/D, h:MM AM/PM`` (no leading
zero on the hour — ``3:14 PM``; the ``12:05 AM`` / ``12:07 PM``
o'clock edges); last-year same M/D → ``M/D/YYYY, h:MM AM/PM``;
garbage / missing ISO → ``""`` (the guard unchanged).
The ``null`` edge (``new Date(null)`` IS the epoch — parseable,
the guard unchanged) is pinned TZ-independently: ``null`` and the
epoch ISO render identically (same instant), in the dated + year
form (1969 ≠ the fixed 2026). The wall clock never appears in an
assertion."""
fn = _body(_text(SOURCES_JS), "fmtSyncTime")
# (iso, expected) — the expected strings are the pins.
cases = [
("2026-10-03T16:06:00", "16:06"), # same day — the audit case
("2026-10-03T00:05:00", "00:05"), # same day, midnight edge
("2026-10-03T23:59:00", "23:59"), # same day, end-of-day edge
("2026-10-02T15:14:00", "10/2, 3:14 PM"), # yesterday, no zero hour
("2026-10-02T00:05:00", "10/2, 12:05 AM"), # yesterday, 12:05 AM edge
("2026-10-02T12:07:00", "10/2, 12:07 PM"), # yesterday, noon edge
("2026-10-02T23:58:00", "10/2, 11:58 PM"), # yesterday, PM minutes
("2025-10-03T21:12:00", "10/3/2025, 9:12 PM"), # last year, same M/D
("2025-01-03T09:12:00", "1/3/2025, 9:12 AM"), # last year, month 1
("garbage", ""), # unparseable → the pre-phase fallback
("", ""), # empty → the pre-phase fallback
("undefined", ""), # missing ISO (the app-restart case) → ""
]
iso_literals = []
for iso, _ in cases:
if iso == "undefined":
iso_literals.append("undefined")
else:
iso_literals.append(f'"{iso}"')
driver_expr = (
"const CASES = [" + ", ".join(iso_literals) + "];\n"
"const STAMPS = CASES.map((c) => fmtSyncTime(c));\n"
"const EPOCH = [fmtSyncTime(null), fmtSyncTime(new RealDate(0).toISOString())];\n"
"console.log(JSON.stringify({ STAMPS, EPOCH }));"
)
out = json.loads(_stamp_driver(fn, driver_expr))
assert out["STAMPS"] == [expected for _, expected in cases], out
assert out["EPOCH"][0] == out["EPOCH"][1], (
"null and the epoch ISO render identically (the same instant —"
" the guard is unchanged: new Date(null) IS the epoch)"
)
assert re.fullmatch(
r"\d{1,2}/\d{1,2}/\d{4}, \d{1,2}:\d{2} (AM|PM)", out["EPOCH"][0]
), "the epoch takes the M/D/YYYY, h:MM AM/PM form (1969 ≠ the fixed year)"
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_sources_js_synced_settle_string_qualifies_the_stamp() -> None:
"""The ``applySyncSuccess`` settle string (the pinned shape
``time ? `Synced ${time}` : "Synced"`` — the qualifier lives
INSIDE the formatter, the shape is unchanged) under node with the
fixed clock: a same-day ``finished_at`` settles as
``Synced 16:06`` (byte-identical to pre-phase), a non-same-day
one settles as ``Synced 10/2, 3:14 PM`` (the date-qualified
badge — the phase's ask), and an unparseable/missing ISO settles
as the bare ``Synced`` (the existing fallback). The ``null`` edge
pins identically to the boundary test (the epoch — the pre-phase
rendered ``Synced 00:00`` there)."""
fn = _body(_text(SOURCES_JS), "fmtSyncTime")
settle = (
"const settle = (iso) => {\n"
" const t = fmtSyncTime(iso);\n"
" return t ? `Synced ${t}` : \"Synced\";\n"
"};\n"
)
iso_literals = [
'"2026-10-03T16:06:00"', # same day
'"2026-10-02T15:14:00"', # yesterday
'"2025-10-03T21:12:00"', # last year
'"garbage"', # unparseable
"undefined", # missing ISO
]
driver_expr = (
settle
+ "const CASES = [" + ", ".join(iso_literals) + "];\n"
+ "const SETTLES = CASES.map(settle);\n"
+ "const EPOCH = [settle(null), settle(new RealDate(0).toISOString())];\n"
+ "console.log(JSON.stringify({ SETTLES, EPOCH }));"
)
out = json.loads(_stamp_driver(fn, driver_expr))
assert out["SETTLES"] == [
"Synced 16:06", # same day — byte-identical to pre-phase
"Synced 10/2, 3:14 PM", # yesterday — date-qualified
"Synced 10/3/2025, 9:12 PM", # last year — date + year
"Synced", # garbage — the bare fallback
"Synced", # missing ISO — the bare fallback
], out
assert out["EPOCH"][0] == out["EPOCH"][1], (
"settle(null) and the epoch ISO settle identically (the guard"
" unchanged)"
)
assert out["EPOCH"][0].startswith("Synced "), (
"the epoch settles with a stamp (new Date(null) is parseable"
" — the pre-phase rendered 'Synced 00:00' there)"
)
# Phase 154 (task 03) — the qualifier's BYTE-IDENTITY: the pre-phase
# fmtSyncTime, pinned VERBATIM from git history (the phase-154
# pre-state — the time-only formatter). Only the function NAME is
# renamed (so the real + the reference run side by side under node);
# the body is the old code byte-for-byte — the old strings (the
# `HH:MM` return + the `""` guard) live in this constant, so the
# byte-identity below is assertable, not assumed. The reference is
# clock-free (it reads no `now` — only the ISO's local time), but
# the same fixed-clock driver (the task-01 boundary set's clock) is
# used, so the real function's same-day comparison runs on the fixed
# now exactly as in the boundary test.
PRE_PHASE_FMT_SYNC_TIME_JS = """
function prePhaseSyncTime(iso) {
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return "";
const pad = (n) => String(n).padStart(2, "0");
return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
"""
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_sources_js_fmt_sync_time_same_day_is_byte_identical_to_pre_phase() -> None:
"""Phase 154 (task 03): the REAL sources.js ``fmtSyncTime`` and
the VERBATIM pre-phase formatter (pinned above) run side by side
under node with the fixed clock (Oct 3 2026 18:30 local — the
task-01 boundary set's clock):
* the pinned reference's own output is asserted against the
verbatim old strings FIRST (a self-check on the constant — a
typo in the pinned function fails here, not silently);
* EVERY same-day ISO renders BYTE-IDENTICALLY to the pre-phase
output (the old ``HH:MM`` — the audit's ``16:06`` case, the
midnight / end-of-day edges), and its settle string is
byte-identical to the pre-phase settle (``"Synced " + HH:MM``
— a same-day badge never moved);
* EVERY unparseable/missing ISO (``garbage`` / ``""`` /
``undefined``) renders byte-identically to the pre-phase
``""`` (the guard unchanged);
* the NON-same-day ISOs (yesterday + last year + the
``null``/epoch edge — the epoch's local time is a different
day AND a different year, so the qualified form applies; its
value is TZ-dependent, so it is pinned by SHAPE: the
``M/D/YYYY, h:MM AM/PM`` family vs the old ``HH:MM``) are the
pinned DIVERGENT set — the qualified form is byte-different
from the pre-phase time-only string (the phase's change lives
exactly there).
The wall clock never appears in an assertion."""
# Source-level self-check: the pinned constant carries the old
# strings verbatim (the `HH:MM` return + the parse guard).
assert (
"return `${pad(d.getHours())}:${pad(d.getMinutes())}`;"
in PRE_PHASE_FMT_SYNC_TIME_JS
), "the old HH:MM return is pinned verbatim"
assert (
'if (Number.isNaN(d.getTime())) return "";' in PRE_PHASE_FMT_SYNC_TIME_JS
), "the old parse guard is pinned verbatim"
fn = _body(_text(SOURCES_JS), "fmtSyncTime")
# (js literal, category) — category: same / fallback / diverge.
cases = [
('"2026-10-03T16:06:00"', "same"), # the audit case
('"2026-10-03T00:05:00"', "same"), # midnight edge
('"2026-10-03T23:59:00"', "same"), # end-of-day edge
('"garbage"', "fallback"),
('""', "fallback"), # empty ISO
("undefined", "fallback"), # missing ISO (the app-restart case)
("null", "diverge"), # the epoch edge — shape-pinned (TZ-dependent)
('"2026-10-02T15:14:00"', "diverge"), # yesterday
('"2026-10-02T00:05:00"', "diverge"),
('"2026-10-02T12:07:00"', "diverge"),
('"2026-10-02T23:58:00"', "diverge"),
('"2025-10-03T21:12:00"', "diverge"), # last year
('"2025-01-03T09:12:00"', "diverge"),
]
# The verbatim old strings (the pre-phase output — pinned here so
# the identity is assertable); the null/epoch value is
# TZ-dependent, so its old string is pinned by shape, not value.
old = {
'"2026-10-03T16:06:00"': "16:06",
'"2026-10-03T00:05:00"': "00:05",
'"2026-10-03T23:59:00"': "23:59",
'"garbage"': "",
'""': "",
"undefined": "",
}
driver_expr = (
"const CASES = [" + ", ".join(lit for lit, _ in cases) + "];\n"
"const PRE = CASES.map((c) => prePhaseSyncTime(c));\n"
"const REAL = CASES.map((c) => fmtSyncTime(c));\n"
"const settle = (f) => (iso) => {\n"
" const t = f(iso);\n"
" return t ? `Synced ${t}` : \"Synced\";\n"
"};\n"
"const PRE_SETTLE = CASES.map((c) => settle(prePhaseSyncTime)(c));\n"
"const REAL_SETTLE = CASES.map((c) => settle(fmtSyncTime)(c));\n"
"console.log(JSON.stringify({ PRE, REAL, PRE_SETTLE, REAL_SETTLE }));"
)
out = json.loads(
_stamp_driver(fn + "\n" + PRE_PHASE_FMT_SYNC_TIME_JS, driver_expr)
)
# 1. the pinned reference reproduces the verbatim old strings
# (self-check on the constant — assertable, not assumed).
for i, (lit, _cat) in enumerate(cases):
if lit in old:
assert out["PRE"][i] == old[lit], (
f"the pre-phase reference for {lit} must be the verbatim old"
f" string {old[lit]!r} — got {out['PRE'][i]!r} (a typo in the"
" pinned constant?"
)
null_i = [lit for lit, _ in cases].index("null")
assert re.fullmatch(r"\d{2}:\d{2}", out["PRE"][null_i]), (
"the null/epoch edge takes the old HH:MM shape on the reference"
" side (the pre-phase is time-only — the value is TZ-dependent,"
" the shape is not)"
)
assert out["PRE_SETTLE"][null_i] == "Synced " + out["PRE"][null_i] # old settle
assert re.fullmatch(
r"\d{1,2}/\d{1,2}/\d{4}, \d{1,2}:\d{2} (AM|PM)", out["REAL"][null_i]
), (
"the null/epoch edge takes the M/D/YYYY, h:MM AM/PM shape on the"
" real side (the epoch is a different day + year — the qualified"
" form; the value is TZ-dependent, the shape is not)"
)
assert out["REAL_SETTLE"][null_i].startswith("Synced ")
# 2. the audit case's settle is byte-identical to pre-phase.
audit_i = [lit for lit, _ in cases].index('"2026-10-03T16:06:00"')
assert out["REAL_SETTLE"][audit_i] == "Synced 16:06", (
"the same-day settle is byte-identical to the pre-phase string"
)
# 3. same-day + fallbacks: byte-identical to the reference, settle
# included; the divergent set: byte-different (the qualified form).
for i, (lit, cat) in enumerate(cases):
if cat == "diverge":
assert out["REAL"][i] != out["PRE"][i], (
f"{lit} must DIFFER from the pre-phase form (the qualified"
f" branch) — got {out['REAL'][i]!r} == {out['PRE'][i]!r}"
)
assert out["REAL_SETTLE"][i] != out["PRE_SETTLE"][i]
else:
assert out["REAL"][i] == out["PRE"][i], (
f"{lit} must be byte-identical to the pre-phase output"
f" (category {cat}) — got {out['REAL'][i]!r} vs"
f" {out['PRE'][i]!r}"
)
assert out["REAL_SETTLE"][i] == out["PRE_SETTLE"][i]
def test_sources_js_failed_error_is_sanitized() -> None: