Files
brain-of-reese/tests/e2e/test_sync_pending_count.py
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

2000 lines
88 KiB
Python
Raw Permalink 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 194 task 05 — the phase's own E2E suite: the count counts the WORK.
Run in isolation (DB must be up: ``podman compose up -d db``):
uv run pytest tests/e2e/test_sync_pending_count.py -v --no-cov
**The owner report (2026-10-19, TODO L6).** A sync on a roughly 10,000-file
knowledge base with a handful of changes counted down
``10000 → 9999 → … → 1216 remaining``. There was almost nothing left to DO at
1216; the sentence still read like there was, because the number was a WALK
POSITION wearing the shape of a work queue (``app/rag/importer.py``'s
per-file counter → ``app/api/sync.py``'s ``files_done`` / ``files_total`` →
``frontend/assets/sources.js``'s ``fmtSyncLabel`` ``N remaining`` part).
Phase 194 moved the count onto the WORK and gave the walk its own phase:
* ``app/rag/importer.py`` predicts the run's change set BEFORE the walk
(``_prescan_pending``: the streamed 1 MiB-chunk ``_file_hash`` against
``_pending_hash_stmt``, a SCALAR ``SELECT documents.file_hash`` on a
session of its own, off the event loop in ONE ``asyncio.to_thread`` span),
and the ``progress`` hook reports the position in THAT set;
* ``app/api/sync.py`` reports ``phase: "scan"`` for the pre-scan with
``scan_done`` / ``scan_total`` beside it, and flips to ``"import"`` on the
FIRST ``progress`` call inside the one existing ``_hook`` closure;
* ``frontend/assets/sources.js``'s ``fmtSyncPhaseLabel`` renders the scan as
``Scanning… (n/total)`` in the SAME count slot the import's
``N remaining`` uses — one writer, one branch, no new element.
Fixture (the house convention, the ``test_sync_remaining_count.py`` shape)
-------------------------------------------------------------------------
A real git repo over the ``file://`` transport (``BOR_GIT_SOURCES``, so the
env fallback IS the effective source under the autouse truncate) with
``N_FILES = 8`` markdown documents in walk order (``sorted(rel)``, the
importer's rule) — the FIRST and the LAST multi-chunk so the phase-129
``· chunk n/m`` part coexists with the count part and then stands alone —
and a mock upstream that delays EVERY call ``SLOW_DELAY_S``
(``tests/e2e/slow_llm.py``) so one single-chunk file's import span
(3 calls ≈ 3.6 s) outlives the page's 2 s status poll. A SECOND module app,
same repo shape, boots with ``BOR_SYNC_PENDING_COUNT=0`` (the phase-184
two-module-app convention) and is arm d's rollback machine. The KB state a
re-sync needs is established with the importer ITSELF, in-process, with the
``FakeEmbedder`` (the same code path, the same stamped ``file_hash``, the
same git commit dates) — a real slow-proxy first import of a 1,000-file KB
would turn the module into ten minutes for no extra evidence: every run
UNDER test is a real uvicorn sync driven from the real Sources page over
HTTP.
Determinism — what is pinned live and what is pinned on the exact payload
------------------------------------------------------------------------
The scan reads no file and embeds nothing, so it is FAST: eight tiny files
pre-scan in single-digit milliseconds and the 2 s poll cannot be made to land
inside that window. This suite does not pretend otherwise and does not pad
the fixture to fake it:
* the **live** scan pins are the ones a real run answers DETERMINISTICALLY —
every tick caught in ``phase == "scan"`` names NO file, carries the full
denominator, and no scan tick ever follows a tick that already counted
files; the terminal KEEPS the completed pair; and the
**``test_prod_shaped_run``** arm (a seeded 1,000-file KB, 2 files changed —
the owner's report at scale) makes the scan window long enough that its
position is caught climbing across several distinct ``scan_done`` values
THROUGH the running server while the loop keeps answering (the phase-179
contract — the scan is off the loop precisely so this poll is served);
* the ``Scanning… (n/total)`` **rendered** frames — and arm b's
unchanged-file HOLD — are pinned on the exact payloads the client's own
guards read, served through ``page.route`` to the real
``sources.js`` + the real 2 s poll (``test_sync_remaining_count.py`` test 2
and the phase-129/152 suites' pattern). The pair itself is pinned against
the real importer in ``tests/integration/test_sync_pending_count.py``
(its hook sequence IS ``[(changed,1,2), (unchanged,1,2), (unchanged,1,2),
(changed,2,2)]``) and through the live status machine in
``tests/integration/test_sync_scan_phase.py``; here it is pinned against
the guards that turn it into a sentence.
Arms (the task file's a–e + the two standing contracts), in test order
----------------------------------------------------------------------
1. ``test_first_sync_counts_the_predicted_set_and_the_machine_completes`` —
**(a) the first sync, fresh KB:** pending == walk, so the label counts the
phase-193 arithmetic exactly — ``7 remaining`` down to ``1``, the part
HIDDEN at the final file while the main part still names it, the chunk
part coexisting — and the wire's null prelude reports ``0/0``, every tick
is the additive key set, every import tick reports the whole 8-file
denominator, the terminal keeps ``scan_done == scan_total == 8`` beside
``files_done == 8`` and settles to ``8 added``.
2. ``test_incremental_sync_counts_only_the_changed_files`` — **(b) the
owner's production case, the phase's heart:** the KB already holds all 8
documents, TWO files changed. Every running tick answers the PREDICTED
denominator (``files_total == 2``, never ``8``) and the pair HOLDS across
the six unchanged files (the exact expected pair per walked file, so the
pin does not depend on which frames the 2 s poll happened to sample);
rendered, the part reads ``1 remaining`` across the first predicted file
and is GONE across the final predicted file while the main part names it;
no label ever shows a walk-derived number; the terminal is
``2 updated · 6 unchanged`` with ``2/2`` files and the completed
``8/8`` scan kept beside it.
3. ``test_scan_frames_and_the_hold_render_from_the_exact_payloads`` — the
same two machines rendered by the REAL client on the frames the live run
produces but the 2 s poll cannot be aimed at: two CONSECUTIVE
unchanged-file frames at the same ``2 remaining`` then the first
predicted file at ``1 remaining`` then the final predicted file with the
part gone; ``Scanning… (3/8)`` → ``(8/8)`` with no file and never
``N remaining``; ``scan_total: 0`` → the BARE ``Scanning…``; the null
prelude → the bare ``Syncing…``; every frame A4-complete (button
``title`` + the sole ``#sync-progress`` live region byte-equal to the
text) and no ``Syncing…`` label ever wearing an ``(n/m)`` tail.
4. ``test_kill_switch_zero_restores_the_walk_position_count`` — **(d):** the
second app, arm b's fixture. No ``scan`` tick exists at all, ``scan_done``
/ ``scan_total`` are ``0/0`` for the whole run including the terminal, and
the count part counts the WALK: ``files_total == 8`` and the rendered
figure naming files that will never be re-indexed (arm b answers ``1
remaining`` for the same file at the same moment in the same repository).
5. ``test_no_change_sync_reports_no_denominator`` — **(c):** the re-sync of
a repository nothing touched. The scan completes (``8/8`` kept), the
predicted set is EMPTY so every tick answers ``files_total == 0`` and no
label EVER carries a count part (the phase-64 ``total === 0`` guard driven
by zero work rather than by a missing denominator), and the terminal is
the byte-identical ``0 added · 8 unchanged``.
6. ``test_empty_source_stays_in_scan_with_no_count_part`` — **(e) the D1
degenerate:** a source with ZERO importable files. Nothing is walked, so
no ``progress`` call exists to flip the phase: the machine never answers
``import``, both pairs stay ``0/0`` through the run, no label ever carries
a count part, and the terminal clears the phase and settles to
``0 added · 0 unchanged``.
7. ``test_prod_shaped_run_scan_climbs_then_counts_two`` — the report AT
SCALE: a seeded 1,000-file KB with 2 files changed, over the real page.
The scan's own position climbs across ≥2 distinct ``scan_done`` values
while ``current_file`` stays null, the import then answers the PREDICTED
denominator (``files_total == 2`` against ``scan_total == 1000`` — the
divergence the pre-phase shape could not express), and the terminal keeps
BOTH pairs beside each other.
8. ``test_upload_run_carries_no_scan_fields_and_no_count_part`` — the
phase-90 (A2) convention with the scan pair ADDED to the not-there list:
a real background upload's live status answers the bare phase-64 key set
(no ``phase``, no chunk/image/summary fields, no ``scan_*``, no memory
pair) with ``null``/``0``/``0``, and the shared label renders that job
bare — no file, no count part (the upload branch never receives a status
object, so phase 194 gives it nothing).
The phase-187 peak clause, across these same runs (AGENTS rule 10 — the
clause is TERMINAL text; task 05's "visible while the scan label is showing"
is read against that rule, so what is pinned is the clause's presence at the
terminal of a scan-labelled run and its ABSENCE on every running label): test
1 pins the real run's settle — the counts line ends in ``· peak <N> MB`` equal
to the terminal payload's ``rss_peak_mb`` (one reading, two surfaces), as TEXT
on the line that already exists, with the box holding its ONE child span and
its ``role=status`` / ``aria-live=polite`` intact; test 3 pins the served
corollary — a running frame that CARRIES ``rss_mb`` / ``rss_peak_mb`` renders
NO clause, and the terminal of that same served sequence renders
``0 added · 2 updated · 6 unchanged · peak 1234 MB`` (a served payload answers
the clause independently of this host, so the shape is pinned even where
``MEMORY_KNOWABLE`` is False).
Test → task mapping (the Playwright Mapping Rule): 1 → arm a + the real-run
peak clause, 2 → arm b, 3 → arms a/b's rendered frames + the A4 placement +
the peak-clause absence-while-running, 4 → arm d, 5 → arm c, 6 → arm e,
7 → arm a's scan-climb at prod shape (arm b's denominator at scale),
8 → the upload pin.
"""
from __future__ import annotations
import asyncio
import io
import json
import os
import re
import subprocess
import sys
import tarfile
import threading
import time
from collections.abc import Iterator
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
import httpx
import pytest
from playwright.sync_api import Page, expect
from sqlalchemy import text
from app.config import Settings
from app.core.memory import rss_mb, rss_peak_mb
from app.db import SessionLocal
from app.rag.chunker import chunk_document
from app.rag.importer import import_sources
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 counts_line
from scripts.git_sync import file_commit_dates
from tests.fakes import FakeEmbedder
REPO = Path(__file__).resolve().parents[2]
#: This module's own ports — the sync suites' ladder continues past the
#: phase-193 suite's 8211/8914 (the one-suite-at-a-time rule makes a
#: collision harmless, but these are free and env-overridable).
APP_PORT = int(os.environ.get("E2E_APP_PORT_PENDING", "8212"))
APP_URL = f"http://127.0.0.1:{APP_PORT}"
ZERO_PORT = int(os.environ.get("E2E_APP_PORT_PENDING_ZERO", "8213"))
ZERO_URL = f"http://127.0.0.1:{ZERO_PORT}"
SLOW_PORT = int(os.environ.get("E2E_SLOW_LLM_PORT_PENDING", "8915"))
SLOW_URL = f"http://127.0.0.1:{SLOW_PORT}"
SOURCES_URL = "/sources.html"
#: Per-LLM-request delay on the proxy. A one-chunk file costs THREE calls
#: (content embed batch + the summary chat + the summary-chunk embed) ≈ 3.6 s
#: against the page's 2 s poll, so every file gets a rendered label of its
#: own — and an UNCHANGED file whose summary is NULL costs TWO (the phase-118
#: backfill), which is what makes an unchanged file's HELD frame observable
#: at all (arm b).
SLOW_DELAY_S = "1.2"
#: The module's wall-time budget (two app boots + three real syncs of the
#: 8-file repo + the 1,000-file prod-shaped run + the served-status arms). A
#: breach is a speed canary, not flake tolerance (the phase-64 convention).
WALL_BUDGET_S = 600.0
#: The sync's subject: a git repo whose DIRECTORY NAME is the source name
#: (the ``repo_name`` basename rule). Deliberately NOT named after anything
#: under test — a path containing "remaining" would make every "does this
#: label carry the count part?" check match the path itself.
SOURCE = "pending"
N_FILES = 8
#: The walk order is ``sorted(rel)`` (the importer's rule), so each file's
#: 1-based WALK position is its rank here — the map the arms assert against.
#: The FIRST and the LAST file are multi-chunk (the phase-129 convention: the
#: chunk part climbs on the first file and stands alone, without a count
#: part, on the last).
REL_ORDER = [
"notes/a-multi-first.md",
"notes/b-second.md",
"notes/c-third.md",
"notes/d-fourth.md",
"notes/e-fifth.md",
"notes/f-sixth.md",
"notes/g-seventh.md",
"notes/z-multi-last.md",
]
FIRST_REL = REL_ORDER[0]
LAST_REL = REL_ORDER[-1]
#: Arm b's change set: walk positions 5 and 7 of 8. The LAST predicted file
#: is therefore neither the last walked file nor the first — the count part
#: holds at ``2 remaining`` across the unchanged files before it, reads
#: ``1 remaining`` across the first predicted one and disappears across the
#: final predicted one while the walk still has a file to go (the exact shape
#: the pre-phase count read "``N`` remaining" through).
CHANGED_RELS = ("notes/e-fifth.md", "notes/g-seventh.md")
#: Arm b's UNCHANGED files whose stored summary the seed clears — walk
#: positions 1 and 2, both BEFORE the first predicted file. Walking one still
#: costs the phase-118 summary backfill (a ``lite`` call + its embed ≈ 2.4 s
#: at this delay), which is what makes an unchanged file's HELD frame land in
#: the browser instead of living only in the integration suite.
HELD_RELS = ("notes/a-multi-first.md", "notes/b-second.md", "notes/d-fourth.md")
#: Arm d (the ``BOR_SYNC_PENDING_COUNT=0`` app) changes positions 5 and 8 of
#: the same fixture: under the rollback semantics the pair is the WALK, so the
#: file arm b renders as "``1`` remaining" appears here as "``3`` remaining"
#: and the last walked file is the only one that hides the part.
ZERO_CHANGED_RELS = ("notes/e-fifth.md", "notes/z-multi-last.md")
#: The count-bearing values the label may ever show (A1): ``1 … N−1``.
MAX_COUNT = N_FILES - 1
#: The prod-shaped arm's KB (the owner's report at scale): 1,000 unchanged
#: documents walked, TWO of them changed. A ``kind="local"`` source row, so
#: the walk reads the seeded directory itself (no clone, no commit dates —
#: the importer's mtime fallback) and the run's ONLY model work is the two
#: changed files.
WIDE_SOURCE = "pendingwide"
WIDE_FILES = 1_000
WIDE_CHANGED_RELS = ("notes/w0003.md", "notes/w0008.md")
#: The archive the upload arm uploads (the phase-98 fixture convention).
UPLOAD_NAME = "uploadkb"
UPLOAD_FILES: dict[str, str] = {
f"docs/{i:02d}.md": f"# Upload doc {i:02d}\n\nDeterministic upload body {i:02d}.\n"
for i in range(3)
}
#: The chunker settings the apps under test are pinned to (the leak-guard
#: convention): the expected ``chunks_total`` is computed with the SAME
#: function and arguments the importer uses, so a local ``.env`` cannot move
#: the number this suite asserts.
CHUNK_TARGET = int(Settings.model_fields["chunk_target_chars"].default)
CHUNK_OVERLAP = int(Settings.model_fields["chunk_overlap_chars"].default)
#: The import-phase label grammar (phase 193) — the main part (the bare
#: ``Syncing…`` of the null prelude, which names no file, or the prefix plus
#: the ``source/path``), the OPTIONAL count part, the OPTIONAL phase-129 chunk
#: part. The arms that pin a NAMED file re-anchor it on their own paths.
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) 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+\))?$")
#: What phase 194 leaves forbidden: no ``Syncing…`` label may wear an
#: ``(n/m)`` tail again (the shape is legal only on ``Scanning…`` and
#: ``Summarizing folders…``).
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 — the additive chain's right-hand
#: side as of phase 194 (phase 64/98/129/152 fields + the phase-187 memory
#: pair + the phase-194 scan pair). ONE constant, mirrored by the served stubs
#: below and pinned on every live tick, so a future key has to be added here
#: (and to the stub) instead of drifting away from the client's view.
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",
}
)
#: The UPLOAD status key set (phase 90 A2): the bare sync subset, and the
#: whole point of the arm — no ``phase``, no chunk / image / summary fields,
#: no memory pair and (new in phase 194) NO ``scan_done`` / ``scan_total``.
UPLOAD_STATUS_KEYS = frozenset(
{
"state",
"started_at",
"finished_at",
"current_file",
"files_done",
"files_total",
"detail",
"error",
}
)
#: The phase-187 clause, and the shape it must NEVER take on a running label.
PEAK_CLAUSE_RE = re.compile(r" · peak (\d+) MB$")
#: Does THIS platform answer the memory helpers? The app under test is a
#: subprocess of this process's own OS, so this answer is the app's answer
#: (the phase-187 suite's reasoning). Where it is False the real-run clause
#: pin steps aside and the ABSENCE contract is pinned instead — the served
#: arm keeps the clause's SHAPE pinned either way, because a served payload
#: answers independently of this host.
MEMORY_KNOWABLE = rss_mb() is not None and rss_peak_mb() is not None
SETTLE_TIMEOUT_MS = 240_000
def _now_iso() -> str:
"""A "just finished" stamp for the served terminals — the phase-154
same-day rule renders it as the plain ``Synced HH:MM``."""
return datetime.now(UTC).isoformat()
# ---------------------------------------------------------------------------
# The fixture KB
# ---------------------------------------------------------------------------
def _tiny_md(title: str) -> str:
"""A one-chunk document (its import span = 3 slowed LLM calls)."""
return f"# {title}\n\nDeterministic one-chunk body for the {title}.\n"
def _multi_md(tag: str, sections: int = 3) -> str:
"""A multi-chunk document: an H1 preamble (one chunk) + *sections* ``##``
sections of ~1500 chars each (one chunk apiece at the code-default
target, one embed batch apiece at the 700-token request budget) — no
randomness, so the chunk count is stable run to run (the phase-129
fixture idiom)."""
parts = [f"# {tag} multi-chunk fixture document\n"]
parts.append(
f"Preamble paragraph of the {tag} pending-count fixture. This paragraph "
"sits under the H1 and chunks on its own.\n"
)
for k in range(1, sections + 1):
parts.append(f"\n## Section {k}\n")
base = (
f"Section {k} paragraph of the {tag} pending-count fixture carries "
"deterministic filler for the chunker: alpha bravo charlie delta "
"echo foxtrot golf hotel india juliet kilo lima mike november oscar "
"papa quebec romeo sierra tango uniform victor whiskey xray yankee "
"zulu number "
)
parts.append((base * 4)[:1500] + "\n\n")
return "".join(parts)
def _fixture_files() -> dict[str, str]:
files = {rel: _tiny_md(Path(rel).stem) for rel in REL_ORDER}
files[FIRST_REL] = _multi_md("first")
files[LAST_REL] = _multi_md("last")
return files
def _expected_content_chunks(rel: str) -> int:
"""The file's TRUE content-chunk count — the SAME function and arguments
the importer's chunk replacement uses (phase 129: ``chunks_total`` is a
chunk count, never a batch count)."""
return len(chunk_document(_fixture_files()[rel], rel, max(400, CHUNK_TARGET), CHUNK_OVERLAP))
def _wide_files() -> dict[str, str]:
"""The prod-shaped fixture: ``WIDE_FILES`` one-chunk documents under
``notes/`` (the walk order is the zero-padded stem, so the changed pair
lands at WALK positions 3 and 8)."""
return {
f"notes/w{i:04d}.md": _tiny_md(f"wide document {i:04d}")
for i in range(1, WIDE_FILES + 1)
}
def _git(cwd: Path, *args: str) -> None:
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(cwd: Path, msg: str) -> None:
_git(cwd, "add", "-A")
_git(
cwd,
"-c", "user.email=e@x", "-c", "user.name=t", "-c", "commit.gpgsign=false",
"commit", "-qm", msg,
)
def _write_repo(root: Path, files: dict[str, str], msg: str) -> Path:
root.mkdir(parents=True, exist_ok=True)
for rel, content in files.items():
target = root / rel
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(content, encoding="utf-8")
_git(root, "init", "-q")
_commit(root, msg)
assert (root / ".git").is_dir()
return root
@pytest.fixture(scope="module")
def pending_repo(tmp_path_factory: pytest.TempPathFactory) -> Path:
"""The real repo the sync clones over ``file://`` — 8 markdown docs in
the walk order ``REL_ORDER``."""
root = tmp_path_factory.mktemp("bor_pending") / SOURCE
_write_repo(root, _fixture_files(), "pending-count fixture")
assert len(list(root.rglob("*.md"))) == N_FILES
return root
@pytest.fixture(scope="module")
def zero_repo(tmp_path_factory: pytest.TempPathFactory) -> Path:
"""The kill-switch app's OWN repo (same shape, different source name):
arms b and d change files in their own repo, so neither arm's committed
state depends on the other's order."""
root = tmp_path_factory.mktemp("bor_pending_zero") / "pendingzero"
_write_repo(root, _fixture_files(), "pending-count rollback fixture")
assert len(list(root.rglob("*.md"))) == N_FILES
return root
@pytest.fixture(scope="module")
def wide_kb(tmp_path_factory: pytest.TempPathFactory) -> Path:
"""The prod-shaped KB: 1,000 tiny docs in a plain DIRECTORY, registered
per test as a ``kind=local`` row. The walk reads the seeded directory
itself (no clone, no commit dates — the importer's mtime fallback), which
is the cheap, honest way to give the pre-scan real work: the alternative
is a git repo whose 1,000-file clone costs more than the scan under
test."""
root = tmp_path_factory.mktemp("bor_pending_wide") / WIDE_SOURCE
root.mkdir(parents=True, exist_ok=True)
for rel, content in _wide_files().items():
target = root / rel
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(content, encoding="utf-8")
assert len(list(root.rglob("*.md"))) == WIDE_FILES
return root
@pytest.fixture(scope="module")
def empty_kb_dir(tmp_path_factory: pytest.TempPathFactory) -> Path:
"""Arm e's source: a directory with ZERO importable files — one visible
file whose extension is not in the A9 family (so "nothing importable" is
a property of the CONTENT, not of an empty directory) and one hidden
``.md`` the walk skips. Registered per test as a ``kind=local`` row (the
phase-49 contract: a stored row IS the effective source, which is what
lets this arm exist on the module's own app instead of a third boot)."""
root = tmp_path_factory.mktemp("bor_pending_empty") / "pendingempty"
root.mkdir(parents=True, exist_ok=True)
(root / "README.gitkeep").write_text("nothing here is importable\n", encoding="utf-8")
(root / "notes").mkdir()
# A REAL dotfile (the walk's hidden rule), not merely a file about
# hiding — the arm's premise is a source with NOTHING importable.
(root / "notes" / ".hidden.md").write_text(
"# hidden\n\nSkipped: dotfile.\n", encoding="utf-8"
)
return root
@pytest.fixture(scope="module")
def upload_archive(tmp_path_factory: pytest.TempPathFactory) -> Path:
root = tmp_path_factory.mktemp("bor_pending_archive")
path = root / f"{UPLOAD_NAME}.tar.gz"
with tarfile.open(path, "w:gz") as tf:
for rel, content in UPLOAD_FILES.items():
data = content.encode("utf-8")
info = tarfile.TarInfo(rel)
info.size = len(data)
info.mtime = 0
tf.addfile(info, io.BytesIO(data))
return path
# ---------------------------------------------------------------------------
# The apps
# ---------------------------------------------------------------------------
def _app_env(
base_url: str,
source: str,
checkout_dir: Path,
upload_dir: Path,
*,
pending_count: str | None = None,
) -> dict[str, str]:
"""The per-module app env (the conftest pattern): the slow proxy is the
LLM, ``BOR_GIT_SOURCES`` is a suite-owned ``file://`` repo (the env
fallback IS the effective source — the autouse truncate keeps the
``git_sources`` table empty), and the chunker knobs are pinned to the
code defaults so the expected ``chunks_total`` stays predictable.
``pending_count`` is the phase's ONE switch: ``None`` leaves
``BOR_SYNC_PENDING_COUNT`` at the default (``1`` — count the predicted
change set), ``"0"`` is the exact phase-193 rollback arm d drives.
"""
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
env["BOR_RELEVANCE_THRESHOLD"] = "0.30"
env["BOR_LEXICAL_SUPPORT_FLOOR"] = "0.15"
env["BOR_SOURCE_USEFULNESS_FLOOR"] = "0.15"
env["BOR_LLM_RETRY_DELAY"] = "0"
env.setdefault(
"BOR_DATABASE_URL",
"postgresql+psycopg://reese:reese@localhost:5432/brain_of_reese",
)
env["BOR_ADMIN_PASSWORD"] = ADMIN_PASSWORD
env["BOR_SESSION_SECRET"] = SESSION_SECRET
env["BOR_GIT_SOURCES"] = f"file://{source}"
env["BOR_SOURCES_DIR"] = str(checkout_dir)
env["BOR_UPLOAD_DIR"] = str(upload_dir)
env["BOR_CHUNK_TARGET_CHARS"] = str(CHUNK_TARGET)
env["BOR_CHUNK_OVERLAP_CHARS"] = str(CHUNK_OVERLAP)
env["BOR_DOCS_REPO"] = ""
env["BOR_SUGGESTIONS"] = json.dumps(["Ask something neutral"])
if pending_count is not None:
env["BOR_SYNC_PENDING_COUNT"] = pending_count
return env
def _spawn_uvicorn(app: str, port: int, env: dict[str, str], probe: str) -> Iterator[None]:
"""One uvicorn subprocess, terminated on teardown (the conftest body,
factored so this module can own TWO apps without duplicating the
lifecycle). The caller yields its OWN base URL — that is what the suite's
``app_url`` hands to the page."""
proc = subprocess.Popen(
[sys.executable, "-m", "uvicorn", app, "--host", "127.0.0.1",
"--port", str(port), "--log-level", "warning"],
cwd=REPO,
env=env,
)
try:
_wait_http(probe)
yield
finally:
proc.terminate()
try:
proc.wait(timeout=10)
except subprocess.TimeoutExpired:
proc.kill()
def _spawn_slow_proxy(port: int, delay_s: str, probe: str) -> Iterator[int]:
"""The delay-injecting proxy (``tests/e2e/slow_llm.py``) in front of the
mock LLM — the suite's timing fixture: it delays EVERY LLM call, embeds
included, so each file's import span outlives the UI's 2 s poll."""
env = dict(os.environ)
env.pop("DEBUGPY", None)
env["SLOW_LLM_DELAY_S"] = delay_s
env["E2E_MOCK_PORT"] = str(MOCK_PORT)
for _ in _spawn_uvicorn(
"tests.e2e.slow_llm:app", port, env, f"{probe}/v1/models"
):
yield port
@pytest.fixture(scope="module")
def slow_llm(mock_llm: int) -> Iterator[int]:
yield from _spawn_slow_proxy(SLOW_PORT, SLOW_DELAY_S, SLOW_URL)
@pytest.fixture(scope="module")
def app_server(
mock_llm: int,
slow_llm: int,
pending_repo: Path,
tmp_path_factory: pytest.TempPathFactory,
) -> Iterator[str]:
"""The app under test — the DEFAULT ``BOR_SYNC_PENDING_COUNT`` (1)."""
env = _app_env(
f"{SLOW_URL}/v1",
str(pending_repo),
tmp_path_factory.mktemp("bor_pending_checkouts"),
tmp_path_factory.mktemp("bor_pending_uploads") / "uploads",
)
for _ in _spawn_uvicorn("app.main:app", APP_PORT, env, f"{APP_URL}/api/health"):
yield APP_URL
@pytest.fixture(scope="module")
def zero_server(
mock_llm: int,
slow_llm: int,
zero_repo: Path,
tmp_path_factory: pytest.TempPathFactory,
) -> Iterator[str]:
"""The SECOND module app — the same fixture shape booted with
``BOR_SYNC_PENDING_COUNT=0`` (the phase-184 two-module-app convention:
one setting, two live servers, the owner-visible rollback proven instead
of asserted about)."""
env = _app_env(
f"{SLOW_URL}/v1",
str(zero_repo),
tmp_path_factory.mktemp("bor_pending_zero_checkouts"),
tmp_path_factory.mktemp("bor_pending_zero_uploads") / "uploads",
pending_count="0",
)
for _ in _spawn_uvicorn("app.main:app", ZERO_PORT, env, f"{ZERO_URL}/api/health"):
yield ZERO_URL
@pytest.fixture(scope="module")
def app_url(app_server: str) -> str:
return app_server
def _truncate_all() -> None:
"""Fresh registry + KB per test (the E2E isolation pattern): the suites
share one Postgres, and a leftover document would move both the ``added``
counts the terminal line pins AND the pre-scan's prediction."""
with SessionLocal() as db:
db.execute(
text(
"TRUNCATE chunks, documents, query_log, steering_notes, "
"kb_overview, git_sources, folder_summaries"
)
)
db.commit()
def _wait_sync_settled(base_url: str, timeout_s: float = 300.0) -> None:
"""No run may leak across a test boundary (the run state lives in the
app's memory and a running run keeps importing into the NEXT test's
truncated KB). Own admin session; usually a no-op."""
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"] == "failed":
print(f"sync pending-count: FAILED terminal at a boundary: {body.get('error')}")
if body["state"] != "running":
return
time.sleep(0.2)
raise AssertionError(f"a sync was still running at a test boundary: {base_url}")
def _settle(base_url: str) -> None:
"""Settle an app that MAY not be up (the zero app boots lazily with its
fixture, and the autouse cleaner must not force it into every test)."""
try:
httpx.get(f"{base_url}/api/health", timeout=2.0).raise_for_status()
except Exception: # noqa: BLE001 — not booted (yet): nothing to settle
return
_wait_sync_settled(base_url)
@pytest.fixture(autouse=True)
def _clean(app_url: str, db_ready: None) -> Iterator[None]:
_settle(app_url)
_settle(ZERO_URL)
_truncate_all()
yield
_truncate_all()
@pytest.fixture(scope="module", autouse=True)
def _wall_time_guard() -> Iterator[None]:
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 KB state a re-sync starts from: the importer itself, in-process
# ---------------------------------------------------------------------------
def _seed_kb(
root: Path,
*,
null_summary_rels: tuple[str, ...] = (),
git: bool = True,
) -> None:
"""Index *root* with the importer ITSELF (in-process, ``FakeEmbedder``).
The same code path a real sync runs, so the rows carry the real
``file_hash`` stamps the phase-147 fast path compares against — which is
what makes the NEXT (real, in-app, slow-proxy) sync predict exactly the
files this test changed and nothing else. The git arms pass the
checkout's own commit dates, so the run under test finds every date
already correct and ``unchanged`` means unchanged.
``null_summary_rels`` then clears those rows' ``summary`` — the
phase-118 (A2) backfill case: the file stays UNCHANGED (its digest
matches, so the pre-scan predicts nothing for it), but walking it still
costs the ``lite`` summary call plus its embed. That is the ONE honest
way to make an unchanged file occupy real, model-bound, loop-free time,
and therefore the way arm b's held frame becomes observable in the
browser instead of theoretical.
``git=False`` skips the commit-date map — the seed of a ``kind=local``
source, whose run uses the importer's mtime fallback on BOTH sides
(matching, so the arm under test reports no date churn either).
"""
dates = {str(root): file_commit_dates(root)} if git else None
outcome: dict[str, BaseException] = {}
async def _go() -> None:
# The Playwright sync API owns an event loop in this process, so the
# seed gets its OWN loop on its OWN thread. The importer is the same
# async code either way and the session is a plain sync one — this is
# the ``tests/integration/test_sync_pending_count.py`` call, one
# thread over.
with SessionLocal() as inner:
await import_sources(
[root],
FakeEmbedder(),
session=inner,
doc_dates_by_root=dates,
)
def _worker() -> None:
try:
asyncio.run(_go())
except BaseException as exc: # noqa: BLE001 — re-raised on the caller
outcome["error"] = exc
seed_thread = threading.Thread(target=_worker)
seed_thread.start()
seed_thread.join()
if "error" in outcome:
raise AssertionError(
f"the KB seed import over {root} failed: {outcome['error']!r}"
) from outcome["error"]
if null_summary_rels:
with SessionLocal() as db:
db.execute(
text(
"UPDATE documents SET summary = NULL "
"WHERE source = :source AND path = ANY(:rels)"
),
{"source": root.name, "rels": list(null_summary_rels)},
)
db.execute(
text(
"DELETE FROM chunks WHERE is_summary AND document_id IN "
"(SELECT id FROM documents WHERE source = :source "
"AND path = ANY(:rels))"
),
{"source": root.name, "rels": list(null_summary_rels)},
)
db.commit()
def _change_files(root: Path, rels: tuple[str, ...], marker: str) -> None:
"""Touch *rels* on disk (and commit them, so the ``file://`` clone/pull
the app performs carries the change)."""
for rel in rels:
path = root / rel
path.write_text(
path.read_text(encoding="utf-8") + f"\n{marker} edit of {rel}\n",
encoding="utf-8",
)
_commit(root, f"{marker}: {', '.join(rels)}")
# ---------------------------------------------------------------------------
# The in-browser recorder: every value the ONE label writer lands
# ---------------------------------------------------------------------------
#: Read in ONE JS task, so a frame can never straddle two writes: the box
#: text (the phase-150 sole sync text surface + sole ``aria-live`` region),
#: the button ``title`` and ``aria-label`` (the phase-64 A4 untruncated
#: placement), and the ``.sync-label-count`` span's own visibility.
_INSTALL_RECORDER = """
() => {
if (window.__borFrames) return window.__borFrames.length;
window.__borFrames = [];
const box = document.querySelector('#sync-progress');
const textEl = document.querySelector('#sync-progress-text');
const btn = document.querySelector('#sync-btn');
const countEl = document.querySelector('.sync-label-count');
const rec = () => {
const frame = {
text: textEl.textContent,
title: btn.getAttribute('title') || '',
aria: btn.getAttribute('aria-label') || '',
count: countEl ? countEl.textContent : '',
countHidden: countEl ? countEl.hidden : null,
hidden: box.hidden,
};
const last = window.__borFrames[window.__borFrames.length - 1];
if (last
&& last.text === frame.text
&& last.title === frame.title
&& last.aria === frame.aria
&& last.countHidden === frame.countHidden) {
return;
}
window.__borFrames.push(frame);
};
const mo = new MutationObserver(rec);
mo.observe(textEl, { childList: true, characterData: true, subtree: true });
mo.observe(btn, { attributes: true, attributeFilter: ['title', 'aria-label'] });
if (countEl) mo.observe(countEl, { attributes: true, attributeFilter: ['hidden'] });
rec();
return window.__borFrames.length;
}
"""
def _install_recorder(page: Page) -> None:
page.evaluate(_INSTALL_RECORDER)
def _frames(page: Page) -> list[dict[str, Any]]:
frames: list[dict[str, Any]] = page.evaluate("() => window.__borFrames")
assert frames, "the in-browser recorder captured no label frames"
return frames
def _texts(frames: list[dict[str, Any]]) -> list[str]:
"""The recorded label sequence, empty frames dropped (the box's
idle-hide is not a label)."""
return [f["text"] for f in frames if f["text"]]
def _count_of(label: str) -> int | None:
"""The rendered ``N remaining`` value, or ``None`` when the label carries
no count part (A1's hidden state)."""
m = re.search(r" (\d+) remaining(?: · |$)", label)
return int(m.group(1)) if m else None
def _file_of(label: str) -> str | None:
"""The document an import label names, RELATIVE to its source (the
fixture's own ``rel`` — the source-name segment is dropped, since the
label's ``source/path`` identity is what the walk orders by)."""
m = re.match(r"^Syncing… (\S+?)(?: \d+ remaining| · chunk |\s*$)", label)
if not m:
return None
full = m.group(1)
return full.split("/", 1)[1] if "/" in full else full
#: The running-label prefixes (the ONLY states ``setSyncLabel`` gives a full
#: untruncated title/announcer with — the terminals are the counts line + the
#: ``Synced HH:MM`` button label, whose title is the idle one by contract).
_RUNNING_PREFIXES = (
"Syncing…",
"Scanning…",
"Importing",
"Writing KB overview…",
"Summarizing folders…",
)
def _assert_a4(frames: list[dict[str, Any]]) -> None:
"""A4 (phase 64, unchanged by 194): every RUNNING label — the scan's
``Scanning… (n/total)`` included — rides the button ``title``, the button
``aria-label`` and the announcer byte-equal to the text, and the count
span is un-hidden exactly when the text carries a count part."""
for frame in frames:
if not frame["text"].startswith(_RUNNING_PREFIXES):
continue
assert frame["title"] == frame["text"], frame
assert frame["aria"] == frame["text"], frame
assert frame["countHidden"] is not (
" remaining" in frame["text"] or "(" in frame["text"]
), frame
def _pending_positions(
walk_order: tuple[str, ...], changed: tuple[str, ...]
) -> dict[str, tuple[int, int]]:
"""The pair the status must carry while each walked file is in flight —
the arithmetic the pre-scan produces, computed HERE from the fixture
(``total`` = the size of the predicted set, ``done`` = the 1-based rank of
the current file INSIDE it, held for every file outside it)."""
pending = [rel for rel in walk_order if rel in set(changed)]
out: dict[str, tuple[int, int]] = {}
seen = 0
for rel in walk_order:
if rel in pending:
seen += 1
out[rel] = (seen, len(pending))
return out
def _walk_positions(walk_order: tuple[str, ...]) -> dict[str, tuple[int, int]]:
"""The same map under ``BOR_SYNC_PENDING_COUNT=0``: every walked file
advances the count, and the denominator is the whole walk (phase 193)."""
return {
rel: (i + 1, len(walk_order)) for i, rel in enumerate(walk_order)
}
class _TickRecorder:
"""The deterministic layer: a daemon thread tight-polling 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.
A body belongs to THIS run once the recorder has seen it go ``running``,
or — for a run faster than the cadence — once its own ``started_at``
post-dates the recorder's start. ``interval`` is the cadence: the default
100 ms resolves the phase machine on an 8-file run, and the prod-shaped
arm tightens it to resolve the pre-scan's position (the scan runs OFF the
loop, so this poll is answered THROUGH it — the phase-179 contract).
"""
def __init__(
self, base_url: str, path: str, *, wait_for_running: bool = True, interval_s: float = 0.1
) -> None:
self._url = f"{base_url}{path}"
self._login_url = f"{base_url}/api/login"
self._wait_for_running = wait_for_running
self._interval_s = interval_s
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:
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:
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=10.0) as client:
client.post(self._login_url, json={"password": ADMIN_PASSWORD})
while not self._stop.is_set():
try:
r = client.get(self._url)
if r.status_code == 200:
body = r.json()
if not self._is_ours(body):
self._stop.wait(self._interval_s)
continue
self._ticks.append(body)
if body["state"] in ("success", "failed", "cancelled"):
self._terminal = body
return
except Exception: # noqa: BLE001 — a blocked loop: retry
pass
self._stop.wait(self._interval_s)
self._thread = threading.Thread(target=run, daemon=True)
self._thread.start()
def stop(self, timeout_s: float = 300.0) -> dict[str, Any]:
deadline = time.monotonic() + timeout_s
while time.monotonic() < deadline:
if self._terminal is not None:
break
time.sleep(0.05)
self._stop.set()
if self._thread is not None:
self._thread.join(timeout=5)
assert self._terminal is not None, (
f"the recorder saw no terminal state (last: {self._ticks[-3:]})"
)
return self._terminal
@property
def ticks(self) -> list[dict[str, Any]]:
return list(self._ticks)
@property
def running_ticks(self) -> list[dict[str, Any]]:
return [t for t in self._ticks if t["state"] == "running"]
def _serve_statuses(page: Page, stages: dict[str, Any]) -> None:
"""Route the two job-status endpoints to the CURRENT stage body (the house
``page.route`` pattern): ``stages`` is mutated by the test between the UI
waits, so each 2 s poll of the REAL page renders exactly the payload the
test put in front of it — the client's own decision points, pinned without
a timing race."""
def handle(route: Any) -> None:
path = route.request.url
if "/api/sync/status" in path:
body = stages.get("sync")
elif "/api/git-sources/upload/status" in path:
body = stages.get("upload")
else: # pragma: no cover — only the two patterns are routed
route.fallback()
return
if body is None:
route.fallback()
return
route.fulfill(status=200, content_type="application/json", body=json.dumps(body))
page.route("**/api/sync/status", handle)
page.route("**/api/git-sources/upload/status", handle)
def _sync_status(**overrides: Any) -> dict[str, Any]:
"""One ``GET /api/sync/status`` body with the endpoint's CURRENT key set
(``EXPECTED_STATUS_KEYS``) — the served stubs stay faithful to the payload
shape; only the values are the test's."""
body: dict[str, Any] = {
"state": "idle",
"started_at": None,
"finished_at": None,
"current_file": None,
"files_done": 0,
"files_total": 0,
"scan_done": 0,
"scan_total": 0,
"phase": None,
"current_summary": None,
"summaries_done": 0,
"summaries_total": 0,
"current_chunk": 0,
"chunks_total": 0,
"images_done": 0,
"images_total": 0,
"detail": {},
"error": None,
"rss_mb": None,
"rss_peak_mb": None,
}
assert set(body) == EXPECTED_STATUS_KEYS, set(body) ^ EXPECTED_STATUS_KEYS
body.update(overrides)
return body
def _upload_status(**overrides: Any) -> dict[str, Any]:
body: dict[str, Any] = {
"state": "idle",
"started_at": None,
"finished_at": None,
"current_file": None,
"files_done": 0,
"files_total": 0,
"detail": {},
"error": None,
}
assert set(body) == UPLOAD_STATUS_KEYS, set(body) ^ UPLOAD_STATUS_KEYS
body.update(overrides)
return body
def _wait_result_line(page: Page, expected: str) -> str:
"""Wait for the PAGE (not the recorder) to settle on its terminal counts
line, and hand it back for the byte-shape assertions. The tight-polling
recorder sees the server's terminal one to two ticks BEFORE the 2 s UI
poll does — reading the box right after ``recorder.stop()`` would race the
page's own settle (the phase-64 'never stale' contract is what this waits
for, so the wait is the pin)."""
loc = page.locator("#sync-progress-text")
expect(loc).to_have_text(counts_line(expected), timeout=SETTLE_TIMEOUT_MS)
return loc.inner_text()
def _admin_sources_page(page: Page, base_url: str, *, settled: bool = True) -> None:
"""Real form login landing on the Sources page (the button's home).
``settled=True`` also waits for the boot re-attach to finish in a
NON-running state; the served-status arms pass ``settled=False`` because
their boot fetch IS the state under test."""
login(page, base_url)
expect(page).to_have_url(base_url + SOURCES_URL, timeout=30_000)
expect(page.locator("#sync-btn")).to_be_visible(timeout=30_000)
if settled:
expect(page.locator("#sync-label")).to_have_text(
re.compile(r"^(Sync sources|Synced \d{1,2}:\d{2})$"), timeout=30_000
)
def _assert_machine(
ticks: list[dict[str, Any]],
terminal: dict[str, Any],
*,
scan_total: int,
files_total: int,
scan_pair_survives_terminal: bool = True,
) -> None:
"""The phase-194 machine, read off the endpoint the UI polls — every
claim deterministic on a real run (none depends on a tick landing inside
the scan window):
* the payload is ADDITIVE — exactly :data:`EXPECTED_STATUS_KEYS` on every
body, running and terminal alike;
* the null prelude reports the scan pair as ``0/0`` (the run has not
reached ``import_sources`` yet);
* a tick caught in ``phase == "scan"`` names NO file, carries the scan's
own denominator and a position that only climbs, and reports no
file-work denominator yet (``files_total == 0`` — the flip happens on
the first ``progress`` call);
* ONE-DIRECTIONAL: no scan tick follows a tick that already counted;
* ``files_total`` never exceeds the scan's denominator (the count and its
denominator stay on the SAME predicted set);
* the terminal CLEARS ``phase`` and KEEPS both pairs side by side (the
phase-64 keep-final-values convention) — on the ``0`` rollback the kept
scan pair is ``0/0``, because that run scans nothing."""
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)}"
)
assert set(terminal) == EXPECTED_STATUS_KEYS, terminal
for tick in ticks:
if tick["state"] == "running" and tick["phase"] is None:
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}"
)
if tick["scan_total"] == 0:
# The scan's OWN first frame (or the degenerate empty walk): the
# phase is set the moment ``import_sources`` is entered and the
# pair lands with the first position, so a tick here legitimately
# carries 0/0 — exactly the payload the UI's ``scan_total === 0``
# guard renders as the bare "Scanning…".
assert tick["scan_done"] == 0, tick
continue
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: {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 "
f"machine ran backwards: ticks={[(t['phase'], t['files_total']) for t in ticks]}"
)
assert terminal["phase"] is None, terminal
if scan_pair_survives_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
if scan_total > 0:
# The count and its denominator stay on the SAME predicted set — the
# work can never exceed what the scan walked. Meaningless on the
# ``BOR_SYNC_PENDING_COUNT=0`` rollback, which scans nothing and whose
# denominator IS the walk.
assert terminal["files_total"] <= terminal["scan_total"], terminal
# ---------------------------------------------------------------------------
# 1. (arm a) The first sync: pending == walk, and the machine completes
# ---------------------------------------------------------------------------
def test_first_sync_counts_the_predicted_set_and_the_machine_completes(
page: Page, app_url: str, db_ready: None
) -> None:
"""A FRESH knowledge base: the predicted change set IS the walk, so the
phase-193 arithmetic survives UNEDITED (the reason the phase-193 fixture
never saw the defect) and the machine completes its full sequence."""
page.set_default_timeout(SETTLE_TIMEOUT_MS)
_admin_sources_page(page, app_url)
_install_recorder(page)
recorder = _TickRecorder(app_url, "/api/sync/status")
recorder.start()
btn = page.locator("#sync-btn")
btn.click()
expect(btn).to_be_disabled()
expect(page.locator("#sync-progress")).to_have_text("Syncing…")
# The FINAL file: the main part names it and the count part is GONE (A1).
# ONE live wait per arm on the state that ENDS the import phase (every
# window here is ≫ the 2 s poll, so it cannot be missed); the rest of the
# sequence is read off the in-browser recorder, which captured every value
# the ONE writer landed — a per-label live wait would race its own poll.
expect(page.locator("#sync-progress-text")).to_have_text(
re.compile(
rf"^Syncing… {re.escape(SOURCE)}/{re.escape(LAST_REL)}"
r"(?! \d+ remaining)(?: · chunk \d+/\d+)?$"
),
timeout=SETTLE_TIMEOUT_MS,
)
expect(page.locator("#sync-label")).to_have_text(SYNCED_LABEL, timeout=SETTLE_TIMEOUT_MS)
terminal = recorder.stop()
assert terminal["state"] == "success", terminal
frames = _frames(page)
texts = _texts(frames)
walk_map = _walk_positions(tuple(REL_ORDER))
# --- the rendered import frames ------------------------------------
import_frames = [f for f in frames if f["text"].startswith("Syncing… ")]
assert len(import_frames) >= 4, f"too few rendered import labels: {texts[:8]}"
for frame in import_frames:
assert IMPORT_LABEL_RE.match(frame["text"]), frame
assert not OLD_FRACTION_RE.search(frame["text"]), frame
rel = _file_of(frame["text"])
expected = walk_map.get(rel or "")
assert expected is not None, f"the label named an unknown file: {frame}"
done, total = expected
# The cross-layer invariant, per FILE (not per sample): the rendered
# N is exactly what this file's pair subtracts to.
assert _count_of(frame["text"]) == (total - done or None), (frame, expected)
# --- the count-down --------------------------------------------------
observed = [
n
for n in (_count_of(t) for t in texts if t.startswith("Syncing… "))
if n is not None
]
assert len(observed) >= 3, f"the label never visibly counted down: {observed}"
assert observed == sorted(observed, reverse=True), f"not a count-DOWN: {observed}"
assert observed[0] == MAX_COUNT, f"the walk did not start at N − 1: {observed}"
assert observed[-1] == 1, f"the count part never reached the last file: {observed}"
assert 0 not in observed, f"a count-bearing label showed 0 remaining: {observed}"
# The FIRST file's frame: the pending-count part at N − 1 AND the
# phase-129 chunk part, coexisting in ONE label.
first_frames = [
t
for t in texts
if t.startswith(f"Syncing… {SOURCE}/{FIRST_REL} {MAX_COUNT} remaining")
]
assert first_frames, f"the first file never rendered {MAX_COUNT} remaining: {texts[:8]}"
# --- the chunk part coexists (phase 129 unbroken) --------------------
both = [t for t in texts if " remaining" in t and " · chunk " in t]
assert both, f"the count part and the chunk part never coexisted: {texts[:8]}"
for label in both:
m = re.search(r" \d+ remaining · chunk (\d+)/(\d+)$", label)
assert m is not None, label
assert int(m.group(2)) == _expected_content_chunks(str(_file_of(label))), label
# --- the scan branch, if the 2 s poll landed inside it ---------------
scan_frames = [f for f in frames if f["text"].startswith("Scanning…")]
import_at = [i for i, f in enumerate(frames) if f["text"].startswith("Syncing… ")]
for frame in scan_frames:
assert SCAN_LABEL_RE.fullmatch(frame["text"]), frame
assert " remaining" not in frame["text"], frame
assert "/" not in frame["text"], frame
if scan_frames and import_at:
assert max(i for i, f in enumerate(frames) if f["text"].startswith("Scanning…")) < (
import_at[0]
), f"a scan label followed an import label: {texts}"
_assert_a4(frames)
# --- the machine, off the endpoint the page polls --------------------
_assert_machine(recorder.ticks, terminal, scan_total=N_FILES, files_total=N_FILES)
for tick in recorder.running_ticks:
if tick["phase"] == "import" and tick["current_file"]:
rel = str(tick["current_file"]).removeprefix(f"{SOURCE}/")
assert (tick["files_done"], tick["files_total"]) == walk_map[rel], tick
assert tick["files_total"] == N_FILES, tick
# --- the terminal -----------------------------------------------------
# The counts line is the byte-identical phase-64 one (the pair below it
# changed meaning; this did not) + the phase-187 clause, TERMINAL-only.
assert terminal["detail"]["added"] == N_FILES, terminal
assert terminal["detail"]["updated"] == 0, terminal
box = page.locator("#sync-progress")
line = _wait_result_line(page, f"{N_FILES} added")
expect(box).to_be_visible()
assert box.locator("> *").count() == 1
expect(box).to_have_attribute("role", "status")
expect(box).to_have_attribute("aria-live", "polite")
if MEMORY_KNOWABLE:
assert terminal["rss_peak_mb"] is not None, terminal
m = PEAK_CLAUSE_RE.search(line)
assert m is not None, line
assert int(m.group(1)) == terminal["rss_peak_mb"], (line, terminal)
else:
assert not PEAK_CLAUSE_RE.search(line), line
# ---------------------------------------------------------------------------
# 2. (arm b) The owner's production case: K of N changed
# ---------------------------------------------------------------------------
def test_incremental_sync_counts_only_the_changed_files(
page: Page, app_url: str, db_ready: None, pending_repo: Path
) -> None:
"""THE phase's heart, in the browser. The KB already holds all 8 documents
and TWO files changed: every tick of the running sync answers the
PREDICTED denominator, so the label reads ``2 remaining`` — and holds
there while ``current_file`` walks over the unchanged ones — instead of the
walk-derived ``7``/``6``/``5`` the owner saw as "1216 remaining" on a
~10,000-file KB.
The unchanged files in this run are not idle: their stored summaries were
cleared by the seed, so each costs the phase-118 backfill (a ``lite`` call
+ its embed) — real model time, real frames, ZERO pending-work. That is
what makes the HOLD visible here rather than only in
``tests/integration/test_sync_pending_count.py``'s hook sequence.
"""
page.set_default_timeout(SETTLE_TIMEOUT_MS)
_seed_kb(pending_repo, null_summary_rels=HELD_RELS)
_change_files(pending_repo, CHANGED_RELS, "arm b")
expected = _pending_positions(tuple(REL_ORDER), CHANGED_RELS)
_admin_sources_page(page, app_url)
_install_recorder(page)
recorder = _TickRecorder(app_url, "/api/sync/status")
recorder.start()
btn = page.locator("#sync-btn")
btn.click()
expect(page.locator("#sync-progress")).to_have_text("Syncing…")
# The FINAL predicted file: the part is GONE while the walk still has
# files left to go (the phase-193 A1 edge, now on the PREDICTED set).
# One live wait; the held frames and the first predicted file are read off
# the recorder below (a per-label live wait races the page's own 2 s poll).
expect(page.locator("#sync-progress-text")).to_have_text(
re.compile(
rf"^Syncing… {re.escape(SOURCE)}/{re.escape(CHANGED_RELS[1])}"
r"(?! \d+ remaining)(?: · chunk \d+/\d+)?$"
),
timeout=SETTLE_TIMEOUT_MS,
)
expect(page.locator("#sync-label")).to_have_text(SYNCED_LABEL, timeout=SETTLE_TIMEOUT_MS)
terminal = recorder.stop()
assert terminal["state"] == "success", terminal
frames = _frames(page)
texts = _texts(frames)
# --- the wire: the denominator is the WORK, never the walk -----------
for tick in recorder.running_ticks:
assert tick["files_total"] in (0, len(CHANGED_RELS)), (
f"a tick answered a walk-derived denominator: {tick}"
)
if tick["current_file"]:
rel = str(tick["current_file"]).removeprefix(f"{SOURCE}/")
assert (tick["files_done"], tick["files_total"]) == expected[rel], tick
# The HOLD, on the frames the server actually answered: at least TWO
# distinct unchanged files, each named while the pair sat at (0, 2) — the
# count did NOT advance on them.
held_ticks = {
str(t["current_file"]).removeprefix(f"{SOURCE}/")
for t in recorder.running_ticks
if t["current_file"]
and (t["files_done"], t["files_total"]) == (0, len(CHANGED_RELS))
}
assert len(held_ticks) >= 2, f"the count never held over unchanged files: {held_ticks}"
assert held_ticks <= set(HELD_RELS), held_ticks
# --- the rendered hold + the hidden final part -----------------------
held_labels = [
f["text"]
for f in frames
if f["text"].startswith("Syncing… ")
and _count_of(f["text"]) == len(CHANGED_RELS)
and _file_of(f["text"]) in HELD_RELS
]
assert len(held_labels) >= 2, f"fewer than two held frames rendered: {texts[:12]}"
assert len({_file_of(t) for t in held_labels}) >= 2, held_labels
final_prefix = f"Syncing… {SOURCE}/{CHANGED_RELS[1]}"
final_frames = [f for f in frames if f["text"].startswith(final_prefix)]
assert final_frames, texts
for frame in final_frames:
assert _count_of(frame["text"]) is None, frame
first_prefix = f"Syncing… {SOURCE}/{CHANGED_RELS[0]}"
first_frames = [f for f in frames if f["text"].startswith(first_prefix)]
assert first_frames and all(_count_of(f["text"]) == 1 for f in first_frames), first_frames
# THE owner-visible regression pin: nothing in this run may show a
# walk-derived count (7/6/5/4/3 remaining are the pre-phase numbers for
# this exact fixture — see the kill-switch arm).
for label in texts:
n = _count_of(label)
assert n is None or n <= len(CHANGED_RELS), (
f"the label counted the WALK, not the work: {label}"
)
_assert_a4(frames)
# --- the machine + the terminal --------------------------------------
_assert_machine(
recorder.ticks, terminal, scan_total=N_FILES, files_total=len(CHANGED_RELS)
)
assert terminal["detail"]["updated"] == len(CHANGED_RELS), terminal
assert terminal["detail"]["unchanged"] == N_FILES - len(CHANGED_RELS), terminal
assert terminal["scan_done"] == N_FILES, terminal
assert terminal["files_done"] == len(CHANGED_RELS), terminal
_wait_result_line(
page,
f"0 added · {len(CHANGED_RELS)} updated · {N_FILES - len(CHANGED_RELS)} unchanged",
)
# ---------------------------------------------------------------------------
# 3. The frames the live run produces, rendered by the REAL client
# ---------------------------------------------------------------------------
def test_scan_frames_and_the_hold_render_from_the_exact_payloads(
page: Page, app_url: str, db_ready: None
) -> None:
"""Arms a and b, rendered. The scan window on an 8-file KB is milliseconds
and the hold frames depend on which unchanged files happen to cost model
time, so neither can be AIMED at from the browser. This arm feeds the
exact payloads the real run answers — including the real HOLD sequence the
importer's hook is pinned to in
``tests/integration/test_sync_pending_count.py``
(``[(changed,1,2), (unchanged,1,2), (unchanged,1,2), (changed,2,2)]``) —
to the real ``sources.js`` through the page's own 2 s poll, and pins what
the owner would read: the scan's own ``(n/total)``, the count holding at
``2 remaining`` while the named file changes, the part shrinking to
``1 remaining``, the part GONE on the final predicted file, and the
phase-187 clause on the TERMINAL line only."""
page.set_default_timeout(SETTLE_TIMEOUT_MS)
src = SOURCE
stages: dict[str, Any] = {"sync": None, "upload": None}
_serve_statuses(page, stages)
def show(**overrides: Any) -> None:
stages["sync"] = _sync_status(state="running", started_at=_now_iso(), **overrides)
# The scan: its OWN position, no file, no ``N remaining``.
show(phase="scan", scan_done=3, scan_total=N_FILES)
box = page.locator("#sync-progress-text")
btn = page.locator("#sync-btn")
_admin_sources_page(page, app_url, settled=False)
_install_recorder(page)
expect(box).to_have_text(f"Scanning… (3/{N_FILES})", timeout=SETTLE_TIMEOUT_MS)
expect(btn).to_have_attribute("title", f"Scanning… (3/{N_FILES})")
expect(btn).to_have_attribute("aria-label", f"Scanning… (3/{N_FILES})")
# 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 ONE writer's ``hidden`` flag plus the span's text
# (the in-browser recorder reads the same property).
expect(page.locator(".sync-label-count")).to_have_text(f"(3/{N_FILES})")
expect(page.locator(".sync-label-count")).to_have_js_property("hidden", False)
expect(btn).to_be_disabled()
# The scan climbs: the SAME label slot, the SAME branch, a new position.
show(phase="scan", scan_done=8, scan_total=N_FILES)
expect(box).to_have_text(f"Scanning… ({N_FILES}/{N_FILES})", timeout=SETTLE_TIMEOUT_MS)
# …and the empty-KB / first-frame guard: 0/0 renders the BARE label.
show(phase="scan", scan_done=0, scan_total=0)
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…")
# The null prelude (the model check + clone/pull span): bare ``Syncing…``.
show(phase=None, files_done=0, files_total=0)
expect(box).to_have_text("Syncing…", timeout=SETTLE_TIMEOUT_MS)
# THE HOLD: three unchanged files, one pair. The count does not move.
_install_recorder(page)
for rel in HELD_RELS:
show(phase="import", current_file=f"{src}/{rel}", files_done=0, files_total=2)
expect(box).to_have_text(
f"Syncing… {src}/{rel} 2 remaining", timeout=SETTLE_TIMEOUT_MS
)
# The first predicted file — with the phase-129 chunk part alongside.
show(
phase="import",
current_file=f"{src}/{CHANGED_RELS[0]}",
files_done=1,
files_total=2,
current_chunk=2,
chunks_total=3,
)
expect(box).to_have_text(
f"Syncing… {src}/{CHANGED_RELS[0]} 1 remaining · chunk 2/3",
timeout=SETTLE_TIMEOUT_MS,
)
# The final predicted file: the part is GONE, the file stays.
show(phase="import", current_file=f"{src}/{CHANGED_RELS[1]}", files_done=2, files_total=2)
expect(box).to_have_text(f"Syncing… {src}/{CHANGED_RELS[1]}", 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)
# The terminal of the same run: the byte-identical counts line, and the
# ONLY place the phase-187 clause appears (the running frames above
# carried rss values and rendered NONE — AGENTS rule 10, terminal-only).
stages["sync"] = _sync_status(
state="success",
started_at=_now_iso(),
finished_at=_now_iso(),
current_file=f"{src}/{CHANGED_RELS[1]}",
files_done=2,
files_total=2,
scan_done=N_FILES,
scan_total=N_FILES,
detail={"added": 0, "updated": 2, "unchanged": N_FILES - 2},
rss_mb=512,
rss_peak_mb=1234,
)
expect(box).to_have_text("0 added · 2 updated · 6 unchanged · peak 1234 MB")
expect(page.locator("#sync-label")).to_have_text(SYNCED_LABEL)
expect(btn).to_be_enabled()
texts = _texts(_frames(page))
scan_labels = [t for t in texts if t.startswith("Scanning…")]
assert scan_labels, texts
for label in scan_labels:
assert SCAN_LABEL_RE.fullmatch(label), label
syncing = [t for t in texts if t.startswith("Syncing…")]
assert syncing, texts
for label in syncing:
assert IMPORT_LABEL_RE.match(label), label
# The pre-phase shape stays dead on this part (the scan's own
# ``(n/total)`` lives only behind the word "Scanning").
assert not OLD_FRACTION_RE.search(label), label
held = [t for t in syncing if _count_of(t) == 2]
assert len(held) >= 2, held
assert len({_file_of(t) for t in held}) >= 2, held
# The clause NEVER appears on a running label — scan or import — and the
# terminal's is text on the line that already exists (one child span).
for label in scan_labels + syncing:
assert "peak" not in label, label
assert page.locator("#sync-progress > *").count() == 1
# ---------------------------------------------------------------------------
# 4. (arm d) BOR_SYNC_PENDING_COUNT=0 — the phase-193 rollback, live
# ---------------------------------------------------------------------------
def test_kill_switch_zero_restores_the_walk_position_count(
page: Page, zero_server: str, db_ready: None, zero_repo: Path
) -> None:
"""The second module app, same fixture, ``BOR_SYNC_PENDING_COUNT=0``: the
exact phase-193 machine and the exact phase-193 numbers.
The contrast IS the phase. For the file at walk position 5 of 8 this app
renders ``3 remaining`` (``8 − 5`` — files that will never be re-indexed
counted as work) while the default app, on the same repository shape and
the same moment of the same walk, renders ``1 remaining``. And no tick of
this run ever reports ``phase: "scan"``: the pre-scan does not run, so the
scan pair is ``0/0`` for its whole life, terminal included."""
page.set_default_timeout(SETTLE_TIMEOUT_MS)
_seed_kb(zero_repo, null_summary_rels=HELD_RELS)
_change_files(zero_repo, ZERO_CHANGED_RELS, "arm d")
walk_map = _walk_positions(tuple(REL_ORDER))
_admin_sources_page(page, zero_server)
_install_recorder(page)
recorder = _TickRecorder(zero_server, "/api/sync/status")
recorder.start()
page.locator("#sync-btn").click()
expect(page.locator("#sync-progress")).to_have_text("Syncing…")
# The LAST walked file (changed here) is the only one that hides the part
# under the walk semantics.
expect(page.locator("#sync-progress-text")).to_have_text(
re.compile(
rf"^Syncing… pendingzero/{re.escape(LAST_REL)}"
r"(?! \d+ remaining)(?: · chunk \d+/\d+)?$"
),
timeout=SETTLE_TIMEOUT_MS,
)
expect(page.locator("#sync-label")).to_have_text(SYNCED_LABEL, timeout=SETTLE_TIMEOUT_MS)
terminal = recorder.stop()
assert terminal["state"] == "success", terminal
# No scan phase exists on this app, at all, ever.
assert [t for t in recorder.ticks if t["phase"] == "scan"] == [], recorder.ticks
for tick in recorder.ticks:
assert (tick["scan_done"], tick["scan_total"]) == (0, 0), tick
assert terminal["phase"] is None, terminal
assert (terminal["scan_done"], terminal["scan_total"]) == (0, 0), terminal
# The pair is the WALK: every named tick carries the 1-based walk position
# over all 8 importable files, and the denominator is the whole KB.
named = [t for t in recorder.running_ticks if t["current_file"]]
assert named, "the run never reported a file at all"
for tick in named:
rel = str(tick["current_file"]).removeprefix("pendingzero/")
assert (tick["files_done"], tick["files_total"]) == walk_map[rel], tick
assert tick["files_total"] == N_FILES, tick
assert terminal["files_done"] == N_FILES, terminal
assert terminal["files_total"] == N_FILES, terminal
assert terminal["detail"]["updated"] == len(ZERO_CHANGED_RELS), terminal
_assert_machine(recorder.ticks, terminal, scan_total=0, files_total=N_FILES)
# The rollback's OWN defect, rendered: a walk-derived count (the files in
# front of the first changed one still counted). Arm b pins the number
# this fixture produces with the switch on: ``2 remaining``.
frames = _frames(page)
texts = _texts(frames)
walk_counts = [
n
for t in texts
if t.startswith("Syncing… ")
and _file_of(t) in HELD_RELS
and (n := _count_of(t)) is not None
]
assert walk_counts, f"no unchanged file rendered a walk-derived count: {texts[:12]}"
assert max(walk_counts) > len(ZERO_CHANGED_RELS), (
f"the rollback app counted the WORK, not the walk: {walk_counts}"
)
for frame in frames:
if frame["text"].startswith("Scanning…"):
raise AssertionError(f"the rollback app rendered a scan label: {frame}")
_assert_a4(frames)
_wait_result_line(
page,
f"0 added · {len(ZERO_CHANGED_RELS)} updated "
f"· {N_FILES - len(ZERO_CHANGED_RELS)} unchanged",
)
# ---------------------------------------------------------------------------
# 5. (arm c) The no-change re-sync: an empty predicted set, no count part
# ---------------------------------------------------------------------------
def test_no_change_sync_reports_no_denominator(
page: Page, app_url: str, db_ready: None, pending_repo: Path
) -> None:
"""The re-sync of a repository nothing touched: the scan walks all 8 files
and predicts NONE of them, so every tick answers ``files_total == 0`` —
the phase-64 ``total === 0`` guard driven by zero WORK rather than by a
missing denominator — and no label EVER carries a count part. The
pre-phase label on this exact run read ``8 remaining``, ``7 remaining``…
while nothing was going to be re-indexed.
A no-change run makes no import LLM call, so its import window is
milliseconds and it renders no per-file label: the wire pair (``0/0`` on
every tick) plus test 3's ``total === 0`` guard render the owner-visible
half deterministically."""
page.set_default_timeout(SETTLE_TIMEOUT_MS)
_seed_kb(pending_repo)
_admin_sources_page(page, app_url)
_install_recorder(page)
recorder = _TickRecorder(app_url, "/api/sync/status")
recorder.start()
btn = page.locator("#sync-btn")
btn.click()
# The click's OWN state first (phase 64): the app's in-memory status still
# carries the PREVIOUS test's terminal, so a settle wait started straight
# after the click can pass on that stale "Synced HH:MM" instead of on this
# run. The spinner + the bare live label are THIS run's first frame.
expect(btn).to_be_disabled()
expect(page.locator("#sync-progress")).to_have_text("Syncing…")
expect(page.locator("#sync-label")).to_have_text(SYNCED_LABEL, timeout=SETTLE_TIMEOUT_MS)
terminal = recorder.stop()
assert terminal["state"] == "success", terminal
for tick in recorder.running_ticks:
assert (tick["files_done"], tick["files_total"]) == (0, 0), (
f"a no-change run reported work to do: {tick}"
)
_assert_machine(recorder.ticks, terminal, scan_total=N_FILES, files_total=0)
# The scan DID walk the whole KB — the divergence (8 scanned, 0 to
# re-index) kept at the terminal, beside the untouched counts line.
assert terminal["scan_done"] == N_FILES, terminal
assert terminal["scan_total"] == N_FILES, terminal
assert terminal["detail"]["unchanged"] == N_FILES, terminal
texts = _texts(_frames(page))
for label in texts:
assert _count_of(label) is None, (
f"a no-change run rendered a count part: {label}"
)
if label.startswith("Syncing…"):
assert not OLD_FRACTION_RE.search(label), label
_wait_result_line(page, f"0 added · {N_FILES} unchanged")
# ---------------------------------------------------------------------------
# 6. (arm e) The empty source: the degenerate D1 pin
# ---------------------------------------------------------------------------
def _register_local_source(base_url: str, path: Path) -> None:
"""Store one ``kind=local`` source row through the admin API (the
phase-49 contract the Sources page's form used to wrap): a stored row IS
the effective source, which is what lets the degenerate and prod-shaped
arms run on the module's own app instead of a third boot."""
with httpx.Client(base_url=base_url, timeout=30.0) as client:
r = client.post("/api/login", json={"password": ADMIN_PASSWORD})
assert r.status_code == 204, r.text
r = client.post("/api/git-sources", json={"kind": "local", "path": str(path)})
assert r.status_code == 201, f"{r.status_code}: {r.text}"
def test_empty_source_stays_in_scan_with_no_count_part(
page: Page, app_url: str, db_ready: None, empty_kb_dir: Path
) -> None:
"""A source with ZERO importable files: the walk yields nothing, so no
``progress`` call exists to flip the phase — the machine goes
``null`` → ``"scan"`` → terminal and NEVER says ``"import"``, both pairs
stay ``0/0``, and the page renders no count part anywhere (the same
``scan_total === 0`` guard test 3 pins on the exact payload).
The scan window of an empty walk is a handful of milliseconds, so the
live pins are the deterministic ones (no import tick exists to catch, and
the terminal keeps the zeroed pair); the server-side degenerate is
``tests/integration/test_sync_scan_phase.py``'s empty-KB arm."""
page.set_default_timeout(SETTLE_TIMEOUT_MS)
_register_local_source(app_url, empty_kb_dir)
_admin_sources_page(page, app_url)
_install_recorder(page)
recorder = _TickRecorder(app_url, "/api/sync/status", interval_s=0.02)
recorder.start()
btn = page.locator("#sync-btn")
btn.click()
# The click's OWN state first (phase 64): the app's in-memory status still
# carries the PREVIOUS test's terminal, so a settle wait started straight
# after the click can pass on that stale "Synced HH:MM" instead of on this
# run. The spinner + the bare live label are THIS run's first frame.
expect(btn).to_be_disabled()
expect(page.locator("#sync-progress")).to_have_text("Syncing…")
expect(page.locator("#sync-label")).to_have_text(SYNCED_LABEL, timeout=SETTLE_TIMEOUT_MS)
terminal = recorder.stop()
assert terminal["state"] == "success", terminal
assert [t for t in recorder.ticks if t["phase"] == "import"] == [], (
f"an empty walk flipped to import: {recorder.ticks}"
)
for tick in recorder.ticks:
assert (tick["scan_done"], tick["scan_total"]) == (0, 0), tick
assert (tick["files_done"], tick["files_total"]) == (0, 0), tick
_assert_machine(recorder.ticks, terminal, scan_total=0, files_total=0)
assert terminal["detail"] == {} or not any(
terminal["detail"].get(k)
for k in ("added", "updated", "pruned", "errors")
), terminal
texts = _texts(_frames(page))
for label in texts:
assert _count_of(label) is None, f"the empty run rendered a count part: {label}"
if label.startswith("Scanning…"):
assert label == "Scanning…", label
_wait_result_line(page, "0 added · 0 unchanged")
# ---------------------------------------------------------------------------
# 7. The owner's report at scale: a 1,000-file scan, two files of work
# ---------------------------------------------------------------------------
def test_prod_shaped_run_scan_climbs_then_counts_two(
page: Page, app_url: str, db_ready: None, wide_kb: Path
) -> None:
"""The report itself, in a browser. A 1,000-file KB, TWO files changed:
the pre-scan's OWN position climbs across several distinct values while
the server keeps answering (the scan runs off the loop — the phase-179
contract, which is exactly why this poll is served THROUGH it), the import
then reports the PREDICTED denominator (``files_total == 2`` against
``scan_total == 1000``: the divergence the pre-phase shape could not
express), and the terminal keeps BOTH pairs side by side.
This is also the arm that makes the scan's ``(n/total)`` visible for real:
eight tiny files pre-scan in single-digit milliseconds, a thousand take
long enough for the position to be observed moving. The ``N remaining``
part never exceeds the work — the owner's ``1216 remaining`` is the bug."""
page.set_default_timeout(SETTLE_TIMEOUT_MS)
_seed_kb(wide_kb, git=False)
for rel in WIDE_CHANGED_RELS:
path = wide_kb / rel
path.write_text(
path.read_text(encoding="utf-8") + f"\narm f edit of {rel}\n", encoding="utf-8"
)
_register_local_source(app_url, wide_kb)
_admin_sources_page(page, app_url)
_install_recorder(page)
recorder = _TickRecorder(app_url, "/api/sync/status", interval_s=0.02)
recorder.start()
btn = page.locator("#sync-btn")
btn.click()
# The click's OWN state first (phase 64): the app's in-memory status still
# carries the PREVIOUS test's terminal, so a settle wait started straight
# after the click can pass on that stale "Synced HH:MM" instead of on this
# run. The spinner + the bare live label are THIS run's first frame.
expect(btn).to_be_disabled()
expect(page.locator("#sync-progress")).to_have_text("Syncing…")
expect(page.locator("#sync-label")).to_have_text(SYNCED_LABEL, timeout=SETTLE_TIMEOUT_MS)
terminal = recorder.stop()
assert terminal["state"] == "success", terminal
scan_ticks = [t for t in recorder.ticks if t["phase"] == "scan"]
positions = sorted({t["scan_done"] for t in scan_ticks})
assert len(positions) >= 2, (
"the scan's position was never observably climbing — the pre-scan is "
f"the phase's own progress surface: {positions} / {WIDE_FILES}"
)
assert positions[-1] > positions[0], positions
for tick in scan_ticks:
# 0/0 is legal here (the scan's own first frame — the phase is set
# before the first position is reported); every REPORTED denominator is
# the whole importable walk.
assert tick["scan_total"] in (0, WIDE_FILES), tick
assert tick["current_file"] is None, tick
assert (tick["files_done"], tick["files_total"]) == (0, 0), tick
# The scan is answered THROUGH the running server: the poll kept getting
# 200s while the pre-scan hashed and compared a thousand files.
assert len(scan_ticks) >= 2, f"the poll starved during the scan: {len(scan_ticks)}"
# The import: the WORK is the denominator, and it is never the walk.
import_ticks = [t for t in recorder.running_ticks if t["phase"] == "import"]
assert import_ticks, "the run never flipped to import"
for tick in import_ticks:
assert tick["files_total"] == len(WIDE_CHANGED_RELS), tick
assert [
t for t in recorder.ticks if t["files_total"] > len(WIDE_CHANGED_RELS)
] == [], (
"the running count reported a WALK denominator on a 1,000-file KB — "
"the owner's exact report"
)
_assert_machine(
recorder.ticks, terminal, scan_total=WIDE_FILES, files_total=len(WIDE_CHANGED_RELS)
)
assert terminal["detail"]["updated"] == len(WIDE_CHANGED_RELS), terminal
assert terminal["detail"]["unchanged"] == WIDE_FILES - len(WIDE_CHANGED_RELS), terminal
# The owner-visible half: no label ever says a walk-sized number remains,
# and a scan label — if the 2 s poll landed in that window — is the honest
# ``Scanning… (n/1000)`` and nothing else.
texts = _texts(_frames(page))
for label in texts:
n = _count_of(label)
assert n is None or n <= len(WIDE_CHANGED_RELS), (
f"a label counted the walk instead of the work: {label}"
)
if label.startswith("Scanning…"):
assert SCAN_LABEL_RE.fullmatch(label), label
assert str(WIDE_FILES) in label, label
_wait_result_line(
page,
f"0 added · {len(WIDE_CHANGED_RELS)} updated "
f"· {WIDE_FILES - len(WIDE_CHANGED_RELS)} unchanged",
)
# ---------------------------------------------------------------------------
# 8. The upload surface: phase 90's "no sync fields", plus the scan pair
# ---------------------------------------------------------------------------
def test_upload_run_carries_no_scan_fields_and_no_count_part(
page: Page, app_url: str, db_ready: None, upload_archive: Path
) -> None:
"""Phase 194 gives the upload job NOTHING — and this is the pin that it
did not leak. The REAL background upload's live status answers the bare
phase-64 key set: no ``phase``, no chunk / image / summary fields, no
memory pair and no ``scan_done`` / ``scan_total``, with ``null``/``0``/``0``
for the whole run (unpack + register, phase 90 A1/A2). Rendered through
the ONE shared builder, that payload is a BARE ``Importing`` — no file and
no count part."""
page.set_default_timeout(SETTLE_TIMEOUT_MS)
with httpx.Client(base_url=app_url, timeout=60.0) as client:
r = client.post("/api/login", json={"password": ADMIN_PASSWORD})
assert r.status_code == 204, r.text
with upload_archive.open("rb") as fh:
r = client.post(
"/api/git-sources/upload",
files={"file": (f"{UPLOAD_NAME}.tar.gz", fh, "application/gzip")},
)
assert r.status_code == 202, r.text
seen: list[dict[str, Any]] = []
body: dict[str, Any] = {}
for _ in range(200):
body = client.get("/api/git-sources/upload/status").json()
seen.append(body)
if body["state"] in ("success", "failed"):
break
time.sleep(0.05)
assert body["state"] == "success", body
for tick in seen:
assert set(tick) == UPLOAD_STATUS_KEYS, (
f"the upload status grew sync fields: {sorted(set(tick) ^ UPLOAD_STATUS_KEYS)}"
)
assert tick["current_file"] is None, tick
assert tick["files_done"] == 0 and tick["files_total"] == 0, tick
# The shared label, on the page's own poll: the upload's REAL payload is
# the bare prefix — no file, no count part (the part needs a denominator
# the upload job never reports).
stages: dict[str, Any] = {
"sync": _sync_status(state="idle"),
"upload": _upload_status(state="running", started_at=_now_iso()),
}
_serve_statuses(page, stages)
_admin_sources_page(page, app_url, settled=False)
_install_recorder(page)
box = page.locator("#sync-progress-text")
btn = page.locator("#sync-btn")
expect(box).to_have_text("Importing", timeout=SETTLE_TIMEOUT_MS)
expect(btn).to_have_attribute("title", "Importing")
expect(page.locator(".sync-label-count")).to_have_text("")
expect(page.locator(".sync-label-count")).to_have_js_property("hidden", True)
expect(btn).to_be_disabled()
stages["upload"] = _upload_status(
state="success", detail={"message": "uploaded"}, finished_at=_now_iso()
)
expect(page.locator("#sync-label")).to_have_text("Sync sources", timeout=SETTLE_TIMEOUT_MS)
expect(page.locator("#sync-progress")).to_be_hidden(timeout=SETTLE_TIMEOUT_MS)
texts = _texts(_frames(page))
importing = [t for t in texts if t.startswith("Importing")]
assert importing, texts
for label in importing:
assert _count_of(label) is None, label
assert not OLD_FRACTION_RE.search(label), label
assert "Scanning" not in label and "(" not in label, label