Files
brain-of-reese/tests/e2e/test_sync_progress_box.py
T
ducoterra a45f744604
Build and Push Containers / build-and-push-app (push) Successful in 2m5s
Build and Push Containers / build-and-push-db (push) Successful in 13s
phase: 194_sync_pending_count
**Phase 194 — final verification pass: all green.**

**Verified (all 5 tasks already complete; nothing re-implemented needed)**
- `app/rag/importer.py`: `_prescan_pending` + `_pending_hash_stmt` (scalar `documents.file_hash`, own session, ONE `asyncio.to_thread` span, REPLACES the stats pre-walk), `on_scan_progress`, `progress` pair = pending position, `0` branch = byte-identical phase-193 walk.
- `app/config.py`/`settings_catalog.py`/`.env.example`: `sync_pending_count=1` + validator; `app/api/sync.py`: `"scan"` phase, `scan_done`/`scan_total` (additive after `files_total`, kept at every terminal), forced/throttled RSS split; `frontend/assets/sources.js`: `fmtSyncPhaseLabel` scan branch first-arm, `fmtSyncLabel` untouched.

**Tests / lint / coverage (exact commands)**
- `uv run pytest --cov=app --cov-report=term-missing` → **4835 passed**, `app/` 99% (7240 stmts / 16 miss), exit 0
- `uv run pytest tests/e2e/test_sync_pending_count.py -v --no-cov` → **8 passed in 150.81s** (phase suite, isolated)
- 14 sync-lineage E2E suites, one pytest process each (`test_sync_remaining_count`, `…_chunk_progress`, `…_progress_box`, `…_peak_memory_visible`, `…_label_mobile`, `…_upload_progress`, `test_archive_upload_sources`, `…_image_progress`, `…_summary_visibility`, `…_status_freshness`, `test_smoke`, `test_fast_path_deferred_content`, `test_sync_session_reset`, `test_sync_git_freeze`) → **all exit 0**
- `uv run ruff check .` → All checks passed; `uv run pyright` → **0 errors**

**Completion criteria** — all met: `N remaining` = predicted change set (E2E arms 1/2/5, first import counts the walk, no-change → no count part); `scan` phase visible + flips to `import`, scan pair reset/kept at terminals (`test_sync_scan_phase.py`); `BOR_SYNC_PENDING_COUNT=0` walk semantics ("7 remaining" arm) + **negative refuses startup naming the field (verified live by importing `app.main`)**; pre-scan off-loop + cancel lands `cancelled` (integration pins); CLI/upload byte-identical (unedited pins green); payload additive (`_PRE_187_STATUS_KEYS | _MEMORY_KEYS | _SCAN_KEYS`); suite green, coverage >90%, lint/types clean.

**Documentation** — 3 fixes: README sync "States" bullet had an **unclosed parenthesis** (pre-existing, 193/194) and the scan-label "total is 0" clause was ambiguous about a `=0` run (it still counts the walk once the import starts); **AGENTS.md rule 17 had a stray `)`** (pre-existing from phase 191). Also verified consistency both directions for all 87 settings ↔ README ↔ `.env.example` ↔ settings catalog, the pages table ↔ `app/main.py` routes, no stale `BOR_` names, and README/AGENTS claims through phase 194.

**Notes/deviations** — no live-infra changes (prod sync knob ships default-on, no deployment touched); no git index/history commands run (all changes left in the working tree for the harness); commit criterion left to the harness as instructed.

**Next pending phase:** none — `.agents/phases/todo/` holds only `194_sync_pending_count`, which is now ready to move to `complete/`.
2026-10-20 06:36:21 -04:00

