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

All 4 task files exist in `complete/`, and only `00_phase.md` remains in `todo/`. This is the final verification pass. I'll read through the task files and check the current state of the code.
This commit is contained in:
2026-10-04 14:24:33 -04:00
parent 966c4b0540
commit c275735db4
36 changed files with 2526 additions and 99 deletions
+17 -3
View File
@@ -335,10 +335,24 @@ def _offered_tool_names(body: dict[str, Any]) -> list[str]:
return [t["function"]["name"] for t in (body.get("tools") or [])]
def _ready_for_sync_line(source: str) -> re.Pattern[str]:
"""Phase 154 (task 02): the ready-for-sync line carrying the
completion stamp — "Uploaded <source> (<stamp>) — press Sync
sources to import it." (the fresh 202 settle). In E2E the stamp
is ALWAYS the same-day ``HH:MM`` form (the run settles within
seconds; the date-qualified forms are unit-pinned on a fixed
clock — E2E never depends on the wall clock)."""
return re.compile(
rf"^Uploaded {re.escape(source)} \(\d{{1,2}}:\d{{2}}\)"
r" — press Sync sources to import it\.$"
)
def _upload_zip(page: Page, app_url: str, archive: Path) -> None:
"""The real admin upload flow: form login → the git-sources page →
pick the archive → submit → the terminal result line (phase 90:
"Uploaded <source> — press Sync sources to import it.")."""
pick the archive → submit → the terminal result line (phase 90 A3,
phase 154 task 02: "Uploaded <source> (<stamp>) — press Sync
sources to import it." — the completion stamp)."""
login(page, app_url, next=GIT_SOURCES_URL)
expect(page).to_have_url(app_url + GIT_SOURCES_URL, timeout=30_000)
expect(page.locator("#sign-out-btn")).to_be_visible(timeout=15_000)
@@ -347,7 +361,7 @@ def _upload_zip(page: Page, app_url: str, archive: Path) -> None:
result = page.locator("#archive-upload-result")
expect(result).to_be_visible(timeout=UPLOAD_TIMEOUT_MS)
expect(result).to_have_text(
f"Uploaded {SOURCE_NAME} — press Sync sources to import it.",
_ready_for_sync_line(SOURCE_NAME),
timeout=UPLOAD_TIMEOUT_MS,
)
+28 -14
View File
@@ -20,7 +20,8 @@ server-side behind ``GET /api/git-sources/upload/status`` (the
phase-32 ``SyncStatus`` pattern; the phase-64 key set with
``current_file``/``files_done``/``files_total`` null/0/0 for the whole
run — phase 90 A2). The settled result line points at the next step:
"Uploaded <name> — press Sync sources to import it." (phase 90 A3) —
"Uploaded <name> (<stamp>) — press Sync sources to import it."
(phase 90 A3, the phase-154 completion stamp) —
the scan is the RAG page's **Sync sources** button's job, so this
suite asserts **zero indexed documents** after every upload.
@@ -66,8 +67,9 @@ Contract under test:
button then carries the BARE "Processing…" label for the whole
background run (no file, no "(n/m)" — the first status GET is held
so the in-run window outlives the 2 s poll) until the status
``success`` lands: the result line reads "Uploaded e2e-upload —
press Sync sources to import it.", the button restores ("Upload"),
``success`` lands: the result line reads "Uploaded e2e-upload
(<stamp>) — press Sync sources to import it." (the phase-154
completion stamp), the button restores ("Upload"),
the file input clears, the list gains exactly one row for
``e2e-upload`` with the **Local** badge and its "Ignore paths"
control (the phase-89 editor the deferral exists for), the terminal
@@ -139,6 +141,19 @@ SOURCES_URL = "/sources.html"
#: filename minus the archive suffix (the phase's locked naming rule).
SOURCE_NAME = "e2e-upload"
def _ready_for_sync_line(name: str) -> re.Pattern[str]:
"""Phase 154 (task 02): the ready-for-sync line carrying the
completion stamp — "Uploaded <name> (<stamp>) — press Sync sources
to import it." (the fresh 202 settle, the page-local name). In
E2E the stamp is ALWAYS the same-day ``HH:MM`` form — the run
settles within seconds (the date-qualified forms are unit-pinned
on a fixed clock — E2E never depends on the wall clock)."""
return re.compile(
rf"^Uploaded {re.escape(name)} \(\d{{1,2}}:\d{{2}}\)"
r" — press Sync sources to import it\.$"
)
#: v1: two sentinel docs. v2 (same filename): alpha CHANGED, beta DROPPED,
#: gamma ADDED — the in-place-replace subject (the on-disk folder swap).
ALPHA_SENTINEL_V1 = "ALPHA-TOKEN-v1-7f31"
@@ -436,8 +451,9 @@ def test_upload_registers_without_indexing(
then carries the BARE "Processing…" label for the whole background
run (no file, no "(n/m)" — phase 90 A2, proven across a held first
status GET); when the status ``success`` lands the result line
reads "Uploaded <source> — press Sync sources to import it."
(A3), the button restores ("Upload"), the file input clears, the
reads "Uploaded <source> (<stamp>) — press Sync sources to
import it." (A3 — the phase-154 completion stamp), the button
restores ("Upload"), the file input clears, the
list gains exactly ONE row for ``e2e-upload`` (Local badge, the
"Ignore paths" control), the terminal status carries the no-count
``{"message": "uploaded"}`` detail with null/0/0 progress — and
@@ -490,9 +506,7 @@ def test_upload_registers_without_indexing(
# …release: the status success lands → the ready-for-sync line +
# the never-stale restore (input cleared).
expect(result).to_be_visible(timeout=UPLOAD_TIMEOUT_MS)
expect(result).to_have_text(
f"Uploaded {SOURCE_NAME} — press Sync sources to import it."
)
expect(result).to_have_text(_ready_for_sync_line(SOURCE_NAME))
expect(btn).to_be_enabled()
expect(btn).to_have_text("Upload")
expect(page.locator("#archive-upload-file")).to_have_value("")
@@ -560,8 +574,8 @@ def test_reupload_replaces_in_place(
# Baseline: v1 through the page (202 → the ready-for-sync line,
# one row, no index).
assert _upload_via_page(page, tarball_v1) == (
f"Uploaded {SOURCE_NAME} — press Sync sources to import it."
assert _ready_for_sync_line(SOURCE_NAME).fullmatch(
_upload_via_page(page, tarball_v1)
)
expect(page.locator("#git-sources-tbody tr", has_text=SOURCE_NAME)).to_have_count(1)
folder = upload_dir / SOURCE_NAME
@@ -570,8 +584,8 @@ def test_reupload_replaces_in_place(
# Re-upload v2 — SAME basename, different parent dir (the file
# input's selection is replaced wholesale).
assert _upload_via_page(page, tarball_v2) == (
f"Uploaded {SOURCE_NAME} — press Sync sources to import it."
assert _ready_for_sync_line(SOURCE_NAME).fullmatch(
_upload_via_page(page, tarball_v2)
)
# No duplicate: exactly ONE row for that source (and one row total).
@@ -635,8 +649,8 @@ def test_bad_file_inline_error(
# The form is not wedged: a good upload right after still works —
# and indexes nothing (phase 90 A1).
assert _upload_via_page(page, tarball_v1) == (
f"Uploaded {SOURCE_NAME} — press Sync sources to import it."
assert _ready_for_sync_line(SOURCE_NAME).fullmatch(
_upload_via_page(page, tarball_v1)
)
expect(error).to_be_hidden()
expect(page.locator("#git-sources-tbody tr", has_text=SOURCE_NAME)).to_have_count(1)
+17 -3
View File
@@ -296,10 +296,24 @@ def _health_db(app_url: str) -> bool:
return False
def _ready_for_sync_line(source: str) -> re.Pattern[str]:
"""Phase 154 (task 02): the ready-for-sync line carrying the
completion stamp — "Uploaded <source> (<stamp>) — press Sync
sources to import it." (the fresh 202 settle). In E2E the stamp
is ALWAYS the same-day ``HH:MM`` form (the run settles within
seconds; the date-qualified forms are unit-pinned on a fixed
clock — E2E never depends on the wall clock)."""
return re.compile(
rf"^Uploaded {re.escape(source)} \(\d{{1,2}}:\d{{2}}\)"
r" — press Sync sources to import it\.$"
)
def _upload_zip(page: Page, app_url: str, archive: Path, source: str) -> None:
"""The real admin upload flow: form login → the git-sources page →
pick the archive → submit → the terminal result line (phase 90:
"Uploaded <source> — press Sync sources to import it.")."""
pick the archive → submit → the terminal result line (phase 90 A3,
phase 154 task 02: "Uploaded <source> (<stamp>) — press Sync
sources to import it." — the completion stamp)."""
login(page, app_url, next=GIT_SOURCES_URL)
expect(page).to_have_url(app_url + GIT_SOURCES_URL, timeout=30_000)
expect(page.locator("#sign-out-btn")).to_be_visible(timeout=15_000)
@@ -308,7 +322,7 @@ def _upload_zip(page: Page, app_url: str, archive: Path, source: str) -> None:
result = page.locator("#archive-upload-result")
expect(result).to_be_visible(timeout=UPLOAD_TIMEOUT_MS)
expect(result).to_have_text(
f"Uploaded {source} — press Sync sources to import it.",
_ready_for_sync_line(source),
timeout=UPLOAD_TIMEOUT_MS,
)
+17 -3
View File
@@ -233,10 +233,24 @@ def _health_db(app_url: str) -> bool:
return False
def _ready_for_sync_line(source: str) -> re.Pattern[str]:
"""Phase 154 (task 02): the ready-for-sync line carrying the
completion stamp — "Uploaded <source> (<stamp>) — press Sync
sources to import it." (the fresh 202 settle). In E2E the stamp
is ALWAYS the same-day ``HH:MM`` form (the run settles within
seconds; the date-qualified forms are unit-pinned on a fixed
clock — E2E never depends on the wall clock)."""
return re.compile(
rf"^Uploaded {re.escape(source)} \(\d{{1,2}}:\d{{2}}\)"
r" — press Sync sources to import it\.$"
)
def _upload_zip(page: Page, app_url: str, archive: Path) -> None:
"""The real admin upload flow: form login → the git-sources page →
pick the archive → submit → the terminal result line (phase 90:
"Uploaded <source> — press Sync sources to import it.")."""
pick the archive → submit → the terminal result line (phase 90 A3,
phase 154 task 02: "Uploaded <source> (<stamp>) — press Sync
sources to import it." — the completion stamp)."""
login(page, app_url, next=GIT_SOURCES_URL)
expect(page).to_have_url(app_url + GIT_SOURCES_URL, timeout=30_000)
expect(page.locator("#sign-out-btn")).to_be_visible(timeout=15_000)
@@ -245,7 +259,7 @@ def _upload_zip(page: Page, app_url: str, archive: Path) -> None:
result = page.locator("#archive-upload-result")
expect(result).to_be_visible(timeout=UPLOAD_TIMEOUT_MS)
expect(result).to_have_text(
f"Uploaded {SOURCE_NAME} — press Sync sources to import it.",
_ready_for_sync_line(SOURCE_NAME),
timeout=UPLOAD_TIMEOUT_MS,
)
+17 -3
View File
@@ -320,10 +320,24 @@ def _health_db(app_url: str) -> bool:
return False
def _ready_for_sync_line(source: str) -> re.Pattern[str]:
"""Phase 154 (task 02): the ready-for-sync line carrying the
completion stamp — "Uploaded <source> (<stamp>) — press Sync
sources to import it." (the fresh 202 settle). In E2E the stamp
is ALWAYS the same-day ``HH:MM`` form (the run settles within
seconds; the date-qualified forms are unit-pinned on a fixed
clock — E2E never depends on the wall clock)."""
return re.compile(
rf"^Uploaded {re.escape(source)} \(\d{{1,2}}:\d{{2}}\)"
r" — press Sync sources to import it\.$"
)
def _upload_zip(page: Page, app_url: str, archive: Path, source: str) -> None:
"""The real admin upload flow: form login → the git-sources page →
pick the archive → submit → the terminal result line (phase 90:
"Uploaded <source> — press Sync sources to import it.")."""
pick the archive → submit → the terminal result line (phase 90 A3,
phase 154 task 02: "Uploaded <source> (<stamp>) — press Sync
sources to import it." — the completion stamp)."""
login(page, app_url, next="/git-sources.html")
expect(page).to_have_url(app_url + "/git-sources.html", timeout=30_000)
expect(page.locator("#sign-out-btn")).to_be_visible(timeout=15_000)
@@ -332,7 +346,7 @@ def _upload_zip(page: Page, app_url: str, archive: Path, source: str) -> None:
result = page.locator("#archive-upload-result")
expect(result).to_be_visible(timeout=UPLOAD_TIMEOUT_MS)
expect(result).to_have_text(
f"Uploaded {source} — press Sync sources to import it.",
_ready_for_sync_line(source),
timeout=UPLOAD_TIMEOUT_MS,
)
+17 -3
View File
@@ -379,10 +379,24 @@ def _health_db(app_url: str) -> bool:
return False
def _ready_for_sync_line(source: str) -> re.Pattern[str]:
"""Phase 154 (task 02): the ready-for-sync line carrying the
completion stamp — "Uploaded <source> (<stamp>) — press Sync
sources to import it." (the fresh 202 settle). In E2E the stamp
is ALWAYS the same-day ``HH:MM`` form (the run settles within
seconds; the date-qualified forms are unit-pinned on a fixed
clock — E2E never depends on the wall clock)."""
return re.compile(
rf"^Uploaded {re.escape(source)} \(\d{{1,2}}:\d{{2}}\)"
r" — press Sync sources to import it\.$"
)
def _upload_zip(page: Page, app_url: str, archive: Path, source_name: str) -> None:
"""The real admin upload flow: form login → the git-sources page →
pick the archive → submit → the terminal result line (phase 90:
"Uploaded <source> — press Sync sources to import it.")."""
pick the archive → submit → the terminal result line (phase 90 A3,
phase 154 task 02: "Uploaded <source> (<stamp>) — press Sync
sources to import it." — the completion stamp)."""
login(page, app_url, next="/git-sources.html")
expect(page).to_have_url(app_url + "/git-sources.html", timeout=30_000)
expect(page.locator("#sign-out-btn")).to_be_visible(timeout=15_000)
@@ -391,7 +405,7 @@ def _upload_zip(page: Page, app_url: str, archive: Path, source_name: str) -> No
result = page.locator("#archive-upload-result")
expect(result).to_be_visible(timeout=UPLOAD_TIMEOUT_MS)
expect(result).to_have_text(
f"Uploaded {source_name} — press Sync sources to import it.",
_ready_for_sync_line(source_name),
timeout=UPLOAD_TIMEOUT_MS,
)
+17 -3
View File
@@ -278,10 +278,24 @@ def _health_db(app_url: str) -> bool:
return False
def _ready_for_sync_line(source: str) -> re.Pattern[str]:
"""Phase 154 (task 02): the ready-for-sync line carrying the
completion stamp — "Uploaded <source> (<stamp>) — press Sync
sources to import it." (the fresh 202 settle). In E2E the stamp
is ALWAYS the same-day ``HH:MM`` form (the run settles within
seconds; the date-qualified forms are unit-pinned on a fixed
clock — E2E never depends on the wall clock)."""
return re.compile(
rf"^Uploaded {re.escape(source)} \(\d{{1,2}}:\d{{2}}\)"
r" — press Sync sources to import it\.$"
)
def _upload_zip(page: Page, app_url: str, archive: Path) -> None:
"""The real admin upload flow: form login → the git-sources page →
pick the archive → submit → the terminal result line (phase 90:
"Uploaded <source> — press Sync sources to import it.")."""
pick the archive → submit → the terminal result line (phase 90 A3,
phase 154 task 02: "Uploaded <source> (<stamp>) — press Sync
sources to import it." — the completion stamp)."""
login(page, app_url, next="/git-sources.html")
expect(page).to_have_url(app_url + "/git-sources.html", timeout=30_000)
expect(page.locator("#sign-out-btn")).to_be_visible(timeout=15_000)
@@ -290,7 +304,7 @@ def _upload_zip(page: Page, app_url: str, archive: Path) -> None:
result = page.locator("#archive-upload-result")
expect(result).to_be_visible(timeout=UPLOAD_TIMEOUT_MS)
expect(result).to_have_text(
f"Uploaded {SOURCE_NAME} — press Sync sources to import it.",
_ready_for_sync_line(SOURCE_NAME),
timeout=UPLOAD_TIMEOUT_MS,
)
+17 -3
View File
@@ -427,10 +427,24 @@ def _clean_kb(app_url: str) -> Iterator[None]:
_truncate_all()
def _ready_for_sync_line(source: str) -> re.Pattern[str]:
"""Phase 154 (task 02): the ready-for-sync line carrying the
completion stamp — "Uploaded <source> (<stamp>) — press Sync
sources to import it." (the fresh 202 settle). In E2E the stamp
is ALWAYS the same-day ``HH:MM`` form (the run settles within
seconds; the date-qualified forms are unit-pinned on a fixed
clock — E2E never depends on the wall clock)."""
return re.compile(
rf"^Uploaded {re.escape(source)} \(\d{{1,2}}:\d{{2}}\)"
r" — press Sync sources to import it\.$"
)
def _upload_tar(page: Page, app_url: str, archive: Path) -> None:
"""The real admin upload flow: form login → the git-sources page →
pick the archive → submit → the terminal result line (phase 90:
"Uploaded <source> — press Sync sources to import it.")."""
pick the archive → submit → the terminal result line (phase 90 A3,
phase 154 task 02: "Uploaded <source> (<stamp>) — press Sync
sources to import it." — the completion stamp)."""
login(page, app_url, next="/git-sources.html")
expect(page).to_have_url(app_url + "/git-sources.html", timeout=30_000)
expect(page.locator("#sign-out-btn")).to_be_visible(timeout=15_000)
@@ -439,7 +453,7 @@ def _upload_tar(page: Page, app_url: str, archive: Path) -> None:
result = page.locator("#archive-upload-result")
expect(result).to_be_visible(timeout=UPLOAD_TIMEOUT_MS)
expect(result).to_have_text(
f"Uploaded {SOURCE_NAME} — press Sync sources to import it.",
_ready_for_sync_line(SOURCE_NAME),
timeout=UPLOAD_TIMEOUT_MS,
)
+769
View File
@@ -0,0 +1,769 @@
"""Phase 154 task 04 E2E (Playwright, mock-only): the two visible
FRESHNESS-STAMP contracts on the live UI (the phase-154 date-qualified
stamps — the same-day shape is pinned here; the non-same-day shapes
are unit-pinned on a fixed clock, so this suite never depends on the
wall clock beyond "the run finishes today").
1. **The RAG sync badge settles same-day-shaped**: a fresh mock sync
(a small registered local source, the mock LLM keeps it short)
settles the ``#sync-btn`` label to ``Synced HH:MM`` — the
DATE-QUALIFIER (phase 154, task 01, ``fmtSyncTime``'s widened
contract) must yield the same-day ``HH:MM`` shape for a run that
finished "now". The assertion is a SHAPE pin (the regex
``^Synced \\d{2}:\\d{2}$`` — the zero-padded 24-hour form
``fmtSyncTime`` produces for the same local calendar day), never a
comparison against the wall clock's actual value (the
date-qualified forms are unit-pinned on a fixed clock, phase 154,
task 03 — the E2E never depends on the wall clock beyond "the
run finishes today").
2. **The upload line's fresh settle carries the name AND the stamp**:
a real small fixture archive through the page's upload form → the
202 "Successfully uploaded — <source>" toast → the 2 s poll's
settle renders the ready-for-sync line
``Uploaded <name> (<stamp>) — press Sync sources to import it.``
(the phase-154 task 02 shape, the page-local safe name +
``fmtUploadStamp``'s completion stamp off the server payload's
``finished_at`` — the terminal status pin carries the non-null
``finished_at`` the stamp is computed from). The stamp matches the
shape pin (same-day ``HH:MM`` in practice; the shape regex also
covers the date-qualified forms, so the pin never breaks across a
midnight boundary).
3. **The boot re-render is NAMELESS but still STAMPED** (the phase's
actual fix — the owner's "the line can sit there indefinitely and
mislead"): after the settled fresh line, a page reload boots
``initUploadStatus``'s success branch — the safe name was
page-local (``lastUploadName`` is null after a reload — the
phase-90 contract), so the line is the NAMELESS
``Uploaded <stamp> — press Sync sources to import it.`` — but the
stamp SURVIVES because it comes from the SERVER payload
(``finished_at``), not from page state. The run's identity pin
(``started_at`` unchanged across the reload) proves the re-render
is the same run's line, not a second upload's.
4. **A running upload never renders the ready-for-sync line** (the
phase-90 regression guard, extended): at the 202 moment the button
is the BARE ``Processing…`` (phase 90, A2 — no file, no counts,
title cleared) and ``#archive-upload-result`` is HIDDEN — the line
appears ONLY on settle (the poll's success tick). The
deterministic in-flight window: the 202 handler synchronously
enters the processing state and schedules the first poll tick at
+2 s, so the ``Processing…`` state holds for ≥ 2 s regardless of
how fast the sub-second background run finishes — the
in-flight observation needs no delay proxy (the upload makes zero
LLM calls; the sync leg of test 1 is 2 files and settles within
the generous budget).
Run in isolation (DB must be up: ``podman compose up -d db``):
uv run pytest tests/e2e/test_sync_status_freshness.py -v --no-cov
MOCK-ONLY suite: ``E2E_REAL_LLM=1`` is not supported — the whole
point is the deterministic mock (no real-LLM dependency), and the
time assertions are shape pins, never wall-clock value comparisons.
Per-module app env (the conftest pattern — mirroring
``test_upload_no_scan.py``'s local helpers, NOT importing that
module): ``BOR_UPLOAD_DIR`` points at a scratch dir (the app runs on
the same host), ``BOR_GIT_SOURCES`` is forced empty (this suite's
own registered rows / upload rows are the only sync sources),
``BOR_LLM_BASE_URL`` is the mock (the sync leg's embeds + overview),
the retry knobs pinned (the sync leg never burns a 5 s retry wait),
and the autouse ``_clean`` truncates the shared Postgres (plus waits
for no running background job — a leaked run must not survive into
the next test's truncated KB).
"""
from __future__ import annotations
import io
import os
import re
import subprocess
import sys
import tarfile
import time
from collections.abc import Iterator
from pathlib import Path
from typing import Any
import httpx
import pytest
from playwright.sync_api import Page, expect
from sqlalchemy import text
from app.config import Settings as _Settings
from app.db import SessionLocal
from e2e.auth_helpers import login
from e2e.conftest import (
ADMIN_PASSWORD,
SESSION_SECRET,
USE_REAL_LLM,
_wait_http,
)
REPO = Path(__file__).resolve().parents[2]
# Phase 79 (task 04, full inventory): in a combined session run the
# conftest session app already owns its port — a second uvicorn on it
# dies on bind and this suite would silently drive the wrong server.
# The module app binds its own port instead (env-overridable).
APP_PORT = int(os.environ.get("E2E_APP_PORT_SYNCFRESH", "8172"))
APP_URL = f"http://127.0.0.1:{APP_PORT}"
SOURCES_URL = "/sources.html"
GIT_SOURCES_URL = "/git-sources.html"
#: The sync-leg fixture source (a host temp dir, ``kind=local``) —
#: exactly two small markdown files, so the counts line is "2 added"
#: and the run is short against the fast mock LLM.
SYNC_SOURCE = "freshness-sync"
SYNC_FILES: dict[str, str] = {
"alpha.md": (
"# Freshness alpha\n"
"\n"
"A small note that exists only to give the phase-154 sync\n"
"fixture a real import.\n"
"\n"
"Marker: FRESHNESS-SYNC-ALPHA-7f31\n"
),
"beta.md": (
"# Freshness beta\n"
"\n"
"The second small note — the counts line's denominator.\n"
"\n"
"Marker: FRESHNESS-SYNC-BETA-2c90\n"
),
}
#: The upload-leg archive basenames (one per upload test — distinct
#: names keep the in-place-replace identity out of the assertions).
UPLOAD_A = "e2e-freshness-a"
UPLOAD_B = "e2e-freshness-b"
UPLOAD_C = "e2e-freshness-c"
def _targz_files(name: str) -> dict[str, str]:
"""One small deterministic markdown file per archive (a valid
replacement archive — what the KB indexes with it is the sync's
call, phase 90)."""
return {
"note.md": (
f"# Freshness {name}\n"
"\n"
"A small note carried by this suite's archive.\n"
f"\nMarker: FRESHNESS-{name.upper()}-9d16\n"
)
}
# ---------------------------------------------------------------------------
# Shape pins (the task's contract: shapes, never wall-clock values)
# ---------------------------------------------------------------------------
#: The date-qualified stamp's shape (phase 154, A1 rule): the
#: same-day ``HH:MM`` (zero-padded — ``fmtSyncTime`` /
#: ``fmtUploadStamp`` pad both fields) OR the date-qualified forms
#: (``M/D(, YYYY), h:MM AM/PM`` — no leading zero on month/day/hour).
#: In practice a run that settles "now" always lands on the same-day
#: branch; the shape regex covers both so the pin never breaks across
#: a midnight boundary. The date-qualified VALUES are unit-pinned on a
#: fixed clock (phase 154, task 03) — this suite never compares a
#: stamp against the actual time.
STAMP = (
r"\d{2}:\d{2}"
r"|\d{1,2}/\d{1,2}(?:/\d{4})?, \d{1,2}:\d{2} [AP]M"
)
#: ``Synced HH:MM`` — the same-day settled sync label (task 01: the
#: qualifier yields the zero-padded ``HH:MM`` for a run that finished
#: on the same local calendar day — byte-identical to the pre-phase
#: shape). The date-qualified ``M/D(, YYYY), h:MM AM/PM`` forms are
#: unit-pinned on a fixed clock (phase 154, task 03) — this pin is
#: the same-day shape the task pins; a run that settles "now" always
#: lands on the same-day branch.
SYNCED_SAME_DAY = re.compile(r"^Synced \d{2}:\d{2}$")
#: The fresh-settle ready-for-sync line WITH the safe name and the
#: stamp (phase 154, task 02 — the page-local ``lastUploadName`` +
#: ``fmtUploadStamp(status)`` off the server payload's
#: ``finished_at``).
def _ready_line(name: str) -> re.Pattern[str]:
return re.compile(
rf"^Uploaded {re.escape(name)} \(({STAMP})\)"
r" — press Sync sources to import it\.$"
)
#: The boot re-render ready-for-sync line — NAMELESS (the safe name
#: was page-local, ``lastUploadName`` is null after a reload — the
#: phase-90 contract) but STAMPED (the stamp comes from the SERVER
#: payload — the phase-154 fix: a line that outlives its upload is
#: self-dating).
NAMELESS_READY_LINE = re.compile(
rf"^Uploaded ({STAMP}) — press Sync sources to import it\.$"
)
#: Generous settle budgets: the upload run settles in milliseconds
#: (phase 90 — unpack + register only, zero LLM calls) and the UI's
#: 2 s poll lands one tick after; the sync leg is 2 files against the
#: fast mock LLM and may settle between the 2 s poll ticks — the
#: settled-state assertions retry until then.
UPLOAD_TIMEOUT_MS = 30_000
SYNC_TIMEOUT_MS = 90_000
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
def _build_targz(path: Path, files: dict[str, str]) -> Path:
"""A deterministic ``.tar.gz`` (mtime 0) over the given files."""
with tarfile.open(path, "w:gz") as tf:
for rel, content in files.items():
data = content.encode("utf-8")
info = tarfile.TarInfo(rel)
info.size = len(data)
info.mtime = 0
tf.addfile(info, io.BytesIO(data))
return path
@pytest.fixture(scope="module")
def sync_dir(tmp_path_factory: pytest.TempPathFactory) -> Path:
"""The sync-leg's local source (``kind=local`` — the directory's
basename is the source name, phase 38): EXACTLY the two fixture
files (the counts line reads ``2 added``)."""
root = tmp_path_factory.mktemp("bor_freshness_sync")
src = root / SYNC_SOURCE
src.mkdir()
for rel, content in SYNC_FILES.items():
(src / rel).write_text(content, encoding="utf-8")
assert len(list(src.rglob("*"))) == len(SYNC_FILES)
return src
@pytest.fixture(scope="module")
def upload_dir(tmp_path_factory: pytest.TempPathFactory) -> Path:
"""The app's ``BOR_UPLOAD_DIR`` for this suite — a scratch dir
the host-side assertions inspect (the app server runs on the same
machine). The app creates it on the first upload."""
return tmp_path_factory.mktemp("bor_freshness_uploads") / "uploads"
@pytest.fixture(scope="module")
def tarball_a(tmp_path_factory: pytest.TempPathFactory) -> Path:
root = tmp_path_factory.mktemp("bor_freshness_a")
return _build_targz(root / f"{UPLOAD_A}.tar.gz", _targz_files(UPLOAD_A))
@pytest.fixture(scope="module")
def tarball_b(tmp_path_factory: pytest.TempPathFactory) -> Path:
root = tmp_path_factory.mktemp("bor_freshness_b")
return _build_targz(root / f"{UPLOAD_B}.tar.gz", _targz_files(UPLOAD_B))
@pytest.fixture(scope="module")
def tarball_c(tmp_path_factory: pytest.TempPathFactory) -> Path:
root = tmp_path_factory.mktemp("bor_freshness_c")
return _build_targz(root / f"{UPLOAD_C}.tar.gz", _targz_files(UPLOAD_C))
@pytest.fixture(scope="module")
def app_server(
mock_llm: int,
upload_dir: Path,
tmp_path_factory: pytest.TempPathFactory,
) -> Iterator[str]:
"""The real app under test — per-module env: uploads unpack into
a scratch dir, the env git list is forced empty (this suite's own
rows are the only sync sources), and the LLM base URL is the mock
(the upload makes zero LLM calls — phase 90; the mock serves the
sync leg's embeds + overview). No ``slow_llm`` proxy: the upload
run is sub-second and the 2 s poll window is what the in-flight
assertion leans on (see the module docstring's test 4 paragraph).
Skips the module (clear instructions) when Postgres is down —
the sync needs the KB tables."""
env = dict(os.environ)
env.pop("DEBUGPY", None)
env["BOR_ENVIRONMENT"] = "e2e"
env["BOR_STATIC_DIR"] = str(REPO / "frontend")
env["BOR_LLM_BASE_URL"] = (
"https://aipi.reeseapps.com/v1"
if USE_REAL_LLM
else f"http://127.0.0.1:{mock_llm}/v1"
)
# Mock-calibrated threshold (conftest pattern): this suite never
# asks the chat model anything — the gate is never on a path.
env["BOR_RELEVANCE_THRESHOLD"] = "0.30"
# Phase 67: instant retry waits + the code-default budget (the
# conftest leak-guard pattern — the sync leg never burns a 5 s
# retry wait).
env["BOR_LLM_RETRY_DELAY"] = "0"
env["BOR_LLM_RETRIES"] = str(_Settings.model_fields["llm_retries"].default)
env.setdefault(
"BOR_DATABASE_URL",
"postgresql+psycopg://reese:reese@localhost:5432/brain_of_reese",
)
# Phase 16: admin auth must be set or create_app() refuses to boot.
env["BOR_ADMIN_PASSWORD"] = ADMIN_PASSWORD
env["BOR_SESSION_SECRET"] = SESSION_SECRET
# The repo's .env file carries the owner's BOR_GIT_SOURCES (the app
# reads it from cwd) — override it with an EMPTY value (the env var
# beats the .env file): the registry must hold EXACTLY this
# suite's own rows.
env["BOR_GIT_SOURCES"] = ""
# Phase 49: unpack uploads into the suite's scratch dir (host-
# inspectable) and keep the (unused) git checkouts out of the dev
# location.
env["BOR_UPLOAD_DIR"] = str(upload_dir)
env["BOR_SOURCES_DIR"] = str(tmp_path_factory.mktemp("bor_checkouts"))
proc = subprocess.Popen(
[sys.executable, "-m", "uvicorn", "app.main:app",
"--host", "127.0.0.1", "--port", str(APP_PORT), "--log-level", "warning"],
cwd=REPO,
env=env,
)
try:
_wait_http(f"{APP_URL}/api/health")
body = httpx.get(f"{APP_URL}/api/health", timeout=5).json()
if body["db"] != "up":
pytest.skip("Postgres not reachable — run `podman compose up -d db` first")
yield APP_URL
finally:
proc.terminate()
try:
proc.wait(timeout=10)
except subprocess.TimeoutExpired:
proc.kill()
@pytest.fixture(scope="module")
def app_url(app_server: str) -> str:
return app_server
def _truncate_all() -> None:
"""Fresh registry + KB per test (the E2E isolation pattern): the
row-count and doc-list assertions must be this test's own doing.
The E2E suites share one Postgres, and a leftover git_sources row
or document would corrupt them (a leftover row would be a SECOND
sync source, skewing the sync's counts). ``sources_meta`` (the KB
generation counter) resets with the KB — the sync re-creates the
row; ``folder_summaries`` / ``steering_notes`` ride along (the
phase-96 pattern)."""
with SessionLocal() as db:
db.execute(
text(
"TRUNCATE chunks, documents, query_log, steering_notes, "
"kb_overview, git_sources, sources_meta, folder_summaries"
)
)
db.commit() # without the commit the TRUNCATE rolls back (the house pattern)
def _wait_no_running_jobs(app_url: str) -> None:
"""No background job may leak across tests (the run states live in
the app's memory, and a still-running sync would keep importing
into the NEXT test's truncated KB, and a still-running upload
would 409 the next test's upload): wait for BOTH status endpoints
to be non-running BEFORE the truncate. Own admin session (the
endpoints are admin-only) — usually a no-op: the tests settle
only after their terminal states."""
with httpx.Client(timeout=5.0) as client:
client.post(f"{app_url}/api/login", json={"password": ADMIN_PASSWORD})
for path in ("/api/sync/status", "/api/git-sources/upload/status"):
body: dict[str, Any] = {}
deadline = time.monotonic() + 90
while time.monotonic() < deadline:
r = client.get(f"{app_url}{path}")
if r.status_code == 200:
body = r.json()
if body["state"] != "running":
break
time.sleep(0.2)
assert body["state"] != "running", (
f"a background job was still running at test boundary: {path} {body}"
)
@pytest.fixture(autouse=True)
def _clean(app_url: str, db_ready: None) -> Iterator[None]:
_wait_no_running_jobs(app_url)
_truncate_all()
yield
_truncate_all()
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _register_local_source(base_url: str, path: str) -> None:
"""Register the temp directory through the authenticated API (the
``test_local_directory_sources.py`` / phase-94 pattern — a plain
directory is an API/DB-only operation)."""
with httpx.Client(base_url=base_url, timeout=30.0) as client:
r = client.post("/api/login", json={"password": ADMIN_PASSWORD})
assert r.status_code == 204, r.text
r = client.post(
"/api/git-sources", json={"kind": "local", "path": path}
)
assert r.status_code == 201, r.text
def _status(page: Page, app_url: str, path: str) -> dict[str, Any]:
"""The status endpoint as the signed-in page (admin cookie)."""
r = page.request.get(f"{app_url}{path}")
assert r.status == 200, r.text
return r.json()
def _admin_git_sources_page(page: Page, app_url: str) -> None:
"""Real form login landing on the git sources page (admin settled:
Sign out visible, the manager revealed by the page module)."""
login(page, app_url, next=GIT_SOURCES_URL)
expect(page).to_have_url(app_url + GIT_SOURCES_URL, timeout=30_000)
expect(page.locator("#sign-out-btn")).to_be_visible(timeout=15_000)
expect(page.locator("#git-sources-gate")).to_be_hidden()
expect(page.locator("#git-sources-content")).to_be_visible()
def _upload_via_page(page: Page, archive: Path) -> None:
"""Pick the archive and submit the form (the 202 path: the toast
fires at the 202, then the button's status polling settles the
line — the caller asserts the line's shape). Every upload in this
suite succeeds (the failure path is another suite's subject), so
any non-202 outcome here is a test error."""
page.set_input_files("#archive-upload-file", str(archive))
page.click("#archive-upload-btn")
toast = page.locator(".toast")
expect(toast).to_have_count(1, timeout=UPLOAD_TIMEOUT_MS)
expect(toast).to_have_class(re.compile(r"\bis-visible\b"))
# ---------------------------------------------------------------------------
# 1. The RAG sync badge settles in the same-day shape (the qualifier's
# E2E half — the date-qualified values are unit-pinned, the shape
# is pinned here on a run that finished "now")
# ---------------------------------------------------------------------------
def test_fresh_sync_settles_same_day_shape(
page: Page, app_url: str, db_ready: None, sync_dir: Path
) -> None:
"""A fresh mock sync (the small registered local source — the mock
LLM keeps the run short) settles the RAG ``#sync-btn`` label to
the same-day ``Synced HH:MM`` shape (phase 154, task 01 — the
qualifier must yield ``HH:MM`` for a run that finished on the
same local calendar day — the SHAPE pin, never a wall-clock value
comparison). The run is real (the counts line ``2 added`` + the
terminal status's non-null ``finished_at`` — the ISO the
badge's stamp is computed from — + 2 of 2 files), the settled
button is re-enabled with the ship aria value restored, and no
error UI is present."""
page.set_default_timeout(30_000)
_register_local_source(app_url, str(sync_dir))
login(page, app_url, next=SOURCES_URL)
expect(page).to_have_url(app_url + SOURCES_URL, timeout=30_000)
btn = page.locator("#sync-btn")
expect(btn).to_be_visible(timeout=30_000)
# The settled boot label — the fresh app's idle state (this is the
# module's first test; the app's in-memory sync state is idle).
expect(page.locator("#sync-label")).to_have_text("Sync sources")
expect(btn).to_be_enabled()
expect(page.locator("#sync-error-banner")).to_be_hidden()
btn.click()
# The click's immediate state (the 202 moment — the bare prefix in
# the box — phase 149: the running label rides the box, the
# button is the spinner).
expect(btn).to_be_disabled()
expect(btn).to_have_attribute("aria-busy", "true")
expect(btn.locator(".sync-icon")).to_have_class(re.compile(r"\bis-spinning\b"))
expect(page.locator("#sync-progress")).to_have_text("Syncing…")
# The settle (phase 154, task 01 — the SAME-DAY SHAPE pin):
# "Synced HH:MM" — the date-qualified ``M/D(, YYYY), h:MM AM/PM``
# forms are unit-pinned on a fixed clock (phase 154, task 03);
# this run finished "now", so the qualifier must yield the
# same-day shape (the shape is asserted; the actual time value is
# never compared against the wall clock).
expect(page.locator("#sync-label")).to_have_text(
SYNCED_SAME_DAY, timeout=SYNC_TIMEOUT_MS
)
# The run was real (the counts line + the terminal status agree —
# the status is the source of truth for the settle; the UI's 2 s
# poll renders it at most one tick later).
expect(page.locator("#sync-progress-text")).to_have_text(
f"{len(SYNC_FILES)} added", timeout=SYNC_TIMEOUT_MS
)
btn = page.locator("#sync-btn")
expect(btn).to_be_enabled()
expect(btn).not_to_have_attribute("aria-busy")
# Phase 150: the settle restores the ship aria value (the running
# full label is gone — the badge is the pill's text).
assert btn.get_attribute("aria-label") == "Sync sources", (
btn.get_attribute("aria-label")
)
expect(page.locator("#sync-error-banner")).to_be_hidden()
terminal = _status(page, app_url, "/api/sync/status")
assert terminal["state"] == "success", terminal
# The run was a fresh import of exactly the two fixture files —
# and its ``finished_at`` (non-null) is the ISO the badge's stamp
# is computed from (the stamp's server-side half).
assert terminal["detail"]["added"] == len(SYNC_FILES), terminal
assert terminal["detail"]["pruned"] == 0, terminal
assert terminal["files_done"] == len(SYNC_FILES), terminal
assert terminal["files_total"] == len(SYNC_FILES), terminal
assert terminal["finished_at"], terminal
# ---------------------------------------------------------------------------
# 2. The upload line's fresh settle carries the name AND the stamp
# (phase 154, task 02 — the phase-90 no-count contract extended)
# ---------------------------------------------------------------------------
def test_upload_line_fresh_settle_carrys_name_and_stamp(
page: Page,
app_url: str,
db_ready: None,
tarball_a: Path,
upload_dir: Path,
) -> None:
"""One real upload through the page: the 202 fires the
"Successfully uploaded — <source>" toast (phase 64, A2); the
sub-second background run (unpack + register only — phase 90)
settles at the next 2 s poll tick into the ready-for-sync line
``Uploaded <name> (<stamp>) — press Sync sources to import it.``
— the SAFE name (the 202's ``UploadAccepted.name`` — the
phase-90 contract) + the phase-154 COMPLETION STAMP
(``fmtUploadStamp`` off the server payload's ``finished_at`` —
the shape pin: same-day ``HH:MM`` in practice, the date-qualified
forms covered by the shape regex). The never-stale restore
(button + input) lands with the line, the terminal status is the
no-count payload (``{"message": "uploaded"}``, null/0/0 — the
phase-90 A2 key set) with a NON-NULL ``finished_at`` (the stamp's
source), the source row is present (Local badge), and the
unpacked folder exists on the host (the run did its unpack +
register work — zero documents indexed: the scan is the Sync
button's job, phase 90, A1)."""
page.set_default_timeout(30_000)
_admin_git_sources_page(page, app_url)
expect(page.locator("#git-sources-tbody tr")).to_have_count(0)
btn = page.locator("#archive-upload-btn")
expect(btn).to_be_enabled()
expect(btn).to_have_text("Upload")
_upload_via_page(page, tarball_a)
# The 202's toast names the safe source name (phase 64, A2)…
toast = page.locator(".toast")
expect(toast).to_have_attribute("role", "status")
expect(toast).to_have_text(f"Successfully uploaded — {UPLOAD_A}")
# …and the settle: the ready-for-sync line WITH the name and the
# completion stamp (phase 154, task 02 — the shape pin).
result = page.locator("#archive-upload-result")
expect(result).to_be_visible(timeout=UPLOAD_TIMEOUT_MS)
expect(result).to_have_text(_ready_line(UPLOAD_A))
# The stamp is present and non-empty (the regex's capture is the
# stamp itself — the pre-phase string has no "(…)" at all, so a
# match IS the stamp's proof).
m = _ready_line(UPLOAD_A).fullmatch(result.text_content() or "")
assert m is not None, "the settled line did not match the named + stamped shape"
assert m.group(1), "the stamp is empty in the settled line"
# The never-stale restore (the poll's success tick owns it)…
expect(btn).to_be_enabled()
expect(btn).to_have_text("Upload")
expect(page.locator("#archive-upload-file")).to_have_value("")
# …and the terminal status: the no-count payload (phase 90, A2)
# with the NON-NULL finished_at the stamp is computed from.
status = _status(page, app_url, "/api/git-sources/upload/status")
assert status["state"] == "success", status
assert status["detail"] == {"message": "uploaded"}, status
assert status["current_file"] is None, status
assert status["files_done"] == 0 and status["files_total"] == 0, status
assert status["finished_at"], status
assert status["started_at"], status
# The row landed (Local badge) and the folder is on the host —
# but the upload indexed NOTHING (phase 90, A1 — the scan is the
# Sync button's job).
row = page.locator("#git-sources-tbody tr", has_text=UPLOAD_A)
expect(row).to_have_count(1, timeout=30_000)
expect(row.locator("span.git-source-kind")).to_have_text("Local")
assert (upload_dir / UPLOAD_A).is_dir(), (
f"the unpacked folder is missing: {upload_dir / UPLOAD_A}"
)
r = page.request.get(f"{app_url}/api/docs")
assert r.status == 200, r.text
assert r.json()["documents"] == []
# ---------------------------------------------------------------------------
# 3. The boot re-render is nameless but STAMPED — the phase's actual
# fix (the stale "Uploaded" line is self-dating after a reload)
# ---------------------------------------------------------------------------
def test_upload_line_boot_rerender_stamped_nameless(
page: Page,
app_url: str,
db_ready: None,
tarball_b: Path,
) -> None:
"""Seed a settled upload (the fresh named line — the run's
identity pinned on the status's ``started_at``), then RELOAD the
page: the boot re-attach (``initUploadStatus``'s success branch —
phase 64, task 05) re-renders the ready-for-sync line — the SAFE
NAME IS GONE (``lastUploadName`` was page-local, null after a
reload — the phase-90 contract) but the STAMP SURVIVES
(``fmtUploadStamp`` reads the SERVER payload's ``finished_at`` —
phase 154, task 02 — the line is self-dating even with the name
unknown; THIS is the regression the phase fixes: a line that
outlives its upload can no longer masquerade as fresh). The page
boots honest (no error banner, no toast — it fired at the 202 on
the previous document life — the button restored), the list shows
exactly one row for the archive, and the terminal status is the
SAME run (the ``started_at`` is unchanged — no second upload)."""
page.set_default_timeout(30_000)
_admin_git_sources_page(page, app_url)
# The seed: a settled upload (the fresh named line with the
# stamp — test 2's shape, re-proven here as the pre-reload state).
_upload_via_page(page, tarball_b)
expect(page.locator(".toast")).to_have_text(
f"Successfully uploaded — {UPLOAD_B}"
)
result = page.locator("#archive-upload-result")
expect(result).to_be_visible(timeout=UPLOAD_TIMEOUT_MS)
expect(result).to_have_text(_ready_line(UPLOAD_B))
# The run's identity (a second upload would reset it — the
# single-run claim is pinned on it).
before = _status(page, app_url, "/api/git-sources/upload/status")
assert before["state"] == "success", before
started_at = before["started_at"]
assert started_at, before
# The reload (the session cookie survives — the boot re-attach is
# the admin's).
page.reload()
expect(page).to_have_url(app_url + GIT_SOURCES_URL, timeout=30_000)
expect(page.locator("#sign-out-btn")).to_be_visible(timeout=15_000)
expect(page.locator("#git-sources-content")).to_be_visible()
# The boot re-render: the NAMELESS line WITH the stamp (phase
# 154, task 02 — the stamp from the server payload). The
# nameless shape has NO "(…)" and NO <name> — the regex's match
# IS the nameless+stamped proof (the pre-phase nameless string
# "Uploaded — press …" has no stamp at all).
expect(result).to_have_text(NAMELESS_READY_LINE, timeout=UPLOAD_TIMEOUT_MS)
m = NAMELESS_READY_LINE.fullmatch(result.text_content() or "")
assert m is not None, "the boot re-render line did not match the nameless + stamped shape"
assert m.group(1), (
"the stamp is empty in the boot re-render line"
)
# The page boots honest: no error banner, no toast at boot (A2 —
# the toast fired at the 202 on the previous document life), the
# never-stale restore (button re-enabled / "Upload").
expect(page.locator("#archive-upload-error")).to_be_hidden()
expect(page.locator(".toast")).to_have_count(0)
expect(page.locator("#archive-upload-btn")).to_be_enabled()
expect(page.locator("#archive-upload-btn")).to_have_text("Upload")
# The list shows exactly one row for the archive (the in-place
# identity survived the reload).
expect(page.locator("#git-sources-tbody tr")).to_have_count(1)
row = page.locator("#git-sources-tbody tr", has_text=UPLOAD_B)
expect(row).to_have_count(1)
expect(row.locator("span.git-source-kind")).to_have_text("Local")
# No second upload: the terminal status is the SAME run — its
# started_at is the one from before the reload.
terminal = _status(page, app_url, "/api/git-sources/upload/status")
assert terminal["state"] == "success", terminal
assert terminal["started_at"] == started_at, (
f"the run's identity changed (a second upload ran): {terminal['started_at']}"
)
assert terminal["detail"] == {"message": "uploaded"}, terminal
assert terminal["finished_at"], terminal
# ---------------------------------------------------------------------------
# 4. A running upload never renders the ready-for-sync line (the
# phase-90 regression guard, extended — the line appears ONLY on
# settle)
# ---------------------------------------------------------------------------
def test_running_upload_line_unchanged(
page: Page,
app_url: str,
db_ready: None,
tarball_c: Path,
) -> None:
"""At the 202 moment the upload run is in flight: the button is
the BARE "Processing…" (phase 90, A2 — no file, no "(n/m)"
counts, the title cleared) and ``#archive-upload-result`` is
HIDDEN — the ready-for-sync line NEVER renders during the run
(the phase-90 contract: the line appears ONLY on settle). The
deterministic in-flight window: the 202 handler synchronously
enters the processing state and schedules the first poll tick at
+2 s, so the "Processing…" state holds for ≥ 2 s regardless of
how fast the sub-second background run finishes — no delay proxy
is needed (the upload makes zero LLM calls). Then the poll's
success tick settles the line (the NAMED shape with the
completion stamp — the line appears only on settle), with the
never-stale restore and no error UI throughout."""
page.set_default_timeout(30_000)
_admin_git_sources_page(page, app_url)
btn = page.locator("#archive-upload-btn")
expect(btn).to_be_enabled()
expect(btn).to_have_text("Upload")
expect(page.locator("#archive-upload-error")).to_be_hidden()
# The 202 moment (the toast fires at the 202 — the run is in
# flight from here until the poll's success tick). The submit
# handler hid the result line before the fetch (a new attempt
# starts clean), so at the in-flight moment the line is hidden
# even though the shared module app's boot re-render may have
# re-shown the PREVIOUS test's settled line at page load (the
# phase-154 boot re-attach — the line's self-dating contract,
# test 3's subject — not a partial-state regression).
_upload_via_page(page, tarball_c)
expect(page.locator(".toast")).to_have_text(
f"Successfully uploaded — {UPLOAD_C}"
)
# The IN-FLIGHT state (the ≥ 2 s window — deterministic): the
# bare "Processing…" label (phase 90, A2), the button disabled
# (the poll owns it — PLAN §7.4), the title cleared, and the
# ready-for-sync line HIDDEN (the line appears ONLY on settle).
expect(btn).to_be_disabled()
expect(btn).to_have_text("Processing…")
expect(btn).to_have_attribute("title", "")
expect(page.locator("#archive-upload-result")).to_be_hidden()
expect(page.locator("#archive-upload-error")).to_be_hidden()
# The in-flight status: the bare payload (phase 90, A2 — the
# run's unpack + register is in flight or just finished; either
# way the line is the poll tick's job, not the in-flight state's).
running = _status(page, app_url, "/api/git-sources/upload/status")
assert running["state"] in ("running", "success"), running
# The SETTLE (the poll's success tick — the line appears ONLY
# here, and the line is THIS run's — the named shape with the
# completion stamp; the distinct safe name also proves the
# previous test's boot re-render line was replaced, not carried
# through) + the never-stale restore, no error UI.
expect(page.locator("#archive-upload-result")).to_have_text(
_ready_line(UPLOAD_C), timeout=UPLOAD_TIMEOUT_MS
)
expect(btn).to_be_enabled()
expect(btn).to_have_text("Upload")
expect(page.locator("#archive-upload-error")).to_be_hidden()
status = _status(page, app_url, "/api/git-sources/upload/status")
assert status["state"] == "success", status
assert status["detail"] == {"message": "uploaded"}, status
assert status["finished_at"], status
+34 -5
View File
@@ -16,8 +16,9 @@ unpack → swap → row upsert continues server-side behind
pattern; the phase-64 key set with ``current_file``/``files_done``/
``files_total`` null/0/0 for the whole run — phase 90 A2), and the
upload's UI processing state is the BARE "Processing…" (no file, no
"(n/m)") until the no-count "Uploaded <name> — press Sync sources to
import it." result line lands (phase 90 A3). The scan — with its live
"(n/m)") until the no-count "Uploaded <name> (<stamp>) — press Sync
sources to import it." result line lands (phase 90 A3 — the phase-154
completion stamp). The scan — with its live
file label — is the RAG page's **Sync sources** button's job, and the
suite proves the new loop: upload → nothing indexed → **the sync that
follows the upload shows its live file label and lands the counts**.
@@ -159,6 +160,31 @@ SLOW_DELAY_S = "0.15"
#: The uploaded archive: 25 small docs under ``docs/``.
UPLOAD_NAME = "e2e-prog"
def _ready_for_sync_line(name: str) -> re.Pattern[str]:
"""Phase 154 (task 02): the ready-for-sync line carrying the
completion stamp — "Uploaded <name> (<stamp>) — press Sync sources
to import it." (the fresh 202 settle, the page-local name). In
E2E the stamp is ALWAYS the same-day ``HH:MM`` form — the run
settles within seconds (the date-qualified forms are unit-pinned
on a fixed clock — E2E never depends on the wall clock)."""
return re.compile(
rf"^Uploaded {re.escape(name)} \(\d{{1,2}}:\d{{2}}\)"
r" — press Sync sources to import it\.$"
)
def _nameless_ready_for_sync_line() -> re.Pattern[str]:
"""Phase 154 (task 02): the boot re-render line — nameless (the
safe name was page-local — ``lastUploadName`` is null after a
reload, phase 90) but SELF-DATING through the completion stamp
(``fmtUploadStamp`` from the server payload): "Uploaded <stamp> —
press Sync sources to import it." (the same-day ``HH:MM`` form in
E2E)."""
return re.compile(
r"^Uploaded \d{1,2}:\d{2} — press Sync sources to import it\.$"
)
UPLOAD_ARCHIVE = f"{UPLOAD_NAME}.tar.gz"
N_FILES = 25
UPLOAD_FILES: dict[str, str] = {
@@ -747,7 +773,7 @@ def test_upload_progress_is_bare(
# the archive — and the KB empty (no scan, phase 90 A1).
result = page.locator("#archive-upload-result")
expect(result).to_have_text(
f"Uploaded {UPLOAD_NAME} — press Sync sources to import it.",
_ready_for_sync_line(UPLOAD_NAME),
timeout=30_000,
)
expect(btn).to_be_enabled()
@@ -915,9 +941,12 @@ def test_upload_reattach_after_reload(
expect(page.locator("#archive-upload-error")).to_be_hidden()
expect(page.locator(".toast")).to_have_count(0)
# …and the settled result line: the nameless variant (the safe
# name was page-local — lastUploadName is null after a reload).
# name was page-local — lastUploadName is null after a reload),
# self-dating through the completion stamp (phase 154 — the stamp
# comes from the server payload, so the line is still self-dating
# with the name unknown).
expect(page.locator("#archive-upload-result")).to_have_text(
"Uploaded — press Sync sources to import it.",
_nameless_ready_for_sync_line(),
timeout=SETTLE_TIMEOUT_MS,
)
expect(page.locator("#archive-upload-btn")).to_be_enabled()
+27 -13
View File
@@ -54,8 +54,9 @@ Contract under test:
* **test 1 — upload does not scan**: the "Upload" button (phase 90 A3)
→ the archive → the 202 "Successfully uploaded — <source>" toast →
the settled result line "Uploaded <source> — press Sync sources to
import it." with the terminal no-count status payload
the settled result line "Uploaded <source> (<stamp>) — press Sync
sources to import it." (the phase-154 completion stamp) with the
terminal no-count status payload
(``{"message": "uploaded"}``, null/0/0 progress — phase 90 A2); the
source row is present (Local badge + the phase-89 "Ignore paths"
control) and its folder exists on the host under ``BOR_UPLOAD_DIR``
@@ -127,6 +128,19 @@ SOURCES_URL = "/sources.html"
#: filename minus the archive suffix (the phase-49 naming rule).
SOURCE_NAME = "e2e-upload-no-scan"
def _ready_for_sync_line(name: str) -> re.Pattern[str]:
"""Phase 154 (task 02): the ready-for-sync line carrying the
completion stamp — "Uploaded <name> (<stamp>) — press Sync sources
to import it." (the fresh 202 settle, the page-local name). In
E2E the stamp is ALWAYS the same-day ``HH:MM`` form — the run
settles within seconds (the date-qualified forms are unit-pinned
on a fixed clock — E2E never depends on the wall clock)."""
return re.compile(
rf"^Uploaded {re.escape(name)} \(\d{{1,2}}:\d{{2}}\)"
r" — press Sync sources to import it\.$"
)
#: v1: two sentinel docs + the notes/ file the phase-89 ignore entry
#: (``notes``) excludes from the sync. v2 (same basename): beta CHANGED,
#: alpha DROPPED, gamma ADDED — the in-place-replace subject (the
@@ -394,7 +408,8 @@ def test_upload_does_not_scan(
"""One real upload through the page: the button reads **Upload**
(phase 90 A3); the 202 fires the "Successfully uploaded — <source>"
toast; the settled result line points at the next step — "Uploaded
<source> — press Sync sources to import it." (A3) — with the
<source> (<stamp>) — press Sync sources to import it." (A3 — the
phase-154 completion stamp) — with the
terminal no-count status payload (``{"message": "uploaded"}``,
null/0/0 progress — A2). The source row is present (Local badge +
the phase-89 "Ignore paths" control) and its folder exists on the
@@ -428,9 +443,7 @@ def test_upload_does_not_scan(
# never-stale restore (button + input)…
result = page.locator("#archive-upload-result")
expect(result).to_be_visible(timeout=UPLOAD_TIMEOUT_MS)
expect(result).to_have_text(
f"Uploaded {SOURCE_NAME} — press Sync sources to import it."
)
expect(result).to_have_text(_ready_for_sync_line(SOURCE_NAME))
expect(btn).to_be_enabled()
expect(btn).to_have_text("Upload")
expect(page.locator("#archive-upload-file")).to_have_value("")
@@ -507,9 +520,10 @@ def test_ignore_list_then_sync_scans(
page.set_default_timeout(30_000)
_admin_git_sources_page(page, app_url)
# Upload: the ready-for-sync line, and still nothing indexed.
assert _upload_via_page(page, tarball_v1) == (
f"Uploaded {SOURCE_NAME} — press Sync sources to import it."
# Upload: the ready-for-sync line (the phase-154 completion
# stamp), and still nothing indexed.
assert _ready_for_sync_line(SOURCE_NAME).fullmatch(
_upload_via_page(page, tarball_v1)
)
assert _docs(page, app_url) == []
expect(page.locator("#git-sources-tbody tr", has_text=SOURCE_NAME)).to_have_count(1)
@@ -622,8 +636,8 @@ def test_reupload_replaces_without_scan(
# Baseline: v1 through the page (202 → the ready-for-sync line,
# one row, no index).
assert _upload_via_page(page, tarball_v1) == (
f"Uploaded {SOURCE_NAME} — press Sync sources to import it."
assert _ready_for_sync_line(SOURCE_NAME).fullmatch(
_upload_via_page(page, tarball_v1)
)
expect(page.locator("#git-sources-tbody tr", has_text=SOURCE_NAME)).to_have_count(1)
folder = upload_dir / SOURCE_NAME
@@ -632,8 +646,8 @@ def test_reupload_replaces_without_scan(
# Re-upload v2 — SAME basename, different parent dir (the file
# input's selection is replaced wholesale).
assert _upload_via_page(page, tarball_v2) == (
f"Uploaded {SOURCE_NAME} — press Sync sources to import it."
assert _ready_for_sync_line(SOURCE_NAME).fullmatch(
_upload_via_page(page, tarball_v2)
)
# No duplicate: exactly ONE row for that source (and one row