Files
brain-of-reese/tests/e2e/test_force_push_resync.py
T
ducoterra c054b47df5
Build and Push Containers / build-and-push-app (push) Successful in 1m47s
Build and Push Containers / build-and-push-db (push) Successful in 13s
phase: 187_sync_peak_memory_line
**Phase 187 — final verification pass: all criteria met, repo 100% green.**

Verified (all four tasks already in `complete/`)
- `app/core/memory.py`: `rss_mb` / `rss_peak_mb` / `cgroup_peak_mb` / `fmt_mb`, pure stdlib, never-raise, **100%** coverage, 24 branch tests.
- Lifecycle lines append-only: `sync: started … rss_mb=`, all three terminals `… rss_mb= rss_peak_mb=` (failed = sanitized one-liner beside the surviving `logger.exception`), `import: summary … rss_mb=`; prefixes byte-identical.
- `GET /api/sync/status`: additive `rss_mb`/`rss_peak_mb` (`int|null`), keyed off the ONE `_sample_rss` (run start, phase transitions, forced terminals, 5 s-throttled hook), `peak >= current` invariant, anonymous 403.
- `sources.js` `fmtSyncPeakClause`: ` · peak <N> MB` trailing the counts line / "Sync cancelled", nothing when null, no new control/live region.

Gates (exact commands)
- `uv run pytest --cov=app --cov-report=term-missing` → **4570 passed**, TOTAL **99%**, `app/api/sync.py` **100%**, `app/core/memory.py` **100%**
- `uv run ruff check .` → clean · `uv run pyright` → **0 errors**
- `uv run pytest tests/e2e/test_sync_peak_memory_visible.py -v --no-cov` → **5 passed in isolation** (ran twice)
- All 27 touched E2E families re-run **in isolation**, all green (progress_box 8, cancel 3, status_freshness 4, summary_visibility 3, sync_button 3, session_reset 4, git_freeze 2, git_log_stream_dates 5, PDF/image suites, …)

Completion criteria: every checkbox ✅ except the commit + phase-dir move, which belong to the harness.

Documentation
- README reviewed in full (pages, commands, config tables cross-checked against `app/config.py` — 85 env rows, all defaults match; phase-187 ops section, greps, process-vs-cgroup caveat): consistent, no changes needed.
- `AGENTS.md`: **fixed one pre-existing inconsistency** — rule 6 claimed the no-CDN test covers "the index page"; it actually parametrizes 10 pages (all but `/doc-edit.html`).

Notable
- Two failures early in the pass were self-inflicted by running the E2E module concurrently with the full suite on the shared Postgres (TRUNCATE deadlock + one order-dependent `test_overview` flake); both vanish under the documented serial isolation — no code defect, no change made.
- **PLAN §9 still needs the owner's reconciliation** (phase 187 adds the sync memory fields + two status keys; PLAN is never agent-edited — AGENTS rule 10 carries the spec). `TODO.md` holds an unrelated owner note, left untouched.

Next pending phase: `188_chunks_hnsw_index`.
2026-10-16 15:22:51 -04:00

591 lines
25 KiB
Python