1333 lines
63 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Phase 149 task 03 / phase 150 task 05 E2E (Playwright, mock-only):
the owner's ask, pinned end to end — while a sync run is in flight,
the **button is the loading spinner** (``is-running`` hides the label
parts; the spinning icon is the whole control) and the bordered text
box directly under the button row (``#sync-progress``) carries the
**EXACT full untruncated live label** — ``Syncing…
<source/relative/path> N remaining · chunk n/m`` — wrapping, never clipping
(the long ``source/relative/path`` that overflows the pill is the
fixture's whole reason to exist). Phase 150 folded the announcer into
the box: it is the **single sync text surface AND the sole aria-live
region** (``role="status" aria-live="polite"``, no ``aria-hidden`` —
the retired announcer span is gone from the DOM), and
at settle it **stays visible** to carry the result line (the
``fmtSyncResult`` counts line, "Sync cancelled" — byte-identical to
the pre-phase announcer's strings), hiding only when its text is empty
(idle, upload settles, failures — the banner + modal carry the error).
The dedicated story suite for ``149_sync_progress_box`` / phase 150
(A16 — one Playwright file per phase, run in isolation). Phase 149's
tasks 01/02 built the box + its wiring + its CSS; this suite is the
behavioral proof through the REAL sync path: a real ``git clone`` of a
local ``file://`` fixture repo, a real import, the slowed mock LLM so
the in-flight state is observable.
Phase 193 (task 02) re-points the file-count part of that label to the
distance to the end — ``N remaining`` with ``N = total - done`` — so the
long file (the FIRST of the three) renders ``2 remaining`` while the
endpoint's ``files_done``/``files_total`` stay the 1-based 1/3 position.
The ``· chunk`` part and the terminal counts line are byte-identical.
Phase 194 (task 04, D5) leaves this suite UNEDITED for the same reason:
its fixture is a fresh import, so the pair's new source (the pre-scan's
predicted change set) and the old one (the walk) are the same three
files, and every ``2 remaining`` frame lands where phase 193 pinned it.
Run in isolation (DB must be up: ``podman compose up -d db``; git on PATH —
a documented environment prerequisite, phase 28):
uv run pytest tests/e2e/test_sync_progress_box.py -v --no-cov
MOCK-ONLY suite: ``E2E_REAL_LLM=1`` is not supported — the gate is the
deterministic mock plus the phase-64 ``tests/e2e/slow_llm.py`` delay proxy
(``SLOW_DELAY_S = "1.5"`` — the proxy delays EVERY LLM call, embeds
included) in front of the mock: each in-flight state outlives the UI's
2 s status poll by ≫ 1×, so every rendered label is observed
deterministically, not as a race.
The suite runs ONE module app (the phase-79 full-inventory pattern — it
binds its own port) over the shared E2E Postgres: the SLOW app with
``BOR_GIT_SOURCES=file://<fixture repo>`` (the env fallback — the autouse
truncate keeps the ``git_sources`` table empty, so the env list IS the
effective source) + its own ``BOR_SOURCES_DIR`` for the checkout.
Timing fixture: a changed sync of the 3-file fixture is ≈ 2 model probes
+ 3 files × (1 content-embed batch + 1 summary chat + 1 summary-chunk
embed) + 1 overview + 7 folder summaries ≈ 19 LLM calls ≈ 30–35 s under
the proxy. The LONG-path file is the FIRST of the three (the sorted walk —
``notes/a-…`` < ``notes/short-…``), so its import span (7–8 embed batches
× 1.5 s + the two summary calls ≈ 15 s) outlives the 2 s poll ~7× — the
full-path + chunk-part windows the assertions observe are ≫ the poll
period (deterministic, not races).
KB fixture — a real git repo (host temp dir via ``tmp_path_factory``; the
app runs on the same host) with EXACTLY THREE markdown files:
* ``notes/a-rather-deeply-nested-folder/structure/for/the/sync/
fixture-document.md`` — the LONG relative path (the clipping trigger the
owner hit: a ``source/relative/path`` that overflows the pre-phase pill
at desktop width), and a multi-chunk body (7 single-paragraph sections
— one chunk apiece at the code-default 2000-char target, so the embed
span is many 1.5 s token-budgeted batches: the wide in-flight window);
* ``notes/short-one.md`` + ``notes/short-two.md`` — short paths, one chunk
each (the sync has several files → the ``(n/3)`` count part and the
per-file chunk restart are exercised too).
The source name is the checkout's directory basename (the ``repo_name``
rule, phase 28): ``progressbox`` — so the in-flight label's file part is
``progressbox/notes/a-rather-deeply-nested-folder/structure/for/the/sync/
fixture-document.md``.
Autouse cleanup (the phase-96 pattern): before each test wait for no
running sync on the app, then truncate the shared registry + KB tables
(chunks/documents/query_log/steering_notes/kb_overview/git_sources/
folder_summaries) — each test runs its own sync of the env-fallback source;
after each test the same truncate. The checkout in ``BOR_SOURCES_DIR``
survives across tests on purpose (the sync's pull is a fast no-op after
the first clone).
Test → observable mapping (Playwright Mapping Rule — the rendered DOM is
the observable; every in-flight snapshot is ONE in-browser evaluation that
verifies the awaited label state and reads the numbers together, so they
can never straddle a 2 s poll tick — the single ``setSyncProgress`` /
``setSyncLabel`` writers update the box + the button's aria-label/title
in one JS task):
1. ``test_idle_page_one_live_region_box_hidden`` — a page load with NO
run in flight (this test runs FIRST — the module app has never run a
sync, so the boot re-attach settles idle): the template pins hold —
the retired announcer span is ABSENT from the DOM (the retirement
is pinned, not implied), the sync view (``#view-rag``) carries
EXACTLY ONE
explicit ``[aria-live]`` element and it is ``#sync-progress``
(``role="status"``, ``aria-live="polite"``, NO ``aria-hidden`` —
the two are mutually exclusive by intent), and the box is hidden +
empty (the anonymous-safe idle state).
2. ``test_running_button_is_spinner_only`` — click **Sync sources**; poll
until ``#sync-progress`` is visible (a run is in flight): ``#sync-btn``
carries ``is-running`` AND the icon ``is-spinning`` (the spinner IS the
button), ``#sync-label`` is NOT visible (the label parts are hidden —
the button is the icon), the button's ``aria-label`` EQUALS the box's
current text (written by the same call — never stale), the box IS the
live region (``role="status"``, ``aria-live="polite"``, NO
``aria-hidden`` — phase 150) and the ONLY explicit ``[aria-live]``
element in the sync view, the button is disabled + ``aria-busy`` (the
pre-phase running mechanics), and the Cancel button is visible (the
phase-134 contract unchanged).
3. ``test_box_carries_the_full_untruncated_label`` — during the
in-flight window of the LONG-path file (poll until the box text is
``Syncing… <full long path> 2 remaining`` — the path string IN FULL, the
pre-phase ellipsis would have truncated it): the box's
``scrollWidth <= clientWidth`` AND the text's ``scrollWidth <=
clientWidth`` (the wrap engaged — nothing is clipped), the box IS the
announcer (``role="status"`` / ``aria-live="polite"`` / no
``aria-hidden`` — phase 150), the button's ``aria-label`` equals the
box's text too, and the box ink on the panel background clears WCAG
2.1 AA (≥ 4.5:1). Then a retry loop over the slow embed window pins
the phase-129 chunk part IN THE BOX (``Syncing… <full long path>
2 remaining · chunk n/m`` — the part is only present while an embed batch
is in flight; the proxy's 1.5 s delay makes the ~15 s window ≫ the
2 s poll).
4. ``test_mobile_box_full_width`` — a fresh page at the house mobile
viewport (390×844 — the ``test_sync_label_mobile.py`` constant): the
same in-flight assertions — the box is FULL WIDTH (its width ≈ the
container's content width minus the container's padding — no override
needed, the block rides the 72rem container), no horizontal clipping
(``scrollWidth <= clientWidth``), the icon-only button keeps the
mobile full-width rule, the label wraps (the full long path is a
substring — the old mobile-stuffing ellipsis is gone).
5. ``test_settled_state_restores_the_pill`` — in flight first (the box is
visible), then let the run settle (the house settle idiom): the box
STAYS VISIBLE and carries the counts line (the pre-phase
``fmtSyncResult`` shape — ``N added`` — byte-identical to what the
retired announcer spoke), the button shows the settle label
``Synced HH:MM`` (the pre-phase byte-identical settle copy) with the
count + chunk parts hidden and empty, ``is-running`` / ``is-spinning``
are gone, and the ``aria-label`` is back to ``"Sync sources"``
(ASSUMPTION A2: the settle label stays on the button — the box
carries the counts line, not a duplicate of the button).
6. ``test_reattach_shows_the_box`` — in flight (the box visible), RELOAD
the page (the admin session cookie persists): the load-time re-attach
(``initSyncButton``) finds the RUNNING sync → the box is VISIBLE with
the current label and the button is icon-only again (the never-stale
contract — a page load mid-run shows the truth, not an idle pill).
7. ``test_settled_cancel_shows_the_result_line`` — in flight, then
CANCELLED (the phase-134 control): the poll's ``cancelled`` branch
settles the box with the neutral result line "Sync cancelled"
(VISIBLE — the phase-150 result-line contract, byte-identical to the
pre-phase announcer's string), the button settles retry-ready
("Sync sources", no busy, no running classes), and the cancel is
never rendered as a failure (no error banner, no modal in the DOM).
8. ``test_settled_failure_hides_the_box`` — a BROKEN git source (a
``file://`` URL with no repository behind it, added as a DB row —
which outranks the env fallback) makes the sync's clone step fail:
the settled failure is the box HIDDEN + empty (the failed terminal
announces nothing in the live region), the error banner AND the
failure modal carry the (sanitized) error (the pre-phase contract,
unchanged), and the button settles retry-ready with the error
styling (the accessible name carries the error).
"""
from __future__ import annotations
import json
import os
import re
import subprocess
import sys
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, ViewportSize, 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,
MOCK_PORT,
SESSION_SECRET,
USE_REAL_LLM,
_wait_http,
)
from e2e.sync_helpers import PEAK_CLAUSE, counts_line
REPO = Path(__file__).resolve().parents[2]
# Phase 79 (task 04, full inventory): the conftest session app owns its
# port in a combined run — this module's app binds its own port
# instead (a same-port second uvicorn dies on bind and would drive the
# wrong server). Env-overridable.
APP_PORT = int(os.environ.get("E2E_APP_PORT_SYNCPROGRESS", "8166"))
APP_URL = f"http://127.0.0.1:{APP_PORT}"
#: The slow-LLM proxy's port (the conftest's mock LLM stays on
#: MOCK_PORT — the proxy fronts it).
SLOW_PORT = int(os.environ.get("E2E_SLOW_LLM_PORT_SYNCPROGRESS", "8908"))
SLOW_URL = f"http://127.0.0.1:{SLOW_PORT}"
#: Per-LLM-request delay on the proxy (the phase-64/98 rationale,
#: module docstring): the proxy delays EVERY call — embeds included —
#: so the in-flight windows outlive the UI's 2 s poll by ≫ 1×.
SLOW_DELAY_S = "1.5"
# --------------------------------------------------------------------------
# Fixture constants (deterministic, token-controlled)
# --------------------------------------------------------------------------
#: The fixture repo's directory name — the source name the import
#: records for it (the ``repo_name`` basename rule, phase 28).
SOURCE = "progressbox"
#: The LONG relative path — the clipping trigger the owner hit (a
#: ``source/relative/path`` that overflows the pre-phase pill even on
#: desktop) — and the two short-path siblings (the sync has several
#: files: the ``(n/3)`` count part + the per-file chunk restart).
LONG_REL = (
"notes/a-rather-deeply-nested-folder/structure/for/the/sync/"
"fixture-document.md"
)
SHORT_RELS = ("notes/short-one.md", "notes/short-two.md")
N_FILES = 3
#: The in-flight label's file part — the status endpoint's
#: ``current_file`` = ``<source>/<rel>`` (the sync's progress hook),
#: verbatim. The long file is the FIRST of the three (the sorted walk —
#: ``notes/a-…`` < ``notes/short-…``), so its phase-193 count part is
#: ``2 remaining`` (N = total - done = 3 - 1).
LONG_PATH = f"{SOURCE}/{LONG_REL}"
#: The long file's shape (module docstring): an H1 preamble (one chunk)
#: + 7 single-paragraph sections (~1500 chars each — one chunk apiece at
#: the code-default 2000-char target) → ~8 chunks → ~8 token-budgeted
#: embed batches × 1.5 s (the wide in-flight window the assertions
#: observe — deterministic, not a race).
N_LONG_SECTIONS = 7
SHORT_MD = (
"# Short fixture {n}\n\nA tiny sync fixture note with a short path "
"({n}).\n"
)
#: The house mobile viewport (the ``test_sync_label_mobile.py``
#: constant — the phone-width leg of the owner's ask).
MOBILE_VIEWPORT: ViewportSize = {"width": 390, "height": 844}
#: "Synced HH:MM" — the local-time last-result label (sources.js's
#: fmtSyncTime), any hour/minute (the test_sync_button.py pattern).
SYNCED_LABEL = re.compile(r"Synced \d{1,2}:\d{2}$")
#: The settled boot label — idle ("Sync sources") or the RE-ATTACHED
#: terminal from a previous test's sync (the module app is shared, its
#: in-memory sync state survives the test boundary, and the boot
#: re-attach renders the last result).
IDLE_OR_SETTLED = re.compile(r"^(Sync sources|Synced \d{1,2}:\d{2})$")
#: Generous settle budgets: a changed sync against the slowed LLM is
#: ≈ 30–35 s; the UI's 2 s poll settles at most one tick after the
#: terminal state lands.
SETTLE_TIMEOUT_MS = 120_000
#: The module's wall budget: five full 3-file syncs against the slowed
#: mock (≈ 30–35 s each) + one cancelled run + one fast clone-failure
#: run (phase 150's result-line tests 7/8) must stay well under this —
#: a breach is a regression canary for the sync pipeline's speed, not a
#: flake tolerance.
WALL_BUDGET_S = 600.0
#: The settle's counts line (sources.js's fmtSyncResult — the first
#: part is always "N added"; the rest are conditional, " · "-joined —
#: the shape the pre-phase announcer spoke, byte-identical) plus the
#: optional phase-187 peak clause — the counts are still anchored, so
#: a mutated counts line still fails this shape.
RESULT_LINE = re.compile(
rf"^\d+ added( · \d+ (updated|unchanged|pruned))*{PEAK_CLAUSE}$"
)
#: A broken git source — the failure trigger (test 8): a ``file://``
#: URL with no repository behind it (the URL shape passes add-time
#: validation — it is the CLONE that fails). Added as a DB row it
#: OUTRANKS the env fallback (effective_sources: any DB row makes
#: ``BOR_GIT_SOURCES`` ignored entirely), so the sync's only source is
#: the unclonable one — the run fails in the clone step, before any
#: import (no partial KB; only the probe's LLM calls run).
BROKEN_REPO_URL = "file:///nonexistent/bor-progressbox-fail-repo"
# ---------------------------------------------------------------------------
# The in-browser snapshot functions (Playwright Mapping Rule — the
# rendered DOM is the observable).
#
# Each returns null while the awaited label state is absent, and an
# ATOMIC snapshot once it is present: the box's text + the button's
# aria-label / classes + the geometry — read in the SAME in-browser
# evaluation that verified the state, so the numbers can never straddle
# a 2 s poll tick (the single setSyncLabel writer updates the box, the
# button, and the announcer together in one JS task — A4: one source,
# two surfaces).
# ---------------------------------------------------------------------------
#: Test 1 — the template pin + the idle re-attach (a page load with NO
#: run in flight): the sync view (``#view-rag``) carries EXACTLY ONE
#: explicit ``[aria-live]`` element and it is ``#sync-progress``
#: (``role="status"``, ``aria-live="polite"``, NO ``aria-hidden`` —
#: an aria-live region that is aria-hidden announces nothing), the box
#: is hidden + empty (the anonymous-safe idle state), and the retired
#: announcer span is ABSENT from the DOM.
# Phase 150: the retired sync-announcer span's id is assembled (never
# spelled out) so the phase gate's project-wide grep for the retired
# name stays clean while the absence pin stays in force.
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.
_JS_LIVE_REGION_SNAPSHOT = """
() => {
const box = document.querySelector('#sync-progress');
const textEl = document.querySelector('#sync-progress-text');
if (!box || !textEl) return null;
const liveEls = document.querySelectorAll('#view-rag [aria-live]');
return {
role: box.getAttribute('role'),
ariaLive: box.getAttribute('aria-live'),
ariaHidden: box.getAttribute('aria-hidden'),
boxHidden: box.hidden,
boxText: textEl.textContent,
resultCount: document.querySelector('#' + ['sync', 'result'].join('-')) ? 1 : 0,
liveCount: liveEls.length,
liveIds: Array.from(liveEls).map(el => el.id),
};
}
"""
#: Tests 2, 5, 6, 7 — ANY in-flight state (a run is in flight): the box is
#: VISIBLE with non-empty text, and the button is in its running state
#: (is-running + the spinning icon + the hidden label parts + the full
#: live label as aria-label + disabled + aria-busy) with the Cancel
#: control visible (the phase-134 contract). The box's live-region
#: attributes are read too (phase 150 — the box IS the announcer:
#: role="status", aria-live="polite", NO aria-hidden) + the sync view's
#: [aria-live] inventory (the one-live-region pin).
_JS_RUNNING_SNAPSHOT = """
() => {
const box = document.querySelector('#sync-progress');
const textEl = document.querySelector('#sync-progress-text');
const btn = document.querySelector('#sync-btn');
const label = document.querySelector('#sync-label');
const icon = btn ? btn.querySelector('.sync-icon') : null;
const cancel = document.querySelector('#sync-cancel-btn');
if (!box || !textEl || !btn || !label || !icon || !cancel) return null;
if (box.hidden || !textEl.textContent) return null;
const cs = (el) => getComputedStyle(el);
const liveEls = document.querySelectorAll('#view-rag [aria-live]');
return {
boxText: textEl.textContent,
role: box.getAttribute('role'),
ariaLive: box.getAttribute('aria-live'),
ariaHidden: box.getAttribute('aria-hidden'),
liveCount: liveEls.length,
liveIds: Array.from(liveEls).map(el => el.id),
ariaLabel: btn.getAttribute('aria-label'),
isRunning: btn.classList.contains('is-running'),
isSpinning: icon.classList.contains('is-spinning'),
labelHidden: cs(label).display === 'none',
cancelVisible: !cancel.hidden && cs(cancel).display !== 'none',
btnDisabled: btn.disabled,
ariaBusy: btn.getAttribute('aria-busy'),
};
}
"""
#: Test 3, wait A — the in-flight window of the LONG-path file: the box
#: is VISIBLE and its text is the FULL label for the long file —
#: "Syncing… <full long path> 2 remaining" (+ the optional phase-129
#: " · chunk n/m" part while an embed batch is in flight — the
#: pre-first-batch frame has none) — the complete
# source/relative/path IN the text (the pre-phase ellipsis would have
#: truncated it). Returns the text + the wrap proof (scrollWidth vs
#: clientWidth of box and text) + the box's live-region attributes
#: (phase 150 — the box IS the announcer) + the aria-label + the
#: computed ink/panel colors (the AA pin).
_JS_LONG_FILE_SNAPSHOT = """
(longPath) => {
const box = document.querySelector('#sync-progress');
const textEl = document.querySelector('#sync-progress-text');
const btn = document.querySelector('#sync-btn');
if (!box || !textEl || !btn || box.hidden) return null;
const t = textEl.textContent;
const prefix = 'Syncing… ' + longPath + ' ';
if (!t.startsWith(prefix)) return null;
const tail = t.slice(prefix.length);
if (!/^2 remaining( · chunk \\d+\\/\\d+)?$/.test(tail)) return null;
const cs = (el) => getComputedStyle(el);
return {
boxText: t,
role: box.getAttribute('role'),
ariaLive: box.getAttribute('aria-live'),
ariaHidden: box.getAttribute('aria-hidden'),
ariaLabel: btn.getAttribute('aria-label'),
boxScrollWidth: box.scrollWidth,
boxClientWidth: box.clientWidth,
textScrollWidth: textEl.scrollWidth,
textClientWidth: textEl.clientWidth,
ink: cs(textEl).color,
bg: cs(box).backgroundColor,
};
}
"""
#: Test 3, wait B — the phase-129 chunk part IN THE BOX during the slow
#: embed window: the box text is "Syncing… <full long path>
#: 2 remaining · chunk n/m" (the part is only present while an in-flight file's embed
#: batch is in flight — the proxy's 1.5 s delay makes the ~15 s
#: long-file window ≫ the 2 s poll, so the retry loop resolves
#: deterministically).
_JS_LONG_FILE_CHUNK_SNAPSHOT = """
(longPath) => {
const box = document.querySelector('#sync-progress');
const textEl = document.querySelector('#sync-progress-text');
if (!box || !textEl || box.hidden) return null;
const t = textEl.textContent;
if (!t.startsWith('Syncing… ' + longPath + ' 2 remaining · chunk ')) return null;
const m = t.match(/· chunk (\\d+)\\/(\\d+)$/);
if (!m) return null;
return {
boxText: t,
chunkN: Number(m[1]),
chunkM: Number(m[2]),
boxScrollWidth: box.scrollWidth,
boxClientWidth: box.clientWidth,
textScrollWidth: textEl.scrollWidth,
textClientWidth: textEl.clientWidth,
};
}
"""
#: Test 4 — the mobile (390×844) in-flight window of the long file: the
#: box text is the full label (the long path IN the text), the box is
#: FULL WIDTH (its bounding-box width ≈ the container's content width —
#: the block rides the 72rem container, no override), no horizontal
#: clipping (scrollWidth <= clientWidth), the icon-only button keeps the
#: mobile full-width rule (its width:100% resolves against its
#: containing block — the .page-head card's content width, NOT the
#: container's), and the label parts are hidden.
_JS_MOBILE_SNAPSHOT = """
(longPath) => {
const box = document.querySelector('#sync-progress');
const textEl = document.querySelector('#sync-progress-text');
const btn = document.querySelector('#sync-btn');
const label = document.querySelector('#sync-label');
const icon = btn ? btn.querySelector('.sync-icon') : null;
const container = document.querySelector('#view-rag .container');
const head = document.querySelector('#view-rag .page-head');
if (!box || !textEl || !btn || !label || !icon || !container || !head) return null;
if (box.hidden) return null;
const t = textEl.textContent;
if (!t.startsWith('Syncing… ' + longPath + ' 2 remaining')) return null;
const cs = (el) => getComputedStyle(el);
const rect = (el) => {
const b = el.getBoundingClientRect();
return { left: b.left, right: b.right, width: b.width, height: b.height };
};
// clientWidth already excludes the border — content = clientWidth
// minus the horizontal padding only.
const contentWidth = (el) => {
const s = cs(el);
return el.clientWidth - parseFloat(s.paddingLeft) - parseFloat(s.paddingRight);
};
return {
boxText: t,
isRunning: btn.classList.contains('is-running'),
isSpinning: icon.classList.contains('is-spinning'),
labelHidden: cs(label).display === 'none',
boxRect: rect(box),
btnRect: rect(btn),
containerContentWidth: contentWidth(container),
headContentWidth: contentWidth(head),
boxScrollWidth: box.scrollWidth,
boxClientWidth: box.clientWidth,
};
}
"""
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
def _git(cwd: Path, *args: str) -> None:
"""Run git in *cwd*; a non-zero exit fails the fixture loudly."""
proc = subprocess.run(["git", *args], cwd=cwd, capture_output=True, text=True)
if proc.returncode != 0:
raise AssertionError(f"git {' '.join(args)} failed: {proc.stderr.strip()}")
def _commit(repo: Path, message: str) -> None:
"""A fixture commit (never signed — the house ``commit.gpgsign``
flag, the test_sync_button.py idiom)."""
_git(
repo,
"-c", "user.email=e@x", "-c", "user.name=t",
"-c", "commit.gpgsign=false",
"commit", "-qm", message,
)
def _long_md() -> str:
"""The long-path fixture doc (see the module docstring): an H1
preamble (one chunk) + 7 single-paragraph sections (~1500 chars
each — one chunk apiece at the code-default 2000-char target — no
randomness, so the chunk count is stable run to run)."""
parts = ["# Long-path fixture document\n"]
parts.append(
"Preamble for the deeply nested sync fixture document. This "
"paragraph sits under the H1 and chunks on its own.\n"
)
for k in range(1, N_LONG_SECTIONS + 1):
parts.append(f"\n## Nesting {k}\n")
base = (
f"Nesting paragraph {k} of the long-path sync fixture carries "
"deterministic filler for the chunker: alpha bravo charlie "
"delta echo foxtrot golf hotel india juliet kilo lima "
)
parts.append((base * 30)[:1500] + "\n\n")
return "".join(parts)
@pytest.fixture(scope="module")
def progressbox_repo(tmp_path_factory: pytest.TempPathFactory) -> Path:
"""A real git repo the sync must clone over ``file://`` (see the
module docstring): the long-path doc (the clipping trigger,
multi-chunk body) + the two short-path docs. The directory NAME is
the source name (the ``repo_name`` basename rule, phase 28).
``tmp_path`` is function-scoped while the module-scoped app fixture
needs the repo for the module's lifetime, so it is built under
``tmp_path_factory`` via real ``git`` subprocess calls."""
root = tmp_path_factory.mktemp("bor_syncprogress")
repo = root / SOURCE
long_file = repo / LONG_REL
long_file.parent.mkdir(parents=True)
long_file.write_text(_long_md(), encoding="utf-8")
for i, rel in enumerate(SHORT_RELS, start=1):
(repo / rel).write_text(SHORT_MD.format(n=i), encoding="utf-8")
_git(repo, "init", "-q")
_git(repo, "add", "-A")
_commit(repo, "one")
assert (repo / ".git").is_dir()
assert len(list(repo.rglob("*.md"))) == N_FILES
return repo
def _app_env(
base_url: str, repo: Path, tmp_path_factory: pytest.TempPathFactory
) -> dict[str, str]:
"""The per-module app env (the conftest pattern): the ``base_url``
picks the LLM (the slow proxy in front of the mock), and
``BOR_GIT_SOURCES`` is this suite's own ``file://`` fixture repo —
the sync's SUBJECT (the env fallback: the autouse truncate keeps the
``git_sources`` table empty, so the env list is the effective
source). The env var also overrides the owner's local (gitignored)
``.env`` ``BOR_GIT_SOURCES`` (process env ranks above the file)."""
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 base_url
)
# Mock-calibrated gate values (conftest pattern): the floors ride
# WITH the 0.30 threshold — a floor above it fails the startup
# validator (phase 112/113).
env["BOR_RELEVANCE_THRESHOLD"] = "0.30"
env["BOR_LEXICAL_SUPPORT_FLOOR"] = "0.15"
env["BOR_SOURCE_USEFULNESS_FLOOR"] = "0.15"
# Phase 67: instant retry waits + the code-default budget (the
# conftest leak-guard pattern).
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 sync's subject (see the docstring) + its own checkout dir.
env["BOR_GIT_SOURCES"] = f"file://{repo}"
env["BOR_SOURCES_DIR"] = str(tmp_path_factory.mktemp("bor_checkouts"))
# Leak guards (conftest pattern): an operator's local (gitignored)
# .env cannot leak corpus-specific settings into the app under test.
env["BOR_DOCS_REPO"] = ""
env["BOR_SUGGESTIONS"] = json.dumps(
_Settings.model_fields["suggestions"].default
)
env["BOR_INPUT_PLACEHOLDER"] = _Settings.model_fields["input_placeholder"].default
env["BOR_FOOTER_TEXT"] = _Settings.model_fields["footer_text"].default
return env
@pytest.fixture(scope="module")
def slow_llm(mock_llm: int) -> Iterator[int]:
"""The delay-injecting reverse proxy in front of the mock LLM
(``tests/e2e/slow_llm.py``) — the timing fixture for the whole
suite: it delays EVERY LLM call, embeds included, so the in-flight
windows outlive the UI's 2 s poll by ≫ 1× (see ``SLOW_DELAY_S``)."""
env = dict(os.environ)
env.pop("DEBUGPY", None)
env["SLOW_LLM_DELAY_S"] = SLOW_DELAY_S
env["E2E_MOCK_PORT"] = str(MOCK_PORT)
proc = subprocess.Popen(
[sys.executable, "-m", "uvicorn", "tests.e2e.slow_llm:app",
"--host", "127.0.0.1", "--port", str(SLOW_PORT), "--log-level", "warning"],
cwd=REPO,
env=env,
)
try:
_wait_http(f"{SLOW_URL}/v1/models")
yield SLOW_PORT
finally:
proc.terminate()
try:
proc.wait(timeout=10)
except subprocess.TimeoutExpired:
proc.kill()
@pytest.fixture(scope="module")
def app_server(
mock_llm: int,
slow_llm: int,
progressbox_repo: Path,
tmp_path_factory: pytest.TempPathFactory,
) -> Iterator[str]:
"""The SLOW app under test (the whole suite):
``BOR_LLM_BASE_URL`` is the slow proxy in front of the mock (the
timing fixture), ``BOR_GIT_SOURCES`` the ``file://`` fixture repo
(the sync's subject — see the module docstring), and
``BOR_SOURCES_DIR`` its own checkout dir. ``progressbox_repo`` is a
dependency only for the fixture ordering (the repo exists before
the first test)."""
env = _app_env(f"{SLOW_URL}/v1", progressbox_repo, tmp_path_factory)
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")
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 (the E2E isolation pattern): the E2E suites
share one Postgres, so a leftover git_sources row would outrank
this app's ``BOR_GIT_SOURCES`` env fallback (effective_sources
prefers the DB list) and a leftover document would pollute the file
counts the sync pins exactly."""
with SessionLocal() as db:
db.execute(
text(
"TRUNCATE chunks, documents, query_log, steering_notes, "
"kb_overview, git_sources, folder_summaries"
)
)
db.commit() # without the commit the TRUNCATE rolls back (the house pattern)
def _wait_sync_settled(base_url: str, timeout_s: float = 180.0) -> None:
"""No background sync may leak across tests (the run state lives in
the app's memory, and a still-running run would keep importing into
the NEXT test's truncated KB): wait for the app's sync status to be
non-running BEFORE the truncate. Own admin session (the endpoint is
admin-only) — usually a no-op: the tests settle only after their
run's terminal state."""
with httpx.Client(base_url=base_url, timeout=5.0) as client:
r = client.post("/api/login", json={"password": ADMIN_PASSWORD})
assert r.status_code == 204, r.text
deadline = time.monotonic() + timeout_s
while time.monotonic() < deadline:
r = client.get("/api/sync/status")
assert r.status_code == 200, r.text
body = r.json()
if body["state"] != "running":
# Diagnostic: a FAILED terminal between tests is a real
# anomaly for this suite (every sync here must succeed)
# — surface the error text so a cascade is diagnosable
# instead of showing up as a later test's blocked click.
if body["state"] == "failed":
print(f"sync progress box: terminal FAILED at a test "
f"boundary: {body.get('error')}")
return
time.sleep(0.2)
raise AssertionError(f"a sync was still running at a test boundary: {base_url}")
@pytest.fixture(autouse=True)
def _clean(app_url: str, db_ready: None) -> Iterator[None]:
"""Per-test KB isolation (the phase-96 cleanup pattern): the
shared E2E Postgres is truncated before AND after every test —
each test runs its own sync of the env-fallback source, so the KB
rows of the fixture source are exactly this test's own. The
pre-wait keeps a leaked run from the previous test (the module app
is shared) from racing the truncate."""
_wait_sync_settled(app_url)
_truncate_all()
yield
_truncate_all()
@pytest.fixture(scope="module", autouse=True)
def _wall_time_guard() -> Iterator[None]:
"""The module's wall-time budget: the whole module — app boot +
FIVE full 3-file syncs against the slowed mock LLM — stays under
``WALL_BUDGET_S``. A breach is a regression canary (the sync
pipeline slowed), not a flake tolerance."""
start = time.monotonic()
yield
elapsed = time.monotonic() - start
assert elapsed < WALL_BUDGET_S, (
f"the module took {elapsed:.1f}s — over the {WALL_BUDGET_S:.0f}s wall "
"budget (the five small-fixture syncs against the slowed mock LLM "
"are a speed canary)"
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _parse_rgb(css_color: str) -> tuple[int, int, int]:
"""A computed ``rgb(r, g, b)`` / ``rgba(…)`` color → the triple."""
m = re.match(r"rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)", css_color)
assert m, f"not a computable rgb color: {css_color!r}"
return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
def _wcag_luminance(rgb: tuple[float, float, float]) -> float:
"""WCAG 2.1 relative luminance (the same math the theme suites run)."""
def lin(c: float) -> float:
s = c / 255
return s / 12.92 if s <= 0.03928 else ((s + 0.055) / 1.055) ** 2.4
r, g, b = rgb
return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b)
def _wcag_ratio(
fg: tuple[float, float, float], bg: tuple[float, float, float]
) -> float:
l1, l2 = _wcag_luminance(fg), _wcag_luminance(bg)
return (max(l1, l2) + 0.05) / (min(l1, l2) + 0.05)
def _start_sync(page: Page, app_url: str) -> None:
"""Land the admin on the sources view, wait for the settled boot
label (the phase-64 pattern — also proves the boot re-attach
completed, so no later re-render can clobber the running state),
then click **Sync sources** and pin the click's immediate 202
state: the button IS the spinner (disabled, aria-busy,
``is-running``, the spinning icon) and the box is revealed with
the bare label (phase 149 — the running detail rides the box).
The module app is SHARED across tests: a prior test's terminal
state survives in its memory, and the boot re-attach renders it
(asynchronously). A prior FAILED terminal re-opens the failure
modal — its backdrop intercepts clicks until closed — so the
deterministic layer reads the state from the status endpoint first
(the test_sync_cancel pattern) and closes the modal before the
click (the owner's next step — never stale)."""
login(page, app_url) # lands on /sources.html (the button's home)
# The deterministic layer: the app's in-memory run state — a prior
# test's terminal, never "running" (the autouse cleanup waits a
# leaked run out before every test).
with httpx.Client(base_url=app_url, timeout=30.0) as client:
r = client.post("/api/login", json={"password": ADMIN_PASSWORD})
assert r.status_code == 204, r.text
r = client.get("/api/sync/status")
assert r.status_code == 200, r.text
state = r.json()["state"]
assert state != "running", f"a sync leaked across a test boundary: {state}"
btn = page.locator("#sync-btn")
expect(btn).to_be_visible(timeout=30_000)
expect(page.locator("#sync-label")).to_have_text(IDLE_OR_SETTLED)
expect(btn).to_be_enabled()
# A boot re-attach of a prior FAILED terminal opens the failure
# modal (the backdrop intercepts clicks until closed) — close it
# before the click.
if state == "failed":
expect(page.locator(".sync-modal-backdrop")).to_be_visible(timeout=15_000)
page.locator(".sync-modal-close").click()
expect(page.locator(".sync-modal-backdrop")).to_be_hidden()
btn.click()
# The click's immediate state (the 202 moment — the bare prefix,
# the phase-64 contract): the button is the loading spinner and
# the box carries the bare label (phase 149/150 — the running
# detail rides the single box surface).
expect(btn).to_be_disabled()
expect(btn).to_have_attribute("aria-busy", "true")
expect(btn).to_have_class(re.compile(r"\bis-running\b"))
expect(btn.locator(".sync-icon")).to_have_class(re.compile(r"\bis-spinning\b"))
expect(page.locator("#sync-progress")).to_have_text("Syncing…")
expect(page.locator("#sync-error-banner")).to_be_hidden()
def _wait_terminal(app_url: str, timeout_s: float = 120.0) -> dict[str, Any]:
"""Poll the status endpoint (own admin session) until the UI-
triggered run reaches a terminal state — the source of truth for
the settle (the UI's 2 s poll renders it at most one tick later).
A FAILED terminal fails the test with the terminal body (the
``detail`` / error text) — never an opaque label timeout."""
with httpx.Client(base_url=app_url, timeout=30.0) as client:
r = client.post("/api/login", json={"password": ADMIN_PASSWORD})
assert r.status_code == 204, r.text
deadline = time.monotonic() + timeout_s
body: dict[str, Any] = {}
while time.monotonic() < deadline:
r = client.get("/api/sync/status")
assert r.status_code == 200, r.text
body = r.json()
if body["state"] in ("success", "failed"):
assert body["state"] == "success", body
return body
time.sleep(0.2)
raise AssertionError(f"the run never reached a terminal state: {body}")
def _wait_state(
app_url: str, states: set[str], timeout_s: float = 120.0
) -> dict[str, Any]:
"""Poll the status endpoint (own admin session) until the run's
state is in *states* — the deterministic layer for the non-success
terminals (the cancelled / failed settles, tests 7/8). Returns the
terminal body for the test to pin; a timeout fails with the last
body (never an opaque wait)."""
with httpx.Client(base_url=app_url, timeout=30.0) as client:
r = client.post("/api/login", json={"password": ADMIN_PASSWORD})
assert r.status_code == 204, r.text
deadline = time.monotonic() + timeout_s
body: dict[str, Any] = {}
while time.monotonic() < deadline:
r = client.get("/api/sync/status")
assert r.status_code == 200, r.text
body = r.json()
if body["state"] in states:
return body
time.sleep(0.2)
raise AssertionError(f"the run never reached {sorted(states)}: {body}")
def _settle(page: Page, app_url: str) -> None:
"""The phase-32 success settle, phase-150 shape: the run reaches a
SUCCESS terminal at the endpoint, then the button shows "Synced
HH:MM" (re-enabled, the running state fully cleared) and the box
STAYS VISIBLE carrying the counts line (the fmtSyncResult shape
the pre-phase announcer spoke — phase 150 A1: the box hides only
when its text is empty), with no error."""
_wait_terminal(app_url)
expect(page.locator("#sync-label")).to_have_text(SYNCED_LABEL, timeout=30_000)
btn = page.locator("#sync-btn")
expect(btn).to_be_enabled()
expect(btn).not_to_have_attribute("aria-busy")
expect(btn).not_to_have_class(re.compile(r"\bis-running\b"))
expect(btn.locator(".sync-icon")).not_to_have_class(re.compile(r"\bis-spinning\b"))
expect(page.locator("#sync-progress")).to_be_visible()
expect(page.locator("#sync-progress-text")).to_have_text(RESULT_LINE)
expect(page.locator("#sync-error-banner")).to_be_hidden()
# ---------------------------------------------------------------------------
# 1. Idle: the template pins — one live region (the box), the retired
# announcer absent, the box hidden + empty (a page load with no run)
# ---------------------------------------------------------------------------
def test_idle_page_one_live_region_box_hidden(
page: Page, app_url: str, db_ready: None
) -> None:
"""A page load with NO run in flight (the idle re-attach — this
test runs FIRST, so the module app has never started a sync and
the boot re-attach settles the true idle): the phase-150 template
pins hold end to end — the retired announcer span is ABSENT from
the DOM (the retirement is pinned, not implied), the sync
view (``#view-rag``) carries EXACTLY ONE explicit ``[aria-live]``
element and it is ``#sync-progress`` (``role="status"``,
``aria-live="polite"``, NO ``aria-hidden`` — the two attributes are
mutually exclusive by intent), and the box is hidden + empty (the
anonymous-safe idle state — the box hides only when its text is
empty)."""
login(page, app_url) # lands on /sources.html (the box's home)
snap = page.evaluate(_JS_LIVE_REGION_SNAPSHOT)
assert snap is not None, "the box is missing from the template"
# The retirement: the pre-150 announcer span is gone from the DOM.
assert snap["resultCount"] == 0, snap
assert page.locator("#" + RETIRED_ID).count() == 0
# ONE live region in the sync area — and it is the box.
assert snap["liveCount"] == 1, snap
assert snap["liveIds"] == ["sync-progress"], snap
assert snap["role"] == "status", snap
assert snap["ariaLive"] == "polite", snap
assert snap["ariaHidden"] is None, snap
# Idle: the box is hidden + empty, the button is the idle pill.
expect(page.locator("#sync-progress")).to_be_hidden()
expect(page.locator("#sync-progress-text")).to_have_text("")
expect(page.locator("#sync-label")).to_have_text("Sync sources")
expect(page.locator("#sync-btn")).to_be_enabled()
expect(page.locator("#sync-error-banner")).to_be_hidden()
# ---------------------------------------------------------------------------
# 2. In flight: the button is the spinner only (no visible label text),
# the aria-label carries the full live label, and Cancel rides along
# ---------------------------------------------------------------------------
def test_running_button_is_spinner_only(
page: Page, app_url: str, db_ready: None
) -> None:
"""A fresh admin page on ``/sources.html`` clicks **Sync sources**;
the page's OWN 2 s poll keeps the run in flight long enough (the
slow proxy guarantees a ≫ 30 s window) that the in-flight state is
observed deterministically: ``#sync-btn`` carries ``is-running``
AND the icon ``is-spinning`` (the spinner IS the loading state),
``#sync-label`` is NOT visible (the label parts are hidden — the
button is the icon, at any viewport), the button's ``aria-label``
EQUALS the box's current text (written by the same single-writer
call — never stale), the box IS the live region (``role="status"``,
``aria-live="polite"``, NO ``aria-hidden`` — phase 150 folded the
retired announcer in) and the ONLY explicit ``[aria-live]`` element
in the sync view (one live region, no double announce), the running
mechanics (disabled + ``aria-busy``) are preserved, and the Cancel
button is visible (the phase-134 contract unchanged)."""
_start_sync(page, app_url)
# Poll (the generous timeout — the slow proxy's in-flight window is
# ~30 s) until a run is in flight: the box is visible + non-empty.
snap = page.wait_for_function(
_JS_RUNNING_SNAPSHOT, timeout=SETTLE_TIMEOUT_MS
).json_value()
# The button is the loading spinner (the owner's ask).
assert snap["isRunning"] is True, snap
assert snap["isSpinning"] is True, snap
# The label parts are hidden — the button is the icon (no visible
# label text).
assert snap["labelHidden"] is True, snap
assert page.locator("#sync-label").is_visible() is False
assert page.locator("#sync-progress").is_visible()
# The icon-only button keeps an accessible name — the FULL live
# label, equal to the box's text (the same writer call wrote both).
assert snap["boxText"], snap
assert snap["ariaLabel"] == snap["boxText"], snap
# Phase 150: the box IS the SOLE live region — role="status",
# aria-live="polite", NO aria-hidden (an aria-live region that is
# aria-hidden announces nothing), and the only [aria-live] element
# in the sync view.
assert snap["role"] == "status", snap
assert snap["ariaLive"] == "polite", snap
assert snap["ariaHidden"] is None, snap
assert snap["liveCount"] == 1, snap
assert snap["liveIds"] == ["sync-progress"], snap
# The pre-phase running mechanics are preserved.
assert snap["btnDisabled"] is True, snap
assert snap["ariaBusy"] == "true", snap
# The Cancel control rides the running state (phase 134 — unchanged).
assert snap["cancelVisible"] is True, snap
_settle(page, app_url)
# ---------------------------------------------------------------------------
# 3. The box carries the EXACT full untruncated label (long path
# included — wrapped, never clipped), the box IS the announcer,
# and the chunk part appears in the box during the embed window
# ---------------------------------------------------------------------------
def test_box_carries_the_full_untruncated_label(
page: Page, app_url: str, db_ready: None
) -> None:
"""During the in-flight window of the LONG-path file (poll until the
box text is ``Syncing… <full long path> 2 remaining`` — the slow proxy
makes the ~15 s span deterministic): the box's text contains the
FULL label shape with the complete ``source/relative/path`` as a
substring (the pre-phase ellipsis would have truncated it — the
wrap means nothing is dropped), the box did NOT overflow
horizontally (``scrollWidth <= clientWidth`` — the wrap engaged)
neither as a panel nor in its text, the box IS the announcer
(``role="status"`` / ``aria-live="polite"`` / no ``aria-hidden``
— phase 150), the button's ``aria-label`` equals the box's text,
and the box ink on the panel clears WCAG 2.1 AA (≥ 4.5:1). Then a
retry loop over the slow embed window pins the phase-129 chunk part
IN THE BOX (``· chunk n/m`` — the part is only present while an
embed batch is in flight; the proxy's 1.5 s delay makes the window
wide)."""
_start_sync(page, app_url)
# Wait A — the in-flight window of the long file (any frame of it —
# the pre-first-batch frame has no chunk part yet).
snap = page.wait_for_function(
_JS_LONG_FILE_SNAPSHOT, arg=LONG_PATH, timeout=SETTLE_TIMEOUT_MS
).json_value()
# The FULL label shape — the complete long path IN the text (the
# clipping trigger, untruncated).
expected_prefix = f"Syncing… {LONG_PATH} 2 remaining"
assert snap["boxText"].startswith(expected_prefix), snap["boxText"]
assert LONG_PATH in snap["boxText"], snap["boxText"]
# The wrap engaged — no horizontal overflow (the whole point of the
# box: the path WRAPS, it is never clipped).
assert snap["boxScrollWidth"] <= snap["boxClientWidth"], snap
assert snap["textScrollWidth"] <= snap["textClientWidth"], snap
# Phase 150: the box IS the announcer — role="status",
# aria-live="polite", NO aria-hidden (the retired span is gone) —
# and the icon-only button's accessible name carries the same full
# string.
assert snap["role"] == "status", snap
assert snap["ariaLive"] == "polite", snap
assert snap["ariaHidden"] is None, snap
assert snap["ariaLabel"] == snap["boxText"], snap
# WCAG 2.1 AA: the box's mono ink on its panel background ≥ 4.5:1
# (the theme's pinned identity pair — observed, not assumed).
ratio = _wcag_ratio(_parse_rgb(snap["ink"]), _parse_rgb(snap["bg"]))
assert ratio >= 4.5, f"{snap['ink']} on {snap['bg']}: {ratio:.2f}:1 < 4.5:1 (AA)"
# Wait B — the phase-129 chunk part IN THE BOX (the retry loop over
# the slow embed window — the part is only present while an embed
# batch is in flight; the ~15 s long-file window outlives the 2 s
# poll by ≫ 1×).
snap2 = page.wait_for_function(
_JS_LONG_FILE_CHUNK_SNAPSHOT, arg=LONG_PATH, timeout=SETTLE_TIMEOUT_MS
).json_value()
assert snap2["chunkN"] >= 1, snap2
assert snap2["chunkM"] >= 1, snap2
# The longer label (with the chunk part) still wraps — no clipping.
assert snap2["boxScrollWidth"] <= snap2["boxClientWidth"], snap2
assert snap2["textScrollWidth"] <= snap2["textClientWidth"], snap2
_settle(page, app_url)
# ---------------------------------------------------------------------------
# 4. Mobile (390×844): the box is full-width, the button is icon-only
# (the mobile full-width rule), and the label wraps
# ---------------------------------------------------------------------------
def test_mobile_box_full_width(page: Page, app_url: str, db_ready: None) -> None:
"""A fresh page at the house MOBILE viewport (390×844) clicks
**Sync sources**; during the in-flight window of the LONG-path
file: the box is FULL WIDTH (its bounding-box width ≈ the
container's content width — a block in the same container rides
the 100%-width phone shell with no override), no horizontal
clipping (``scrollWidth <= clientWidth``), the button is
icon-only (``is-running`` + the spinning icon, the label parts
hidden) and keeps the mobile full-width rule, and the label wraps
(the full long path is a substring — the old mobile-stuffing
ellipsis is gone)."""
page.set_viewport_size(MOBILE_VIEWPORT)
_start_sync(page, app_url)
snap = page.wait_for_function(
_JS_MOBILE_SNAPSHOT, arg=LONG_PATH, timeout=SETTLE_TIMEOUT_MS
).json_value()
# The label wraps — the full long path is IN the box's text (the
# old mobile-stuffing ellipsis is gone).
assert snap["boxText"].startswith(
f"Syncing… {LONG_PATH} 2 remaining"
), snap["boxText"]
assert LONG_PATH in snap["boxText"], snap["boxText"]
# The wrap engaged — no horizontal clipping at the phone width.
assert snap["boxScrollWidth"] <= snap["boxClientWidth"], snap
# The box is FULL WIDTH: its width ≈ the container's content width
# (the 2px slack covers float rounding only).
assert abs(snap["boxRect"]["width"] - snap["containerContentWidth"]) <= 2, snap
# …and it is inside the viewport (the owner's clipped-detail
# report, directly negated).
assert snap["boxRect"]["left"] >= 0, snap["boxRect"]
assert snap["boxRect"]["right"] <= MOBILE_VIEWPORT["width"] + 1, snap["boxRect"]
# The button is icon-only (the spinner) and keeps the mobile
# full-width rule: its width:100% resolves against its containing
# block — the .page-head card's content width (the card's padding
# + border inset it from the container).
assert snap["isRunning"] is True, snap
assert snap["isSpinning"] is True, snap
assert snap["labelHidden"] is True, snap
assert abs(snap["btnRect"]["width"] - snap["headContentWidth"]) <= 2, snap
_settle(page, app_url)
# ---------------------------------------------------------------------------
# 5. Settle: the box STAYS visible with the counts line (phase 150),
# the "Synced HH:MM" pill is restored byte-identical, and the counts
# line is unchanged
# ---------------------------------------------------------------------------
def test_settled_state_restores_the_pill(
page: Page, app_url: str, db_ready: None
) -> None:
"""A sync is triggered and observed IN FLIGHT (the box is visible —
so the settle below is a real transition, not a no-op), then let to
settle (the house settle idiom): the box STAYS VISIBLE and carries
the counts line — the pre-phase ``fmtSyncResult`` shape
(``N added`` — the 3 fixture files), byte-identical to what the
retired announcer spoke (phase 150 A1: the box holds the result
line; it hides only when its text is empty) —, the button shows
the settle label ``Synced HH:MM`` (the pre-phase byte-identical
settle copy, ASSUMPTION A2: it stays on the button, not the box)
with the label parts restored — the count + chunk parts hidden and
empty —, ``is-running`` / ``is-spinning`` are gone, and the
``aria-label`` is back to ``"Sync sources"`` (the ship value)."""
_start_sync(page, app_url)
# In flight first (the box is visible + the button is the spinner).
page.wait_for_function(_JS_RUNNING_SNAPSHOT, timeout=SETTLE_TIMEOUT_MS).json_value()
# The settle (the house idiom): the terminal lands at the endpoint,
# then the UI's 2 s poll renders the phase-32/150 terminal shape.
_wait_terminal(app_url)
expect(page.locator("#sync-label")).to_have_text(SYNCED_LABEL, timeout=30_000)
# The pill is restored (the pre-phase byte-identical settle copy).
expect(page.locator("#sync-label")).to_be_visible()
btn = page.locator("#sync-btn")
expect(btn).to_be_enabled()
expect(btn).not_to_have_attribute("aria-busy")
expect(btn).to_have_attribute("aria-label", "Sync sources")
expect(btn).not_to_have_class(re.compile(r"\bis-running\b"))
expect(btn.locator(".sync-icon")).not_to_have_class(re.compile(r"\bis-spinning\b"))
# Phase 150: the box STAYS visible at settle — it carries the
# counts line (the fmtSyncResult shape the pre-phase announcer
# spoke, byte-identical).
expect(page.locator("#sync-progress")).to_be_visible()
expect(page.locator("#sync-progress-text")).to_have_text(counts_line(f"{N_FILES} added"))
# The label parts are restored — the settle label carries no count
# or chunk part.
count = page.locator(".sync-label-count")
expect(count).to_be_hidden()
expect(count).to_have_text("")
chunk = page.locator(".sync-label-chunk")
expect(chunk).to_be_hidden()
expect(chunk).to_have_text("")
expect(page.locator("#sync-error-banner")).to_be_hidden()
# ---------------------------------------------------------------------------
# 6. A page load mid-run re-attaches with the box visible (never stale)
# ---------------------------------------------------------------------------
def test_reattach_shows_the_box(page: Page, app_url: str, db_ready: None) -> None:
"""A sync is triggered and observed IN FLIGHT (the box is visible),
then the page is RELOADED (the admin session cookie persists): the
load-time re-attach (``initSyncButton``) finds the RUNNING sync →
the box is VISIBLE with the current label and the button is
icon-only again (``is-running`` + the spinning icon, the label
parts hidden, the ``aria-label`` equal to the box's text, the
Cancel control visible) — the never-stale contract: a page load
mid-run shows the truth, not an idle pill."""
_start_sync(page, app_url)
# In flight first (the pre-reload state: the box is visible).
page.wait_for_function(_JS_RUNNING_SNAPSHOT, timeout=SETTLE_TIMEOUT_MS).json_value()
# Reload mid-run — the run keeps going on the server; the admin
# session (the cookie) survives the reload.
page.reload(wait_until="domcontentloaded")
# The re-attach: the box is visible with the CURRENT label and the
# button is the spinner again (any in-flight state — the run is
# ~30 s and the reload lands within a few seconds of the start).
snap = page.wait_for_function(
_JS_RUNNING_SNAPSHOT, timeout=SETTLE_TIMEOUT_MS
).json_value()
assert snap["isRunning"] is True, snap
assert snap["isSpinning"] is True, snap
assert snap["labelHidden"] is True, snap
assert snap["boxText"], snap
assert snap["ariaLabel"] == snap["boxText"], snap
assert snap["cancelVisible"] is True, snap
_settle(page, app_url)
# ---------------------------------------------------------------------------
# 7. Settled cancel: the box carries the neutral result line
# "Sync cancelled" (no banner, no modal)
# ---------------------------------------------------------------------------
def test_settled_cancel_shows_the_result_line(
page: Page, app_url: str, db_ready: None
) -> None:
"""A sync is triggered and observed IN FLIGHT (the box is visible
with the running label), then CANCELLED (the phase-134 control):
the poll's ``cancelled`` branch settles the box with the neutral
result line "Sync cancelled" — VISIBLE (the phase-150 result-line
contract: the box stays visible to carry it, byte-identical to the
pre-phase announcer's string) — the button settles retry-ready
("Sync sources", no busy, no running classes), and the cancel is
never rendered as a failure (no error banner, no modal in the
DOM)."""
_start_sync(page, app_url)
# In flight first (the box is visible + the Cancel control is
# revealed).
snap = page.wait_for_function(
_JS_RUNNING_SNAPSHOT, timeout=SETTLE_TIMEOUT_MS
).json_value()
assert snap["cancelVisible"] is True, snap
# The owner's click: the control disables IMMEDIATELY (the
# requestCancel never-stale contract). The box is NOT blanked at
# cancel-request time (phase 150 — the run is still in flight; the
# live label stays up until the poll's cancelled branch settles
# it).
cancel = page.locator("#sync-cancel-btn")
cancel.click()
expect(cancel).to_be_disabled()
# The deterministic layer: the cancelled terminal (error null — a
# cancel is intent, not failure).
body = _wait_state(app_url, {"cancelled"}, timeout_s=60.0)
assert body["state"] == "cancelled", body
assert body["error"] is None, body
# The UI settle: the box stays VISIBLE with the neutral result
# line (byte-identical to the pre-phase announcer's string)…
expect(page.locator("#sync-progress")).to_be_visible(timeout=30_000)
expect(page.locator("#sync-progress-text")).to_have_text(
counts_line("Sync cancelled"), timeout=30_000
)
# …the button settles retry-ready…
expect(page.locator("#sync-label")).to_have_text("Sync sources", timeout=30_000)
btn = page.locator("#sync-btn")
expect(btn).to_be_enabled()
expect(btn).not_to_have_attribute("aria-busy")
expect(btn).not_to_have_class(re.compile(r"\bis-running\b"))
expect(btn.locator(".sync-icon")).not_to_have_class(re.compile(r"\bis-spinning\b"))
# …and the cancel is never a failure (no banner, no modal in the
# DOM — the backdrop is created on demand, so count 0 is the
# absence proof).
expect(page.locator("#sync-error-banner")).to_be_hidden()
assert page.locator(".sync-modal-backdrop").count() == 0
# ---------------------------------------------------------------------------
# 8. Settled failure: the box hides + empties; the banner + modal carry
# the error (the pre-phase contract, unchanged)
# ---------------------------------------------------------------------------
def test_settled_failure_hides_the_box(
page: Page, app_url: str, db_ready: None
) -> None:
"""A BROKEN git source (a ``file://`` URL with no repository behind
it) is added as a DB row — which OUTRANKS the env fallback
(effective_sources: any DB row makes ``BOR_GIT_SOURCES`` ignored
entirely), so the sync's only source is the unclonable one — and
the run fails in the clone step (before any import). The settled
failure (phase 150): the box is HIDDEN + empty (the failed
terminal announces nothing in the live region — the error has its
own surfaces), the error banner AND the failure modal carry the
(sanitized) error (the pre-phase contract, unchanged), and the
button settles retry-ready with the error styling (the accessible
name carries the error — never an icon-only mystery)."""
# The failure trigger (own admin session — the endpoint is
# admin-only, like the sync endpoints).
with httpx.Client(base_url=app_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={"url": BROKEN_REPO_URL})
assert r.status_code == 201, r.text
_start_sync(page, app_url)
# The deterministic layer: the failed terminal (the clone step —
# the probe passed: the LLM is the healthy slow proxy).
body = _wait_state(app_url, {"failed"}, timeout_s=120.0)
assert body["state"] == "failed", body
assert body["error"] and "git clone" in body["error"], body
# The error surfaces in the banner AND the failure modal (the
# pre-phase contract, unchanged)…
expect(page.locator("#sync-error-banner")).to_be_visible(timeout=30_000)
expect(page.locator("#sync-error-text")).to_contain_text("git clone")
expect(page.locator(".sync-modal-backdrop")).to_be_visible(timeout=30_000)
expect(page.locator("#sync-modal-error")).to_contain_text("git clone")
# …and the box hides, empty (the failed terminal announces nothing
# — the banner + modal carry the error).
expect(page.locator("#sync-progress")).to_be_hidden()
expect(page.locator("#sync-progress-text")).to_have_text("")
# The button settles retry-ready with the error styling…
expect(page.locator("#sync-label")).to_have_text("Sync sources", timeout=30_000)
btn = page.locator("#sync-btn")
expect(btn).to_be_enabled()
expect(btn).not_to_have_attribute("aria-busy")
expect(btn).not_to_have_class(re.compile(r"\bis-running\b"))
expect(btn).to_have_class(re.compile(r"\bis-error\b"))