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/`.
This commit is contained in:
@@ -67,7 +67,12 @@ tiny chunk). The sorted walk processes ``big.md`` FIRST, so the label's
|
||||
phase-193 count part reads ``1 remaining`` during the big span and is
|
||||
HIDDEN during the small one (the final file: N = total - done = 0 — the
|
||||
endpoint's ``files_done``/``files_total`` stay the 1-based 1/2 then 2/2
|
||||
position; only the LABEL counts down) — and the small file's single
|
||||
position; only the LABEL counts down). Phase 194 moves where that pair
|
||||
comes FROM — the pre-scan's PREDICTED change set instead of the walk —
|
||||
and changes nothing here, because this fixture is a FRESH import: both
|
||||
files are pending, so the pending position IS the walk position (see
|
||||
``tests/e2e/test_sync_remaining_count.py`` for the arm where they
|
||||
diverge) — and the small file's single
|
||||
chunk is the proof that the
|
||||
chunk position RESTARTS per file (the big file ends at its last
|
||||
batch's first index, the small file reports ``(1, 1)``).
|
||||
|
||||
@@ -231,12 +231,19 @@ PRE_187_STATUS_KEYS = frozenset(
|
||||
)
|
||||
MEMORY_KEYS = frozenset({"rss_mb", "rss_peak_mb"})
|
||||
|
||||
#: Phase 194 (task 02): the two additive PRE-SCAN position keys — the next
|
||||
#: link of the same additive chain (the payload is exactly the pre-187 set
|
||||
#: plus the memory pair plus the scan pair).
|
||||
SCAN_KEYS = frozenset({"scan_done", "scan_total"})
|
||||
|
||||
#: The pre-phase integer count slots (their types are part of the
|
||||
#: unchanged-shape pin in test 2 — never a float, never null).
|
||||
_COUNT_KEYS = frozenset(
|
||||
{
|
||||
"files_done",
|
||||
"files_total",
|
||||
"scan_done",
|
||||
"scan_total",
|
||||
"current_chunk",
|
||||
"chunks_total",
|
||||
"images_done",
|
||||
@@ -643,8 +650,9 @@ def test_status_payload_reports_rss(page: Page, app_url: str, db_ready: None) ->
|
||||
running = _wait_sync_state(page, app_url, {"running"})
|
||||
assert running["state"] == "running", running
|
||||
# The additive contract on the wire: exactly the pre-phase keys plus
|
||||
# the memory pair — nothing renamed, nothing dropped.
|
||||
assert set(running) == PRE_187_STATUS_KEYS | MEMORY_KEYS, set(running)
|
||||
# the memory pair plus the phase-194 scan pair — nothing renamed,
|
||||
# nothing dropped.
|
||||
assert set(running) == PRE_187_STATUS_KEYS | MEMORY_KEYS | SCAN_KEYS, set(running)
|
||||
for key, value in running.items():
|
||||
if key in MEMORY_KEYS:
|
||||
# The documented type: int | null (null is "no reading").
|
||||
@@ -674,7 +682,7 @@ def test_status_payload_reports_rss(page: Page, app_url: str, db_ready: None) ->
|
||||
# --- after the terminal ---------------------------------------------
|
||||
terminal = _wait_sync_state(page, app_url, {"success", "failed", "cancelled"})
|
||||
assert terminal["state"] == "success", terminal
|
||||
assert set(terminal) == PRE_187_STATUS_KEYS | MEMORY_KEYS, set(terminal)
|
||||
assert set(terminal) == PRE_187_STATUS_KEYS | MEMORY_KEYS | SCAN_KEYS, set(terminal)
|
||||
current, peak = terminal["rss_mb"], terminal["rss_peak_mb"]
|
||||
if MEMORY_KNOWABLE:
|
||||
assert peak is not None and isinstance(peak, int), terminal
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,10 @@ 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):
|
||||
|
||||
@@ -11,20 +11,46 @@ position *in* the whole; the owner reads the distance *to the end*.
|
||||
Phase 193 changes exactly ONE label part: ``fmtSyncLabel``'s count is
|
||||
now ``N remaining`` with ``N = total − done``.
|
||||
|
||||
**Server untouched.** No endpoint, no payload field, no poll changed:
|
||||
``GET /api/sync/status`` still answers with the phase-64 key set
|
||||
(``current_file`` / ``files_done`` / ``files_total`` — the 1-based
|
||||
position of the in-flight file over the pre-walk count), and the suite
|
||||
pins exactly that (test 1's cross-layer invariant): the payload still
|
||||
carries the ``(done, total)`` position while the label speaks the
|
||||
subtraction of the very same pair. The change is one string built by
|
||||
``frontend/assets/sources.js``.
|
||||
**Phase 194 (task 04, D5) retargets what the pair COUNTS — the label
|
||||
builder stays byte-identical.** Phase 193 itself touched no server
|
||||
code; phase 194 did, and this is the suite whose pins move with it.
|
||||
``GET /api/sync/status`` no longer answers with the bare phase-64 key
|
||||
set: it is that set plus the phase-187 memory pair plus the phase-194
|
||||
scan pair (``scan_done`` / ``scan_total`` — the PRE-SCAN's own walk
|
||||
position), and ``phase`` gained the ``"scan"`` stage between the null
|
||||
prelude and ``"import"`` (the machine the page renders as
|
||||
``Scanning… (n/total)``). More importantly the ``(done, total)`` pair
|
||||
the label subtracts is no longer the WALK position: on a progress
|
||||
caller (the UI sync is the only one) it is the position in the
|
||||
importer's pre-scan PREDICTED change set — ``files_total`` the number
|
||||
of files that will actually be re-indexed, ``files_done`` the 1-based
|
||||
position in that set. ``N remaining`` therefore counts the WORK, which
|
||||
is the fix for the owner's prod report of 2026-10-19 ("1216 remaining"
|
||||
on a ~10,000-file KB with ~10 files to sync). The phase-193 wording —
|
||||
the 1-based position across EVERY importable file of the stats
|
||||
pre-walk — is exactly what ``BOR_SYNC_PENDING_COUNT=0`` restores, and
|
||||
nothing below asserts it except as that rollback's history.
|
||||
|
||||
**A1 (owner-confirmed):** ``N remaining`` shows while ``N ≥ 1`` and is
|
||||
HIDDEN at the final file (``N == 0``) — the main part names that file;
|
||||
``total === 0`` (the pre-walk, the phase-64 contract) also renders no
|
||||
count part. **A2 (owner-confirmed):** the upload job shares the ONE
|
||||
builder, so it shares the shape (``Importing <file> N remaining``).
|
||||
What survives the semantic shift UNEDITED is a property of this
|
||||
suite's own fixture: every arm starts from a TRUNCATED KB, so the
|
||||
prediction and the walk coincide (all ``N_FILES`` files are pending),
|
||||
``files_total`` stays ``N_FILES`` and the ``1 … N−1`` count frames land
|
||||
in the same slots phase 193 pinned — test 1's cross-layer invariant is
|
||||
untouched arithmetic. The case where the two DIVERGE — a re-sync — is
|
||||
test 4, and the phase's own suite
|
||||
(``tests/e2e/test_sync_pending_count.py``, task 05) carries the K-of-N
|
||||
incremental arm.
|
||||
|
||||
**A1 (owner-confirmed, phase 193):** ``N remaining`` shows while
|
||||
``N ≥ 1`` and is HIDDEN at the final file (``N == 0``) — the main part
|
||||
names that file; ``total === 0`` renders no count part either — and
|
||||
since phase 194 there are TWO things that make it ``0``: the scan has
|
||||
not reported a denominator yet (the null prelude / the ``0`` rollback,
|
||||
test 2) and a run whose predicted change set is EMPTY (test 4).
|
||||
**A2 (owner-confirmed):** the upload job shares the ONE builder, so it
|
||||
shares the shape (``Importing <file> N remaining``) — and it gains
|
||||
NOTHING from phase 194: the upload job runs no pre-scan and its
|
||||
endpoint carries no scan fields (phase 90 A2), so test 3 is UNEDITED.
|
||||
|
||||
Fixture: a real git repo (the phase-28/32/107 ``file://`` transport-true
|
||||
convention — ``BOR_GIT_SOURCES=file://<fixture repo>``, so the env
|
||||
@@ -59,22 +85,44 @@ Arms (the phase's three, in test order):
|
||||
label carries a count part at all while the main part still names
|
||||
that file; no ``Syncing…`` label anywhere carries the pre-phase
|
||||
``(n/m)`` tail; the ``(n/m)`` shape survives ONLY in the summaries
|
||||
phase (A1's exception) and the overview label still paints; the
|
||||
terminal result line is byte-identical to the phase-64 counts line
|
||||
(the ``counts_line`` helper); and the endpoint's own tick series
|
||||
still reports the untouched ``(done, total)`` position whose
|
||||
subtraction equals every rendered ``N``.
|
||||
phase (A1's exception) and, since phase 194, in the scan phase's own
|
||||
``Scanning… (n/total)`` (never on a ``Syncing…`` label, and no scan
|
||||
label ever follows an import one); the overview label still paints;
|
||||
the terminal result line is byte-identical to the phase-64 counts
|
||||
line (the ``counts_line`` helper); and the endpoint's own tick
|
||||
series still reports the ``(done, total)`` pair whose subtraction
|
||||
equals every rendered ``N``. Phase 194 adds the machine's own
|
||||
evidence to that same tick series
|
||||
(``_assert_phase_machine_tick_series``): every body is exactly the
|
||||
pre-phase key set plus the memory pair plus the scan pair, the null
|
||||
prelude reports the scan pair as ``0/0``, a tick caught INSIDE the
|
||||
scan names no file and carries the full scan denominator, no scan
|
||||
tick follows a tick that already counted files, and the terminal
|
||||
KEEPS both pairs beside each other (the phase-64 keep-final-values
|
||||
convention) with ``scan_total == files_total == N_FILES`` on this
|
||||
fresh KB — the parity that lets the phase-193 frames stand. The scan
|
||||
window itself is five tiny files (single-digit milliseconds), so a
|
||||
100 ms tick is not guaranteed to land inside it: the deterministic
|
||||
``Scanning… (n/total)`` frames are test 2's, and the choreographed
|
||||
server-side sequence lives in
|
||||
``tests/integration/test_sync_scan_phase.py``.
|
||||
2. ``test_prewalk_and_final_file_labels_have_no_count_part`` — **(c)
|
||||
the pre-walk edge**, pinned on the exact payload the guard reads:
|
||||
the page's own 2 s poll fed ``phase: null`` / ``files_total: 0``
|
||||
renders the bare ``Syncing…`` (no count part, the count span hidden,
|
||||
the title equal), the first file renders ``N − 1 remaining``, a
|
||||
the zero-total edges**, pinned on the exact payload the guards read:
|
||||
the page's own 2 s poll fed a ``phase: "scan"`` body renders
|
||||
``Scanning… (n/total)`` (count span visible, title equal, never a
|
||||
file and never ``N remaining``), the same stage with
|
||||
``scan_total: 0`` renders the bare ``Scanning…`` (the empty-KB
|
||||
guard), a ``phase: null`` / ``files_total: 0`` prelude renders the
|
||||
bare ``Syncing…`` (no count part, the count span hidden, the title
|
||||
equal), the first file renders ``N − 1 remaining``, a
|
||||
mid-run tick renders ``1 remaining`` *beside* ``· chunk 2/5``, the
|
||||
final-file tick (``done == total``) renders the file with the count
|
||||
part GONE and never brings it back, and the success terminal settles
|
||||
to the untouched counts line. The served bodies use the status
|
||||
endpoint's unchanged key set — the client's own decision points
|
||||
(``total > 0``, ``total − done > 0``) are what is under test.
|
||||
to the untouched counts line. The served bodies carry the status
|
||||
endpoint's key set — the phase-64 fields plus the memory and scan
|
||||
pairs, the stubs pinned against the same constant test 1 reads —
|
||||
because the client's own decision points (``total > 0``,
|
||||
``total − done > 0``, ``scan_total > 0``) are what is under test.
|
||||
3. ``test_upload_label_shares_the_count_down_builder`` — **(b) the
|
||||
upload arm (A2)**: the upload branch of the same two-job tree shares
|
||||
the builder, so a running upload status that carries counts renders
|
||||
@@ -88,12 +136,30 @@ Arms (the phase's three, in test order):
|
||||
upload label can ever regress to ``(n/m)``. The upload branch never
|
||||
receives a status object (its endpoint has no phase / chunk / image
|
||||
fields — phase 129 / 152), so the chunk part coexists in the SYNC
|
||||
arms only (test 1 and 2); that is asserted here too.
|
||||
arms only (test 1 and 2); that is asserted here too. UNEDITED by
|
||||
phase 194 — the upload job runs no pre-scan and its endpoint gains
|
||||
no scan keys.
|
||||
4. ``test_resync_of_an_unchanged_repo_reports_zero_work`` — **the
|
||||
phase-194 re-sync arm**, the run phase 193's fixture never had (the
|
||||
owner's prod case in its mildest form): sync this same repo TWICE
|
||||
and the second run predicts NOTHING, so its ``(done, total)`` pair
|
||||
is ``0/0`` on every tick — the A1 no-count-part exception driven by
|
||||
an EMPTY pending set instead of by a missing denominator — while the
|
||||
scan pair reports the whole KB walked (``scan_done == scan_total ==
|
||||
N_FILES``, kept at the terminal). That divergence (5 scanned, 0 to
|
||||
re-index) is the phase in one line, and it is exactly the shape that
|
||||
used to render "``N`` remaining" for files nobody would re-index.
|
||||
The terminal counts line stays the byte-identical
|
||||
``0 added · N unchanged``. A no-change run makes no LLM call, so its
|
||||
import window is milliseconds and it renders no file label: the wire
|
||||
pair (``0/0``) plus test 2's ``total === 0`` guard pin is the honest
|
||||
form of "no count part" here.
|
||||
|
||||
Test → phase-task mapping (Playwright Mapping Rule, task 03 arms a/b/c):
|
||||
1 → (a) + the terminal-line pin + the untouched-payload pin,
|
||||
2 → (c) + the A1 hidden-at-final-file edge (+ (a)'s arithmetic on exact
|
||||
inputs), 3 → (b).
|
||||
Test → phase-task mapping (Playwright Mapping Rule, task 03 arms a/b/c
|
||||
+ task 04's D5 retarget): 1 → (a) + the terminal-line pin + the
|
||||
additive-payload + phase-machine tick-series pins,
|
||||
2 → (c) + the A1 hidden-at-final-file edge + the ``Scanning…`` frames
|
||||
(+ (a)'s arithmetic on exact inputs), 3 → (b), 4 → the D5 re-sync arm.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -148,10 +214,11 @@ SOURCES_URL = "/sources.html"
|
||||
#: its own (the module docstring's determinism note).
|
||||
SLOW_DELAY_S = "1.5"
|
||||
|
||||
#: The module's wall-time budget (app boot + one full 5-file sync + the
|
||||
#: two served-status arms). A breach is a speed canary, not flake
|
||||
#: tolerance (the phase-64 convention).
|
||||
WALL_BUDGET_S = 300.0
|
||||
#: The module's wall-time budget (app boot + two full 5-file syncs — the
|
||||
#: phase-194 re-sync arm runs the fixture's sync twice — plus the two
|
||||
#: served-status arms). A breach is a speed canary, not flake tolerance
|
||||
#: (the phase-64 convention).
|
||||
WALL_BUDGET_S = 480.0
|
||||
|
||||
#: The sync's subject: a git repo whose DIRECTORY NAME is the source
|
||||
#: name (the ``repo_name`` basename rule, phase 28). Deliberately NOT
|
||||
@@ -196,11 +263,52 @@ CHUNK_OVERLAP = int(Settings.model_fields["chunk_overlap_chars"].default)
|
||||
IMPORT_LABEL_RE = re.compile(
|
||||
r"^Syncing… \S+(?: [0-9]+ remaining)?(?: · chunk \d+/\d+)?$"
|
||||
)
|
||||
#: The scan-phase label grammar phase 194 (task 03, D4) adds: the bare
|
||||
#: ``Scanning…`` (no denominator yet — the empty KB and the scan's own
|
||||
#: first frame) or it plus the ``(n/total)`` count part. NEVER a file
|
||||
#: (``current_file`` is null through the phase) and never ``N
|
||||
#: remaining`` — the walk position is honest behind the word "Scanning",
|
||||
#: never behind "Syncing" (TODO L6).
|
||||
SCAN_LABEL_RE = re.compile(r"^Scanning…(?: \(\d+/\d+\))?$")
|
||||
#: The pre-phase shape phase 193 removed from this part — no ``Syncing…``
|
||||
#: label may ever carry a ``(n/m)`` tail again.
|
||||
#: label may ever carry a ``(n/m)`` tail again. Phase 194 leaves that
|
||||
#: true: the ``(n/m)`` shape is legal ONLY on a ``Scanning…`` or a
|
||||
#: ``Summarizing folders…`` label (and no label starting ``Syncing…``
|
||||
#: matches either).
|
||||
OLD_FRACTION_RE = re.compile(r"\(\d+/\d+\)")
|
||||
SYNCED_LABEL = re.compile(r"^Synced \d{1,2}:\d{2}$")
|
||||
|
||||
#: The FULL ``GET /api/sync/status`` key set this suite's stubs mirror and
|
||||
#: every real tick is pinned against — the phase-64/98/129/152 fields,
|
||||
#: the phase-187 memory pair and the phase-194 SCAN pair (``scan_done`` /
|
||||
#: ``scan_total``). The additive chain, pinned as ONE constant so a future
|
||||
#: key has to be added here (and to the stub below) instead of drifting
|
||||
#: away from the client's view of the payload.
|
||||
EXPECTED_STATUS_KEYS = frozenset(
|
||||
{
|
||||
"state",
|
||||
"started_at",
|
||||
"finished_at",
|
||||
"current_file",
|
||||
"files_done",
|
||||
"files_total",
|
||||
"scan_done",
|
||||
"scan_total",
|
||||
"phase",
|
||||
"current_summary",
|
||||
"summaries_done",
|
||||
"summaries_total",
|
||||
"current_chunk",
|
||||
"chunks_total",
|
||||
"images_done",
|
||||
"images_total",
|
||||
"detail",
|
||||
"error",
|
||||
"rss_mb",
|
||||
"rss_peak_mb",
|
||||
}
|
||||
)
|
||||
|
||||
SETTLE_TIMEOUT_MS = 180_000
|
||||
|
||||
|
||||
@@ -527,17 +635,55 @@ class _TickRecorder:
|
||||
"""The deterministic layer: a daemon thread tight-polling (~100 ms)
|
||||
the SAME status endpoint the UI's 2 s poll reads, with its OWN admin
|
||||
session (the Playwright sync API is not thread-safe, so the thread
|
||||
never touches the page). It starts nothing — it only watches."""
|
||||
never touches the page). It starts nothing — it only watches.
|
||||
|
||||
def __init__(self, base_url: str, path: str) -> None:
|
||||
``wait_for_running`` is the phase-194 re-sync arm's need: a recorder
|
||||
started while a PREVIOUS run's terminal is still on the endpoint must
|
||||
not treat that body as its own. A body belongs to THIS run once the
|
||||
recorder has seen it go ``running``, or — the belt-and-braces half,
|
||||
for a run faster than the 100 ms cadence — once its own
|
||||
``started_at`` post-dates the recorder's start (the
|
||||
``test_fast_path_deferred_content.py`` idiom). Without the flag every
|
||||
body counts, which is what the module's first arm wants (there is no
|
||||
previous run to confuse it with).
|
||||
"""
|
||||
|
||||
def __init__(self, base_url: str, path: str, *, wait_for_running: bool = False) -> None:
|
||||
self._url = f"{base_url}{path}"
|
||||
self._login_url = f"{base_url}/api/login"
|
||||
self._wait_for_running = wait_for_running
|
||||
self._saw_running = not wait_for_running
|
||||
self._started_after = 0.0
|
||||
self._ticks: list[dict[str, Any]] = []
|
||||
self._terminal: dict[str, Any] | None = None
|
||||
self._stop = threading.Event()
|
||||
self._thread: threading.Thread | None = None
|
||||
|
||||
@staticmethod
|
||||
def _epoch(body: dict[str, Any]) -> float | None:
|
||||
"""The body's ``started_at`` as epoch seconds (``None`` when no
|
||||
run ever started, or the stamp is unreadable)."""
|
||||
raw = body.get("started_at")
|
||||
if not raw:
|
||||
return None
|
||||
try:
|
||||
return datetime.fromisoformat(str(raw)).timestamp()
|
||||
except ValueError: # pragma: no cover — defensive, an odd stamp
|
||||
return None
|
||||
|
||||
def _is_ours(self, body: dict[str, Any]) -> bool:
|
||||
"""Does this body belong to the run this recorder was started for?"""
|
||||
if body["state"] == "running":
|
||||
self._saw_running = True
|
||||
if self._saw_running:
|
||||
return True
|
||||
started = self._epoch(body)
|
||||
return started is not None and started >= self._started_after - 1.0
|
||||
|
||||
def start(self) -> None:
|
||||
if self._wait_for_running:
|
||||
self._started_after = time.time()
|
||||
|
||||
def run() -> None:
|
||||
with httpx.Client(timeout=5.0) as client:
|
||||
client.post(self._login_url, json={"password": ADMIN_PASSWORD})
|
||||
@@ -546,6 +692,10 @@ class _TickRecorder:
|
||||
r = client.get(self._url)
|
||||
if r.status_code == 200:
|
||||
body = r.json()
|
||||
if not self._is_ours(body):
|
||||
# A previous run's terminal — not ours.
|
||||
self._stop.wait(0.02)
|
||||
continue
|
||||
self._ticks.append(body)
|
||||
if body["state"] in ("success", "failed", "cancelled"):
|
||||
self._terminal = body
|
||||
@@ -575,6 +725,13 @@ class _TickRecorder:
|
||||
def running_ticks(self) -> list[dict[str, Any]]:
|
||||
return [t for t in self._ticks if t["state"] == "running"]
|
||||
|
||||
@property
|
||||
def ticks(self) -> list[dict[str, Any]]:
|
||||
"""EVERY body this recorder saw, the terminal included (the
|
||||
phase-194 phase-machine pin reads the whole series, not just the
|
||||
running frames)."""
|
||||
return list(self._ticks)
|
||||
|
||||
|
||||
def _serve_statuses(page: Page, stages: dict[str, Any]) -> None:
|
||||
"""Route the two job-status endpoints to the CURRENT stage body (the
|
||||
@@ -605,10 +762,12 @@ def _serve_statuses(page: Page, stages: dict[str, Any]) -> None:
|
||||
|
||||
|
||||
def _sync_status(**overrides: Any) -> dict[str, Any]:
|
||||
"""One ``GET /api/sync/status`` body with the endpoint's UNCHANGED
|
||||
key set (the phase-64/98/129/152/187 fields the client reads) — the
|
||||
served stubs are faithful to the payload shape; only the values are
|
||||
the test's."""
|
||||
"""One ``GET /api/sync/status`` body with the endpoint's CURRENT key
|
||||
set (the phase-64/98/129/152/187 fields the client reads plus the
|
||||
phase-194 scan pair — the additive chain ``EXPECTED_STATUS_KEYS``
|
||||
pins) — the served stubs are faithful to the payload shape; only the
|
||||
values are the test's. The equality against that constant is asserted
|
||||
below so a stub can never drift from the real endpoint unnoticed."""
|
||||
body: dict[str, Any] = {
|
||||
"state": "idle",
|
||||
"started_at": None,
|
||||
@@ -616,6 +775,11 @@ def _sync_status(**overrides: Any) -> dict[str, Any]:
|
||||
"current_file": None,
|
||||
"files_done": 0,
|
||||
"files_total": 0,
|
||||
# Phase 194 (task 02): the pre-scan's position, additive after
|
||||
# files_total and 0/0 until the scan reports (the degenerate and
|
||||
# BOR_SYNC_PENDING_COUNT=0 edges both render no count part).
|
||||
"scan_done": 0,
|
||||
"scan_total": 0,
|
||||
"phase": None,
|
||||
"current_summary": None,
|
||||
"summaries_done": 0,
|
||||
@@ -629,6 +793,7 @@ def _sync_status(**overrides: Any) -> dict[str, Any]:
|
||||
"rss_mb": None,
|
||||
"rss_peak_mb": None,
|
||||
}
|
||||
assert set(body) == EXPECTED_STATUS_KEYS, set(body) ^ EXPECTED_STATUS_KEYS
|
||||
body.update(overrides)
|
||||
return body
|
||||
|
||||
@@ -648,6 +813,85 @@ def _upload_status(**overrides: Any) -> dict[str, Any]:
|
||||
return body
|
||||
|
||||
|
||||
def _assert_phase_machine_tick_series(
|
||||
ticks: list[dict[str, Any]],
|
||||
terminal: dict[str, Any],
|
||||
*,
|
||||
scan_total: int,
|
||||
files_total: int,
|
||||
prelude_zero: bool = True,
|
||||
) -> None:
|
||||
"""The phase-194 machine, read off the status endpoint the UI polls.
|
||||
|
||||
Every claim here is deterministic on a real run, which is why none of
|
||||
them depends on a tick happening to land inside the scan (a five-file
|
||||
scan is milliseconds — see the module docstring):
|
||||
|
||||
* the payload is ADDITIVE — exactly :data:`EXPECTED_STATUS_KEYS` on
|
||||
every body, idle/running/terminal alike;
|
||||
* the null prelude reports the scan pair as ``0/0`` (the run has not
|
||||
reached ``import_sources`` yet — the scan is what sets it);
|
||||
* a tick caught inside ``phase == "scan"`` names NO file, carries the
|
||||
full ``scan_total`` denominator and a position that only climbs;
|
||||
* the machine is ONE-DIRECTIONAL — no scan tick follows a tick that
|
||||
already counted files (``files_total > 0`` means the first
|
||||
``progress`` call flipped it, and the phase never flips back);
|
||||
* the terminal CLEARS ``phase`` and KEEPS both pairs side by side
|
||||
(the phase-64 keep-final-values convention);
|
||||
* ``files_total`` is never larger than the scan's denominator on a
|
||||
run whose KB did not change mid-run — the count and its denominator
|
||||
stay on the SAME predicted set.
|
||||
"""
|
||||
for tick in ticks:
|
||||
assert set(tick) == EXPECTED_STATUS_KEYS, (
|
||||
f"the status payload drifted from the additive key set: "
|
||||
f"{sorted(set(tick) ^ EXPECTED_STATUS_KEYS)}"
|
||||
)
|
||||
if terminal not in ticks:
|
||||
assert set(terminal) == EXPECTED_STATUS_KEYS, terminal
|
||||
|
||||
if prelude_zero:
|
||||
for tick in ticks:
|
||||
if tick["state"] == "running" and tick["phase"] is None:
|
||||
# The prelude (the model-check + clone/pull span): the
|
||||
# scan has not started, so it has reported nothing. The
|
||||
# success terminal sets state BEFORE clearing phase, so
|
||||
# no terminal body can reach this arm.
|
||||
assert (tick["scan_done"], tick["scan_total"]) == (0, 0), (
|
||||
f"the null prelude already carried scan counts: {tick}"
|
||||
)
|
||||
|
||||
scan_ticks = [t for t in ticks if t["phase"] == "scan"]
|
||||
for tick in scan_ticks:
|
||||
assert tick["current_file"] is None, (
|
||||
f"the scan named a file — it counts, it does not read: {tick}"
|
||||
)
|
||||
assert tick["scan_total"] == scan_total, tick
|
||||
assert 0 < tick["scan_done"] <= scan_total, tick
|
||||
positions = [t["scan_done"] for t in scan_ticks]
|
||||
assert positions == sorted(positions), (
|
||||
f"the scan position went backwards across ticks: {positions}"
|
||||
)
|
||||
counted = [i for i, t in enumerate(ticks) if t["files_total"] > 0]
|
||||
if scan_ticks and counted:
|
||||
last_scan = max(i for i, t in enumerate(ticks) if t["phase"] == "scan")
|
||||
assert last_scan < counted[0], (
|
||||
"a scan frame followed a frame that already counted files — "
|
||||
"the phase machine ran backwards"
|
||||
)
|
||||
|
||||
assert terminal["phase"] is None, terminal
|
||||
assert terminal["scan_total"] == scan_total, terminal
|
||||
assert terminal["scan_done"] == scan_total, (
|
||||
f"the terminal did not keep the completed scan: {terminal}"
|
||||
)
|
||||
assert terminal["files_total"] == files_total, terminal
|
||||
assert terminal["files_total"] <= terminal["scan_total"], (
|
||||
"the predicted change set is bigger than the scan that predicted "
|
||||
f"it: {terminal}"
|
||||
)
|
||||
|
||||
|
||||
def _admin_sources_page(page: Page, app_url: str, *, settled: bool = True) -> None:
|
||||
"""Real form login landing on the RAG page (the button's home).
|
||||
|
||||
@@ -796,9 +1040,11 @@ def test_running_label_counts_down_across_a_real_sync(
|
||||
assert terminal["detail"]["added"] == N_FILES, terminal
|
||||
|
||||
# --- the cross-layer invariant: the label is the payload's subtraction
|
||||
# The endpoint is untouched: it still answers with the 1-based
|
||||
# position, and each rendered N equals total − done of the file it
|
||||
# named.
|
||||
# Neither half of this changed in phase 194: the SUBTRACTION is the
|
||||
# phase-193 builder, and on this truncated-KB fixture the pair it
|
||||
# subtracts (the pre-scan's pending position, phase 194) coincides
|
||||
# with the walk position, so every rendered N still equals
|
||||
# total − done of the file the label named.
|
||||
position: dict[str, tuple[int, int]] = {}
|
||||
for tick in recorder.running_ticks:
|
||||
if tick["current_file"]:
|
||||
@@ -821,27 +1067,62 @@ def test_running_label_counts_down_across_a_real_sync(
|
||||
else:
|
||||
assert shown is None, f"{path}: done==total but a part rendered: {frame}"
|
||||
|
||||
# --- phase 194 (task 04, D5): the machine, off the SAME tick series --
|
||||
# The additive key set, the 0/0 prelude, the file-less scan ticks,
|
||||
# the one-directional sequence and the terminal that keeps BOTH
|
||||
# pairs (here equal, because a fresh KB's prediction IS its walk).
|
||||
_assert_phase_machine_tick_series(
|
||||
recorder.ticks, terminal, scan_total=N_FILES, files_total=N_FILES
|
||||
)
|
||||
|
||||
# --- phase 194: the scan label, WHEREVER the page caught the stage ---
|
||||
# Whether the UI's 2 s poll lands inside a five-file scan is a coin
|
||||
# flip (test 2 pins the stage deterministically through a served
|
||||
# status); what is never a coin flip is the SHAPE of the frames it
|
||||
# does catch and where they may sit in the sequence.
|
||||
scan_frames = [f for f in frames if f["text"].startswith("Scanning…")]
|
||||
for frame in scan_frames:
|
||||
assert SCAN_LABEL_RE.fullmatch(frame["text"]), frame
|
||||
assert " remaining" not in frame["text"], frame
|
||||
assert SOURCE not in frame["text"], f"the scan named a file: {frame}"
|
||||
# A4: the untruncated label rides the title AND the announcer.
|
||||
assert frame["title"] == frame["text"], frame
|
||||
assert frame["aria"] == frame["text"], frame
|
||||
assert frame["countHidden"] is not ("(" in frame["text"]), frame
|
||||
first_import = next(
|
||||
(i for i, t in enumerate(texts) if t.startswith("Syncing… ")), None
|
||||
)
|
||||
if first_import is not None:
|
||||
assert all(
|
||||
i < first_import for i, t in enumerate(texts) if t.startswith("Scanning…")
|
||||
), f"a scan label rendered after the import phase began: {texts[:14]}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. (c) The pre-walk edge + the A1 hidden edge, on the exact payloads the
|
||||
# guards read
|
||||
# 2. (c) The zero-total edges + the A1 hidden edge, on the exact payloads
|
||||
# the guards read
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_prewalk_and_final_file_labels_have_no_count_part(
|
||||
page: Page, app_url: str, db_ready: None
|
||||
) -> None:
|
||||
"""The two edges of A1, pinned on the payloads the guards actually
|
||||
read (the served bodies use the status endpoint's unchanged key set —
|
||||
what is under test is the client's ONE builder): ``total === 0``
|
||||
renders no count part, and neither does ``done === total``."""
|
||||
"""The zero-total edges of A1 plus the phase-194 scan stage, pinned on
|
||||
the payloads the guards actually read (the served bodies carry the
|
||||
status endpoint's key set — what is under test is the client's ONE
|
||||
builder): ``scan_total === 0`` renders no count part, ``total === 0``
|
||||
renders none either, and neither does ``done === total``."""
|
||||
page.set_default_timeout(SETTLE_TIMEOUT_MS)
|
||||
stages: dict[str, Any] = {
|
||||
"sync": _sync_status(state="running"), # the pre-walk: no file, 0/0
|
||||
# The stage phase 194 inserts where the pre-walk used to stand:
|
||||
# the pre-scan mid-flight, denominator known, position climbing.
|
||||
"sync": _sync_status(
|
||||
state="running", phase="scan", scan_done=2, scan_total=N_FILES
|
||||
),
|
||||
"upload": None,
|
||||
}
|
||||
_serve_statuses(page, stages)
|
||||
# settled=False: the boot fetch is the pre-walk stub — the re-attach
|
||||
# settled=False: the boot fetch is the scan stub — the re-attach
|
||||
# itself is part of what this arm pins.
|
||||
_admin_sources_page(page, app_url, settled=False)
|
||||
_install_recorder(page)
|
||||
@@ -849,9 +1130,36 @@ def test_prewalk_and_final_file_labels_have_no_count_part(
|
||||
box = page.locator("#sync-progress-text")
|
||||
btn = page.locator("#sync-btn")
|
||||
|
||||
# (c) The pre-walk — phase null, current_file null, files_total 0:
|
||||
# the bare prefix, no count part, the count span hidden, the title
|
||||
# (phase 194, D4) The scan stage speaks its OWN label: "Scanning…"
|
||||
# plus the (n/total) part in the SAME count slot the import phase's
|
||||
# "N remaining" uses (one writer, one hidden-toggle, one A4
|
||||
# placement) — never a file, never a chunk part, and the full label
|
||||
# on the button title.
|
||||
expect(box).to_have_text(f"Scanning… (2/{N_FILES})", timeout=SETTLE_TIMEOUT_MS)
|
||||
# The PART's own toggle, not Playwright visibility: while a run is in
|
||||
# flight the button is the spinner and CSS hides the label spans, so
|
||||
# the honest pin is the writer's ``hidden`` flag plus the span's text
|
||||
# (the in-browser recorder in test 1 reads the same property).
|
||||
expect(page.locator(".sync-label-count")).to_have_text(f"(2/{N_FILES})")
|
||||
expect(page.locator(".sync-label-count")).to_have_js_property("hidden", False)
|
||||
expect(btn).to_have_attribute("title", f"Scanning… (2/{N_FILES})")
|
||||
expect(btn).to_be_disabled()
|
||||
|
||||
# …and the same stage with NO denominator yet — the empty KB, and the
|
||||
# scan's own first frame: the phase-64 ``total === 0`` guard renders
|
||||
# the bare phase name, never "(0/0)".
|
||||
stages["sync"] = _sync_status(state="running", phase="scan")
|
||||
expect(box).to_have_text("Scanning…", timeout=SETTLE_TIMEOUT_MS)
|
||||
expect(page.locator(".sync-label-count")).to_have_text("")
|
||||
expect(page.locator(".sync-label-count")).to_have_js_property("hidden", True)
|
||||
expect(btn).to_have_attribute("title", "Scanning…")
|
||||
|
||||
# (c) The null prelude — phase null, current_file null, files_total 0
|
||||
# (the pre-phase pre-walk, and exactly what the
|
||||
# BOR_SYNC_PENDING_COUNT=0 rollback reports for the whole run): the
|
||||
# bare prefix, no count part, the count span hidden, the title
|
||||
# identical (A4).
|
||||
stages["sync"] = _sync_status(state="running")
|
||||
expect(box).to_have_text("Syncing…", timeout=SETTLE_TIMEOUT_MS)
|
||||
expect(page.locator(".sync-label-count")).to_be_hidden()
|
||||
expect(btn).to_have_attribute("title", "Syncing…")
|
||||
@@ -913,19 +1221,41 @@ def test_prewalk_and_final_file_labels_have_no_count_part(
|
||||
finished_at=_now_iso(),
|
||||
files_done=N_FILES,
|
||||
files_total=N_FILES,
|
||||
# The scan pair KEPT at the terminal beside the file counts (the
|
||||
# phase-64 keep-final-values convention) — the counts line below
|
||||
# ignores them, which is the point of pinning them here.
|
||||
scan_done=N_FILES,
|
||||
scan_total=N_FILES,
|
||||
detail={"added": N_FILES, "updated": 0, "unchanged": 0, "pruned": 0},
|
||||
)
|
||||
expect(box).to_have_text(counts_line(f"{N_FILES} added"), timeout=SETTLE_TIMEOUT_MS)
|
||||
expect(page.locator("#sync-label")).to_have_text(SYNCED_LABEL, timeout=30_000)
|
||||
expect(btn).to_be_enabled()
|
||||
|
||||
# Nothing rendered in this run ever spoke the pre-phase shape or a
|
||||
# "0 remaining".
|
||||
# Nothing rendered in this run ever spoke the pre-phase shape on a
|
||||
# "Syncing…" label or a "0 remaining".
|
||||
texts = _texts(_frames(page))
|
||||
assert not any(OLD_FRACTION_RE.search(t) for t in texts if t.startswith("Syncing…")), texts
|
||||
assert not any("0 remaining" in t for t in texts), texts
|
||||
assert "Syncing…" in texts, texts
|
||||
|
||||
# The scan stage rendered, in its own shape, and ONLY before the
|
||||
# import phase started: the (n/m) is the scan's position (and the
|
||||
# summaries phase's), never the sync's.
|
||||
scan_texts = [t for t in texts if t.startswith("Scanning…")]
|
||||
assert f"Scanning… (2/{N_FILES})" in scan_texts, texts
|
||||
assert "Scanning…" in scan_texts, texts
|
||||
for label in scan_texts:
|
||||
assert SCAN_LABEL_RE.fullmatch(label), label
|
||||
assert " remaining" not in label, label
|
||||
first_import = next(
|
||||
(i for i, t in enumerate(texts) if t.startswith("Syncing… ")), None
|
||||
)
|
||||
assert first_import is not None, texts
|
||||
assert all(
|
||||
i < first_import for i, t in enumerate(texts) if t.startswith("Scanning…")
|
||||
), texts
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. (b) A2 — the upload job shares the builder and the shape
|
||||
@@ -1021,3 +1351,118 @@ def test_upload_label_shares_the_count_down_builder(
|
||||
# has no phase / chunk / image fields — phase 129 / 152), so the
|
||||
# chunk part belongs to the sync arms only.
|
||||
assert " · chunk " not in label, label
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4. (phase 194, task 04 / D5) The re-sync: the prediction, not the walk
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_resync_of_an_unchanged_repo_reports_zero_work(
|
||||
page: Page, app_url: str, db_ready: None, remaining_repo: Path
|
||||
) -> None:
|
||||
"""The run phase 193's fixture never had: the SAME repo synced twice.
|
||||
|
||||
Run 1 fills the truncated KB, so its predicted change set IS the
|
||||
whole walk (``files_total == scan_total == N_FILES`` — the parity
|
||||
test 1 rides). Run 2 finds every byte where it left them, so the
|
||||
pre-scan predicts NOTHING: the ``progress`` pair is ``0/0`` on every
|
||||
tick and the client's ``total === 0`` guard (test 2) renders no count
|
||||
part — while ``scan_done`` / ``scan_total`` report the whole KB walked
|
||||
and are KEPT at the terminal beside the zeroed file pair. That
|
||||
divergence (5 scanned, 0 to re-index) is phase 194 in one line, and
|
||||
it is exactly the shape that used to render "``N`` remaining" for
|
||||
files nobody was going to re-index (the owner's prod report).
|
||||
|
||||
A no-change run makes ZERO LLM call (the phase-147 fast path), so its
|
||||
import window is milliseconds and the 2 s UI poll is not expected to
|
||||
render a file label for it: the pin is the WIRE pair plus the
|
||||
ABSENCE of a count part in whatever the page did render, not a
|
||||
rendered per-file label (test 1 covers the rendered side).
|
||||
"""
|
||||
page.set_default_timeout(SETTLE_TIMEOUT_MS)
|
||||
_admin_sources_page(page, app_url)
|
||||
_install_recorder(page)
|
||||
btn = page.locator("#sync-btn")
|
||||
box = page.locator("#sync-progress-text")
|
||||
|
||||
# --- run 1: the fresh import, the parity test 1 rides ---------------
|
||||
expect(btn).to_be_enabled()
|
||||
# wait_for_running: test 1's TERMINAL is still what the endpoint
|
||||
# answers with here (the run state lives in the app, not the page),
|
||||
# and without the flag this recorder would hand that body back as its
|
||||
# own before this test had synced anything at all.
|
||||
rec1 = _TickRecorder(app_url, "/api/sync/status", wait_for_running=True)
|
||||
rec1.start()
|
||||
btn.click()
|
||||
terminal1 = rec1.stop()
|
||||
assert terminal1["state"] == "success", terminal1
|
||||
expect(box).to_have_text(
|
||||
counts_line(f"{N_FILES} added"), timeout=SETTLE_TIMEOUT_MS
|
||||
)
|
||||
assert terminal1["files_total"] == N_FILES, terminal1
|
||||
assert terminal1["scan_total"] == N_FILES, terminal1
|
||||
frame_mark = page.evaluate("() => window.__borFrames.length")
|
||||
|
||||
# --- run 2: the same bytes, the owner's incremental case ------------
|
||||
expect(btn).to_be_enabled(timeout=SETTLE_TIMEOUT_MS)
|
||||
rec2 = _TickRecorder(app_url, "/api/sync/status", wait_for_running=True)
|
||||
rec2.start()
|
||||
btn.click()
|
||||
terminal2 = rec2.stop()
|
||||
assert terminal2["state"] == "success", terminal2
|
||||
|
||||
# The owner-visible terminal line stays the byte-identical phase-64
|
||||
# counts line — the exact surface the prediction never replaces.
|
||||
expect(box).to_have_text(
|
||||
counts_line(f"0 added · {N_FILES} unchanged"), timeout=SETTLE_TIMEOUT_MS
|
||||
)
|
||||
expect(page.locator("#sync-label")).to_have_text(SYNCED_LABEL)
|
||||
expect(btn).to_be_enabled()
|
||||
detail = terminal2["detail"]
|
||||
assert (detail["added"], detail["updated"], detail["unchanged"]) == (
|
||||
0,
|
||||
0,
|
||||
N_FILES,
|
||||
), detail
|
||||
|
||||
# THE PHASE IN ONE LINE: the scan walked the KB, the work was nothing.
|
||||
assert terminal2["scan_total"] == N_FILES, terminal2
|
||||
assert terminal2["scan_done"] == N_FILES, terminal2
|
||||
assert terminal2["files_total"] == 0, (
|
||||
f"a no-change run reported a non-zero denominator: {terminal2}"
|
||||
)
|
||||
assert terminal2["files_done"] == 0, terminal2
|
||||
for tick in rec2.ticks:
|
||||
assert (tick["files_done"], tick["files_total"]) == (0, 0), (
|
||||
f"the empty pending set leaked a walk position: {tick}"
|
||||
)
|
||||
assert tick["scan_total"] in (0, N_FILES), tick
|
||||
# `prelude_zero=False`: the runner resets ``state`` to ``running``
|
||||
# one synchronous statement before it resets the scan pair, and the
|
||||
# status endpoint answers from a threadpool — a poll landing in that
|
||||
# window would legitimately see the PREVIOUS run's kept 5/5. The
|
||||
# claim itself is pinned race-free in test 1 and test 2; repeating it
|
||||
# here would buy nothing but a 1-in-100k flake.
|
||||
_assert_phase_machine_tick_series(
|
||||
rec2.ticks, terminal2, scan_total=N_FILES, files_total=0, prelude_zero=False
|
||||
)
|
||||
|
||||
# Whatever run 2 rendered carried NO count part: no "N remaining"
|
||||
# (the empty pending set → the total === 0 guard) and no scan (n/m)
|
||||
# once the import phase had begun.
|
||||
run2_texts = _texts(_frames(page)[frame_mark:])
|
||||
assert run2_texts, (
|
||||
f"nothing was rendered after the run-1 settle mark ({frame_mark}) — "
|
||||
"the click's own label is missing from the recorder"
|
||||
)
|
||||
assert not any(" remaining" in t for t in run2_texts), run2_texts
|
||||
for label in run2_texts:
|
||||
if label.startswith("Scanning…"):
|
||||
assert SCAN_LABEL_RE.fullmatch(label), label
|
||||
if label.startswith("Syncing…"):
|
||||
assert not OLD_FRACTION_RE.search(label), label
|
||||
scan_idx = [i for i, t in enumerate(run2_texts) if t.startswith("Scanning…")]
|
||||
imp_idx = [i for i, t in enumerate(run2_texts) if t.startswith("Syncing… ")]
|
||||
if scan_idx and imp_idx:
|
||||
assert max(scan_idx) < imp_idx[0], run2_texts
|
||||
|
||||
Reference in New Issue
Block a user