"""Phase 136 (task 02) E2E (Playwright): a force push no longer breaks
the sync — the checkout resets to the head of the tracked branch.
Source: ``TODO.md:5`` — the owner's verbatim repro: "Force pushes
break brain of reese git pull. Brain of reese should always have the
head of the branch specified, so if pull fails due to force pushes it
should just reset origin/<branch>."
Run in isolation (DB must be up: ``podman compose up -d db``; git on
PATH — a documented environment prerequisite, phase 28):
uv run pytest tests/e2e/test_force_push_resync.py -v --no-cov
The owner's exact flow is pinned end to end: sync (clone + import) →
force-push the origin (its history rewritten to an UNRELATED root
commit) → sync again → SUCCESS, and the KB holds the NEW head's
content. Pre-phase, that second sync landed ``failed``: the legacy
``git pull --ff-only`` (the ``branch=None`` env row) refused the
divergent head and the run died in the ``failed`` state with a
user-facing error. This module is the regression pin — the pin has
TEETH: against the pre-phase ``clone_or_pull`` (verified by
temporarily reverting the phase-136 change on a scratch branch, then
restoring), run 2 lands ``failed`` and the terminal-state assertion
fails, exactly as the pre-phase behavior demanded.
Per-module app env (the ``test_sync_button.py`` module-app pattern):
this module's app boots on its own port (a same-port second uvicorn
dies on bind and would drive the wrong server) with
``BOR_GIT_SOURCES=file://<fixture repo>`` (an env-fallback git row —
``branch=None``, the default-branch path the owner's repro exercises;
the registry is truncated per test, so the fallback is the effective
source list) and a dedicated ``BOR_SOURCES_DIR``. The session app (no
git sources) is never started in this isolated run, so no port clash.
The fixture (module-scoped, real ``git`` — the
``test_sync_button.sync_git_repo`` pattern): one branch (``main``),
one commit, ``notes/doc.md`` with ``Marker: RESE-FPPUSH-V1-5e8a``.
``_force_push_new_head`` rewrites the origin IN PLACE to a NEW
ORPHAN root commit (the branch ref deleted and recreated at the new
commit — full divergence, not just a new commit on top): the
``file://`` equivalent of a force push, whose OBSERVABLE contract is
that the origin's single branch now points at a commit with UNRELATED
history (no common ancestor with the old head — asserted) and the new
content. The app's next ``git fetch`` sees the divergent head.
Test → contract mapping (two tests):
1. ``test_sync_survives_a_force_push`` — the owner's exact flow,
through the UI: run 1 (clone + import: "1 added", the catalog
holds ``doc.md``) → the force push (V2) → run 2 (``state ==
"success"`` — NOT ``failed``; the UI settles to "Synced HH:MM"
with the error banner HIDDEN and no error modal — a reset is a
LOGGED event, not a user-facing failure; the ``#sync-progress``
box shows "0 added · 1 updated" (phase 150 — the single sync text
surface) and the document modal serves the V2
sentinel — the reset's content flowed through the import; the
counts are the secondary signal, the content is primary) → run 3
(idempotent: "0 added · 1 unchanged" — the checkout is stably at
the new head; the reset was not a one-off fluke, and "unchanged"
rules out drift back to V1, which would have re-reported as
"updated").
2. ``test_failed_pull_is_not_the_outcome`` — the negative pin at the
API seam (the task's PREFERRED direct-API leg, kept as its own
test rather than folded into test 1 as step 4b: it costs one
small API-only sync against the same module app — cheap while the
fixture exists — and keeps the UI test focused): a FRESH force
push (V3 — its own orphan root: test 1's checkout already sits at
V2, so a second rewrite is needed to make THIS sync the
post-force-push one) → ``POST /api/sync`` (202) + poll →
``state == "success"`` (the same contract as run 2 without the
UI), and the KB row's content carries the V3 sentinel. Purely the
status endpoint + the DB: the UI's 2 s poll loop starts only on a
click or a boot-while-running, so it does NOT adopt an
API-triggered run (the house rule — the status object, not the
request, is authoritative — the test_sync_button API-trigger
pattern asserts the endpoint, never the label).
Wall-time guard: the whole module < 120 s (app boot + four small
syncs against the mock LLM — no ``slow_llm`` proxy needed: the point
is the TERMINAL state, not a running window).
"""
from __future__ import annotations
import os
import re
import subprocess
import sys
import time
import uuid
from collections.abc import Iterator
from datetime import datetime
from pathlib import Path
from typing import Any
import pytest
from playwright.sync_api import Page, expect
from sqlalchemy import text
from app.db import SessionLocal
from e2e.auth_helpers import login
from e2e.conftest import (
ADMIN_PASSWORD,
SESSION_SECRET,
USE_REAL_LLM,
_wait_http,
)
from e2e.sync_helpers import counts_line
REPO = Path(__file__).resolve().parents[2]
# Phase 79 (task 04, full inventory): the conftest session app owns its
# port in a combined run — this module app binds its own port instead
# (a same-port second uvicorn dies on bind and would drive the wrong
# server). Env-overridable.
APP_PORT = int(os.environ.get("E2E_APP_PORT_FPPUSH", "8135"))
APP_URL = f"http://127.0.0.1:{APP_PORT}"
#: The fixture repo's directory name — the source name the import
#: records for it (the repo_name basename rule, phase 28).
FIXTURE_NAME = "fppush-fixture"
FIXTURE_DOC = "notes/doc.md"
#: The per-version sentinels inside the fixture doc — the V2/V3
#: sentinels in the KB after their force push are the reset's
#: observable end-to-end effect (the NEW head's content is what the
#: KB serves).
SENTINEL_V1 = "RESE-FPPUSH-V1-5e8a"
SENTINEL_V2 = "RESE-FPPUSH-V2-2b7f"
SENTINEL_V3 = "RESE-FPPUSH-V3-4a9d"
V1_DOC = (
"# Force-push fixture note\n"
"\n"
"One small note that exists only to pin the force-push resync end\n"
"to end: a real git clone, a real import, a force-pushed origin,\n"
"and the reset back to the head of the tracked branch.\n"
"\n"
f"Marker: {SENTINEL_V1}\n"
)
V2_DOC = (
"# Force-push fixture note (rewritten root)\n"
"\n"
"The origin's history was rewritten to a NEW orphan root commit —\n"
"the old commit is gone and the histories share no ancestor. The\n"
"next sync must reset to this head, not fail.\n"
"\n"
f"Marker: {SENTINEL_V2}\n"
)
V3_DOC = (
"# Force-push fixture note (rewritten root, second time)\n"
"\n"
"A second unrelated rewrite — the API-level resync pin needs THIS\n"
"sync to be the post-force-push one, so the origin gets a fresh\n"
"orphan root again.\n"
"\n"
f"Marker: {SENTINEL_V3}\n"
)
#: "Synced HH:MM" — the local-time last-result label (sources.js's
#: fmtSyncTime), any hour/minute.
SYNCED_LABEL = re.compile(r"Synced \d{1,2}:\d{2}")
#: The module's wall budget (the task's wall-time guard): app boot +
#: four small syncs against the mock LLM stay under 120 s. A breach is
#: a regression canary (the sync pipeline slowed), not a flake
#: tolerance.
WALL_BUDGET_S = 120.0
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
def _git(cwd: Path, *args: str) -> None:
"""Run git in *cwd*; a non-zero exit fails the fixture loudly."""
proc = subprocess.run(["git", *args], cwd=cwd, capture_output=True, text=True)
if proc.returncode != 0:
raise AssertionError(f"git {' '.join(args)} failed: {proc.stderr.strip()}")
def _rev_parse(repo: Path, *args: str) -> str:
"""``git rev-parse`` in *repo* — stripped stdout, loud on failure."""
proc = subprocess.run(
["git", "rev-parse", *args], cwd=repo, capture_output=True, text=True
)
if proc.returncode != 0:
raise AssertionError(
f"git rev-parse {' '.join(args)} failed: {proc.stderr.strip()}"
)
return proc.stdout.strip()
def _commit(repo: Path, message: str) -> None:
"""A fixture commit (never signed — the house ``commit.gpgsign``
flag, the test_sync_button.py idiom)."""
_git(
repo,
"-c", "user.email=e@x", "-c", "user.name=t",
"-c", "commit.gpgsign=false",
"commit", "-qm", message,
)
@pytest.fixture(scope="module")
def fppush_repo(tmp_path_factory: pytest.TempPathFactory) -> Path:
"""A real one-commit, one-branch git repo the sync must clone
(the ``test_sync_button.sync_git_repo`` pattern): ``main``, one
commit, ``notes/doc.md`` with ``Marker: RESE-FPPUSH-V1-5e8a``.
Built under ``tmp_path_factory`` (``tmp_path`` is function-scoped
while the module-scoped app fixture needs the repo for the
module's lifetime) via real ``git`` subprocesses. ``init -b
main`` pins the default branch NAME (the system's
``init.defaultBranch`` may differ).
"""
root = tmp_path_factory.mktemp("bor_fppush")
repo = root / FIXTURE_NAME
(repo / "notes").mkdir(parents=True)
(repo / "notes" / "doc.md").write_text(V1_DOC, encoding="utf-8")
_git(repo, "init", "-q", "-b", "main")
_git(repo, "add", "-A")
_commit(repo, "one")
assert (repo / ".git").is_dir()
return repo
@pytest.fixture(scope="module")
def app_server(mock_llm: int, fppush_repo: Path) -> Iterator[str]:
"""The real app under test — per-module env: the sync's subject is
a real ``file://`` git source (env-fallback row — ``branch=None``,
the default-branch path the owner's repro exercises) with its own
checkout dir (the conftest session app boots without
``BOR_GIT_SOURCES`` and is never started in this isolated run)."""
env = dict(os.environ)
env.pop("DEBUGPY", None)
env["BOR_ENVIRONMENT"] = "e2e"
env["BOR_STATIC_DIR"] = str(REPO / "frontend")
env["BOR_LLM_BASE_URL"] = (
"https://aipi.reeseapps.com/v1"
if USE_REAL_LLM
else f"http://127.0.0.1:{mock_llm}/v1"
)
# Mock-calibrated threshold (conftest pattern) — keeps every story
# suite's deterministic gate behavior; production default stays 0.62.
env["BOR_RELEVANCE_THRESHOLD"] = "0.30"
env.setdefault(
"BOR_DATABASE_URL",
"postgresql+psycopg://reese:reese@localhost:5432/brain_of_reese",
)
# Phase 16: admin auth must be set or create_app() refuses to boot.
env["BOR_ADMIN_PASSWORD"] = ADMIN_PASSWORD
env["BOR_SESSION_SECRET"] = SESSION_SECRET
# This suite's subject: one real local repo via the env fallback
# (the registry is truncated per test, so the fallback is the
# effective source list — the test_sync_button.py idiom).
env["BOR_GIT_SOURCES"] = f"file://{fppush_repo}"
env["BOR_SOURCES_DIR"] = str(fppush_repo.parent / "checkouts")
proc = subprocess.Popen(
[sys.executable, "-m", "uvicorn", "app.main:app",
"--host", "127.0.0.1", "--port", str(APP_PORT), "--log-level", "warning"],
cwd=REPO,
env=env,
)
try:
_wait_http(f"{APP_URL}/api/health")
yield APP_URL
finally:
proc.terminate()
try:
proc.wait(timeout=10)
except subprocess.TimeoutExpired:
proc.kill()
@pytest.fixture(scope="module")
def app_url(app_server: str) -> str:
return app_server
def _truncate_kb() -> None:
"""Fresh KB + registry per test (the E2E isolation pattern): each
test's sync counts are its own. ``git_sources`` too — the shared
Postgres may carry leftover registry rows from other suites, and
``effective_sources`` prefers the DB list over this app's
``BOR_GIT_SOURCES`` env fallback. ``sources_meta`` (the KB
generation counter) resets with the KB — the bump is an upsert, so
the sync re-creates the row."""
with SessionLocal() as db:
db.execute(text(
"TRUNCATE chunks, documents, query_log, kb_overview, "
"git_sources, sources_meta"
))
db.commit()
@pytest.fixture(autouse=True)
def _clean_kb(db_ready: None) -> Iterator[None]:
_truncate_kb()
yield
_truncate_kb()
@pytest.fixture(scope="module", autouse=True)
def _wall_time_guard() -> Iterator[None]:
"""The module's wall-time guard (the task's 120 s budget): the
whole module — app boot + four small syncs of a one-file repo
against the mock LLM — stays under 120 s. A breach is a
regression canary (the sync pipeline slowed), not a flake
tolerance."""
start = time.monotonic()
yield
elapsed = time.monotonic() - start
assert elapsed < WALL_BUDGET_S, (
f"the module took {elapsed:.1f}s — over the {WALL_BUDGET_S:.0f}s wall "
"budget (the small-fixture syncs against the mock LLM are a "
"speed canary)"
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _force_push_new_head(repo: Path, content: str) -> None:
"""Rewrite the origin's history to a NEW root commit (a full
divergence — not just a new commit on top): delete the branch
ref and recreate it at a new orphan commit with *content*.
The ``file://`` equivalent of a force push: the app's next
``git fetch`` sees the divergent head. The OBSERVABLE contract is
asserted here — the branch now points at a commit with UNRELATED
history (no ``merge-base`` with the old head) and the new
content. The fixture repo is left with EXACTLY ONE branch (the
original, at the new root), so the helper is repeatable (the
second test force-pushes again).
"""
branch = _rev_parse(repo, "--abbrev-ref", "HEAD")
old_head = _rev_parse(repo, "HEAD")
# A unique orphan branch name per call (a repeatable helper must
# not collide with a leftover ref); it is deleted again at the end
# so the fixture is left with exactly one branch.
orphan = f"fppush-root-{uuid.uuid4().hex[:8]}"
_git(repo, "checkout", "-q", "--orphan", orphan)
(repo / "notes" / "doc.md").write_text(content, encoding="utf-8")
_git(repo, "add", "-A")
_commit(repo, "fppush: rewritten root")
new_head = _rev_parse(repo, "HEAD")
# The force push proper: the branch ref is DELETED and recreated
# at the orphan root — the old history is no longer reachable from
# any ref (a real force push's observable effect).
_git(repo, "update-ref", "-d", f"refs/heads/{branch}")
_git(repo, "update-ref", f"refs/heads/{branch}", new_head)
_git(repo, "checkout", "-q", "-B", branch, new_head)
_git(repo, "update-ref", "-d", f"refs/heads/{orphan}")
# The OBSERVABLE contract: the branch at the new head, and
# UNRELATED history (no common ancestor with the old root).
assert _rev_parse(repo, branch) == new_head
proc = subprocess.run(
["git", "merge-base", old_head, new_head], cwd=repo,
capture_output=True, text=True,
)
assert proc.returncode != 0, (
"the rewrite must be UNRELATED history (no common ancestor); "
f"merge-base found: {proc.stdout.strip()!r}"
)
def _go_top(page: Page) -> None:
"""Back to the catalog's top level (no-op when already there —
the breadcrumb is hidden at the top, the test_sync_button.py
idiom)."""
if not page.locator("#kb-crumb").is_hidden():
page.locator("#kb-crumb a.kb-crumb-link").first.click()
def _expect_fixture_row(page: Page) -> None:
"""The REAL clone was imported — the fixture path is listed. The
catalog is the drill-down tree (phase 97), so drill source →
folder ``notes`` first (the ``test_sync_button._expect_fixture_row``
pattern)."""
_go_top(page)
page.click(f'#folders-tbody a.folder-link:text-is("{FIXTURE_NAME}")')
page.click('#folders-tbody a.folder-link:text-is("notes")')
expect(page.locator("#docs-tbody tr", has_text=FIXTURE_DOC)).to_have_count(1)
def _started_at(body: dict[str, Any]) -> float:
"""The status body's ``started_at`` as a wall-clock epoch (the
server stamps it in UTC; this process runs on the same host —
same clock, the test_git_source_branch idiom)."""
started = body.get("started_at")
if not started:
return 0.0
try:
return datetime.fromisoformat(str(started)).timestamp()
except ValueError:
return 0.0
def _wait_sync_state(
page: Page,
app_url: str,
*,
clicked_at: float,
timeout_s: float = 90.0,
settle_ui: bool = True,
) -> dict[str, Any]:
"""Poll the (cookie-authenticated) status endpoint until the
current run reaches a TERMINAL state, then settle the UI.
The ``test_sync_button._wait_sync_done`` pattern — the terminal
set is ``{"success", "failed"}`` (a cancel is never issued in this
module) — plus the UI-level settle wait on ``#sync-label``
(``settle_ui=True``): a success waits for the "Synced HH:MM"
last-result label (the UI's 2 s poll loop settles it; the
never-stale contract, §7.4), a ``failed`` terminal returns
WITHOUT settling (the label never leaves "Syncing…" on a
failure — the caller's terminal-state assertion is what pins
it). ``settle_ui=False`` (the API-triggered negative pin) skips
the label settle: the UI's poll loop starts only on a click or a
boot-while-running, so an API-triggered run is never adopted
visually — the status endpoint is the authoritative surface.
The module app is shared across the test's sequential runs, so
the PREVIOUS run's terminal lives in the app's memory until this
run supersedes it: a terminal is accepted only after a running
tick was observed or its ``started_at`` post-dates the click (the
stale-terminal freshness gate, the ``test_git_source_branch``
idiom) — the caller captures ``clicked_at`` just before the
trigger (the click or the ``POST /api/sync``).
"""
deadline = time.monotonic() + timeout_s
saw_running = False
body: dict[str, Any] = {}
while time.monotonic() < deadline:
r = page.request.get(f"{app_url}/api/sync/status")
assert r.status == 200, r.text
body = r.json()
state = body["state"]
if state == "running":
saw_running = True
elif state in ("success", "failed") and (
saw_running or _started_at(body) >= clicked_at - 1.0
):
if state == "success" and settle_ui:
expect(page.locator("#sync-label")).to_have_text(
SYNCED_LABEL, timeout=15_000
)
return body
time.sleep(0.25)
raise AssertionError(f"sync did not reach a terminal state: {body}")
# ---------------------------------------------------------------------------
# 1. THE owner's exact flow: sync → force push → sync again → success
# with the NEW head's content
# ---------------------------------------------------------------------------
def test_sync_survives_a_force_push(
page: Page, app_url: str, db_ready: None, fppush_repo: Path
) -> None:
"""The owner's exact reproduction, through the UI: run 1 (clone +
import) → force-push the origin (an orphan-root rewrite, V2) →
run 2 (``state == "success"`` — NOT the pre-phase ``failed``; the
error banner stays hidden and no error modal exists — a reset is
a logged event, not a user-facing failure) with the KB serving
the NEW head's content → run 3 (idempotent at the new head)."""
page.set_default_timeout(30_000)
login(page, app_url) # lands on /sources.html (the button's home)
btn = page.locator("#sync-btn")
expect(btn).to_be_visible()
expect(page.locator("#sync-label")).to_have_text("Sync sources")
# --- run 1: clone + import (the V1 head) --------------------------
clicked_at = time.time()
btn.click()
expect(btn).to_be_disabled()
expect(page.locator("#sync-label")).to_have_text("Syncing…")
body = _wait_sync_state(page, app_url, clicked_at=clicked_at)
assert body["state"] == "success", body
# The fixture doc (one A9-format file) is the only change — the
# counts line rides the box (phase 150).
expect(page.locator("#sync-progress-text")).to_have_text(counts_line("1 added"))
# The REAL clone was imported: the fixture path is in the Sources
# table (the drill-down tree, the test_sync_button pattern).
_expect_fixture_row(page)
# --- the force push (server-side — the test owns the origin) ------
_force_push_new_head(fppush_repo, V2_DOC)
# --- run 2: the resync — SUCCESS, the reset is not a failure ------
expect(btn).to_be_enabled() # never stale — re-enabled at the terminal
clicked_at = time.time()
btn.click()
expect(btn).to_be_disabled()
expect(page.locator("#sync-label")).to_have_text("Syncing…")
body = _wait_sync_state(page, app_url, clicked_at=clicked_at)
# THE regression pin: pre-phase, the --ff-only pull refused the
# divergent head and this terminal was "failed" (the run died in
# the failed state with a user-facing error).
assert body["state"] == "success", body
expect(btn).to_be_enabled()
# A reset is a LOGGED event, not a user-facing failure: the error
# banner stays hidden and no error modal was ever created (it is
# created on demand, so count 0 is the absence proof)…
expect(page.locator("#sync-error-banner")).to_be_hidden()
expect(page.locator(".sync-modal-backdrop")).to_have_count(0)
# …and the digest delta re-indexed the file as updated (the counts
# are the secondary signal — the content assertion below is
# primary). The counts line rides the box (phase 150).
expect(page.locator("#sync-progress-text")).to_have_text(counts_line("0 added · 1 updated"))
# The KB holds the NEW head: the document modal serves the V2
# sentinel — the reset's content flowed through the import.
_expect_fixture_row(page)
doc_row = page.locator("#docs-tbody tr", has_text=FIXTURE_DOC)
doc_row.locator("td:nth-child(2) a.doc-link").click()
expect(page.locator(".doc-modal")).to_be_visible()
expect(page.locator("#doc-modal-content")).to_contain_text(SENTINEL_V2)
page.keyboard.press("Escape")
expect(page.locator(".doc-modal")).to_be_hidden()
# --- run 3: idempotent — the checkout is stably at the new head ---
expect(btn).to_be_enabled()
clicked_at = time.time()
btn.click()
expect(btn).to_be_disabled()
expect(page.locator("#sync-label")).to_have_text("Syncing…")
body = _wait_sync_state(page, app_url, clicked_at=clicked_at)
assert body["state"] == "success", body
# "1 unchanged" means the content hash did not move — drift back
# to V1 would have re-reported as "1 updated" (the no-drift pin).
# The counts line rides the box (phase 150).
expect(page.locator("#sync-progress-text")).to_have_text(counts_line("0 added · 1 unchanged"))
# ---------------------------------------------------------------------------
# 2. The negative pin at the API seam: after a force push, POST
# /api/sync + poll → state == "success" (no UI)
# ---------------------------------------------------------------------------
def test_failed_pull_is_not_the_outcome(
page: Page, app_url: str, db_ready: None, fppush_repo: Path
) -> None:
"""The API-level negative pin (no UI — the task's PREFERRED
direct-API leg, its own test rather than folded into test 1 as
step 4b so the UI test stays focused): a FRESH force push (V3 —
its own orphan root: test 1's checkout already sits at V2, so a
second rewrite is needed to make THIS sync the post-force-push
one) → ``POST /api/sync`` (202) + poll → ``state ==
"success"`` — the same contract as run 2 without the UI — and
the KB row's content carries the V3 sentinel.
Purely the status endpoint + the DB (no page navigation, no UI
assertion): the UI's 2 s poll loop starts only on a click or a
boot-while-running, so it does NOT adopt an API-triggered run —
the status endpoint is the authoritative surface (the house
rule, and the test_sync_button API-trigger pattern)."""
r = page.request.post(
f"{app_url}/api/login", data={"password": ADMIN_PASSWORD}
)
assert r.status == 204, r.text
_force_push_new_head(fppush_repo, V3_DOC)
clicked_at = time.time()
r = page.request.post(f"{app_url}/api/sync")
assert r.status == 202, r.text
body = _wait_sync_state(
page, app_url, clicked_at=clicked_at, settle_ui=False
)
assert body["state"] == "success", body
# Fresh after the per-test truncate: the (reset) checkout's file
# is re-added from the NEW head.
assert body["detail"]["added"] == 1, body
# The NEW head's content is what the KB serves (the sentinel in
# the stored document).
with SessionLocal() as db:
content = db.execute(
text(
"SELECT content FROM documents "
"WHERE source = :src AND path = :path"
),
{"src": FIXTURE_NAME, "path": FIXTURE_DOC},
).scalar_one()
assert SENTINEL_V3 in content