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

833 lines
36 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 186 (task 03) E2E (Playwright, mock-only): the dates the owner
reads come through the STREAMED commit-date walk.
Phase 186 changed the TRANSPORT of the git date walk, nothing else:
``file_commit_dates`` used to buffer ``git log --name-only
--format=@@%cI`` as ONE string (346,829 lines / 15 MB for the incident
box's litellm checkout) and hand it to a parser that materialised a
second copy with ``splitlines()``; it now parses the pipe line by line
through ``stream_git_lines`` (the second, documented spawn site beside
``run_git``). This module is the owner-visible half of that promise —
the phase-106 D2 contract (every document's date is that FILE'S
last-commit date) re-pinned through the browser with the streaming walk
in place, plus the two neighbours the streaming move could plausibly
have broken: the mtime fallback and the phase-106 D1 manual-date lock.
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_git_log_stream_dates.py -v --no-cov
**Fixture repo** — a real two-commit ``file://`` repo (the
``test_git_source_dates.py`` ``_git``/``_commit`` shape, with
``GIT_AUTHOR_DATE``/``GIT_COMMITTER_DATE`` pinned per commit so the
dates are exact and stable):
* commit A @ ``2026-01-05T12:00:00Z`` adds ``notes/old.md`` (never
touched again), ``notes/changed.md`` and ``notes/café.md``;
* commit B @ ``2026-03-20T12:00:00Z`` modifies ``notes/changed.md``
ONLY — the one file whose date distinguishes "last commit" from "the
repo tip" and from "when the sync ran";
* both dates are MID-DAY (12:00Z) so the local calendar DAY of the
instant is the committed day from UTC−12 to UTC+11, and the YEAR is
2026 in every timezone on earth — the visible-text assertions only
ever compare the two cells against EACH OTHER, every exact assertion
rides the ``title`` / API ISO (the house locale-stable idiom, the
``test_document_dates.py`` L455 lesson).
**The uncommitted file — written into the CHECKOUT.** ``notes/untracked.md``
is seeded in the app's own checkout (``BOR_SOURCES_DIR/<repo_name(url)>``),
NOT in the fixture repository: the sync never re-clones (``clone_or_pull``
fetches + ``merge --ff-only`` the existing checkout, and git never touches
an untracked file), so a file placed there survives every later sync —
exactly the owner's "a file git has never committed" case. Because the
first test's sync is the one that creates the checkout, the seed helper
creates it first with a real ``git clone`` when it is still missing, so
any single test of this module also passes standalone (``-k``). Its mtime
is pinned with ``os.utime`` to ``2026-02-14T09:30:00Z`` — between the two
commits, so a stored date of ``2026-02-14`` can only be the mtime and can
never be confused with a commit date or with the run's own day. The same
helper re-pins ``notes/café.md``'s mtime to ``2026-04-11T08:00:00Z``
(AFTER both commits) for the quoted-path test; git re-hashes but never
rewrites a file whose content matches, so the pin survives every sync.
**Non-ASCII / ``core.quotepath``-quoted paths — recorded as-is, pinned
UNCHANGED (measured on the authoring box, git 2.51, 2026-10-16).** With
the default ``core.quotepath=true`` a full-history ``git log
--name-only --format=@@%cI`` prints ``notes/café.md`` as the eight-bit-
escaped, quote-wrapped line ``"notes/caf\\303\\251.md"``, and the parser
(whose grammar phase 186 did NOT touch — only how the bytes arrive) turns
it into the key ``'"notes/caf/303/251.md"'``: ``line.split()`` keeps the
quotes, ``replace("\\\\", "/")`` turns each octal backslash into a slash,
``lstrip("/")`` cannot strip the leading ``"``. That key never matches the
importer's ``dates_map.get(rel)``, so a non-ASCII path falls back to its
FILE MTIME today — before and after this phase, byte-identically (the
streaming site passes the same bytes through, and
``test_quoted_nonascii_path_falls_back_exactly_as_before`` pins both
halves: the mangled key IN the walk's dict, and the document showing
its mtime instead of commit A).
Fixing it means changing the parser's quoting semantics (e.g. ``-c
core.quotepath=false``, or NUL-separated paths), which is a DATE change
for every non-ASCII document already in a KB — out of scope for a
transport phase, recorded here for a future one (LOCKED A1: this phase
changes how the bytes arrive, never which dates the KB stores).
Test → contract mapping (five tests):
1. ``test_document_dates_are_the_last_commit_dates`` — after a real
sync of the fixture repo, ``notes/old.md`` carries commit A
(``2026-01-05``) and ``notes/changed.md`` carries commit B
(``2026-03-20``) — through ``GET /api/docs`` + ``GET
/api/documents/content`` (the per-document surface; there is no
``GET /api/docs/{id}``), through the RAG table's ``Created`` cell
(full ISO on the ``title``, and the two cells' visible text
differs), and through the viewer's ``.doc-created`` badge. Neither
date is the run's own day, and both differ from the row's
``indexed_at`` (which IS the sync/clone day).
2. ``test_uncommitted_file_falls_back_to_mtime`` — the file git has
never committed is stored at its file mtime (the fail-soft contract
reached through the streaming walk, asserted against the same
``file_mtime_datetime``/``normalize_doc_date`` the importer uses),
and the run reaches ``success`` with no error surface.
3. ``test_manual_date_survives_a_resync`` — the D1 lock: set a date in
the viewer's editor, re-sync, the manual date is untouched while the
OTHER documents keep coming from the stream (``dates_updated=0``,
the stream re-derived the same values, and only the manual row is
flagged in the DB).
4. ``test_resync_is_unchanged_and_dates_hold`` — a second sync with
nothing changed reports everything ``unchanged`` (0 added / 0
updated / 0 pruned) with a byte-identical date map: the stream
re-parses, it does not drift.
5. ``test_quoted_nonascii_path_falls_back_exactly_as_before`` — the
UNCHANGED non-ASCII behaviour described above (module docstring),
pinned through the stream.
Isolation (the house per-module pattern): a dedicated app port, the
deterministic mock LLM (``E2E_REAL_LLM=1`` is SKIPPED — the dates come
from git, not from the model), a dedicated ``BOR_SOURCES_DIR`` for the
checkout, ``BOR_GIT_SOURCES=file://<fixture repo>`` as the env-fallback
source row, and the shared E2E Postgres truncated before AND after every
test (with a wait for any in-flight run first — the run state lives in
the module app's memory).
"""
from __future__ import annotations
import os
import re
import subprocess
import sys
import time
from collections.abc import Callable, 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 select, text
from app.db import SessionLocal
from app.models import Document
from app.rag.doc_dates import file_mtime_datetime, normalize_doc_date
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
from scripts.git_sync import file_commit_dates
from scripts.import_docs import repo_name
REPO = Path(__file__).resolve().parents[2]
#: A per-module app on its own port (the conftest session app owns 8123
#: in a combined run — a same-port second uvicorn would drive the wrong
#: server). Env-overridable, the ``test_git_source_dates`` pattern.
APP_PORT = int(os.environ.get("E2E_APP_PORT_STREAMDATES", "8203"))
APP_URL = f"http://127.0.0.1:{APP_PORT}"
# ---------------------------------------------------------------------------
# Fixture constants (deterministic — see the module docstring)
# ---------------------------------------------------------------------------
#: The fixture repo's directory name — the source name the import records
#: (the ``repo_name`` basename rule, phase 28) and the checkout dir name.
REPO_NAME = "git-stream-dates"
OLD_DOC = "notes/old.md"
CHANGED_DOC = "notes/changed.md"
#: The non-ASCII path, quoted by ``core.quotepath`` in the walk (the
#: module docstring's UNCHANGED-behaviour record).
QUOTED_DOC = "notes/café.md"
#: Never committed — seeded into the CHECKOUT by ``seed_checkout``.
UNTRACKED_DOC = "notes/untracked.md"
#: The two controlled commit dates (both instants, mid-day: the local
#: calendar DAY is the committed day for every timezone the CI can run
#: in, the YEAR is stable everywhere — the ``test_document_dates.py`` L455
#: lesson).
COMMIT_A = "2026-01-05T12:00:00Z"
COMMIT_B = "2026-03-20T12:00:00Z"
#: The same instants as stored (``%cI`` → ``datetime.fromisoformat`` →
#: ``normalize_doc_date`` → Postgres → ``isoformat()``) — the exact ISO
#: the API, the ``Created`` cell ``title`` and the badge ``title`` carry.
COMMIT_A_ISO = "2026-01-05T12:00:00+00:00"
COMMIT_B_ISO = "2026-03-20T12:00:00+00:00"
#: The pinned mtimes (``os.utime``, whole seconds — so the stored ISO is
#: exact). The untracked file sits BETWEEN the two commits; the quoted
#: file AFTER both, so a fallback date can never be mistaken for one of
#: the commit dates above.
UNTRACKED_MTIME = datetime(2026, 2, 14, 9, 30, tzinfo=UTC)
UNTRACKED_MTIME_ISO = "2026-02-14T09:30:00+00:00"
QUOTED_MTIME = datetime(2026, 4, 11, 8, 0, tzinfo=UTC)
QUOTED_MTIME_ISO = "2026-04-11T08:00:00+00:00"
#: The owner's correction (the date-editor surface, phase 106 D1/D7).
EDITED_ISO = "2021-05-05T00:00:00+00:00"
#: The walk's key for ``notes/café.md`` under the default
#: ``core.quotepath=true`` — the measured, UNCHANGED mangle (module
#: docstring): the quotes survive ``split()``, every octal backslash
#: becomes a slash, and the leading quote survives ``lstrip("/")``.
QUOTED_WALK_KEY = '"notes/caf/303/251.md"'
#: Every committed file (the checkout before ``seed_checkout`` runs).
COMMITTED_DOCS = (OLD_DOC, CHANGED_DOC, QUOTED_DOC)
#: The untracked file's body (its content is irrelevant — only its
#: existence in the working tree and its pinned mtime matter).
UNTRACKED_BODY = (
"# Not in git\n\nA working-tree file the repository has never committed: its "
"only date signal is the file's own mtime.\n"
)
#: "Synced HH:MM" (``sources.js`` ``fmtSyncTime``) and the idle/boot label.
SYNCED_LABEL = re.compile(r"Synced \d{1,2}:\d{2}")
IDLE_LABEL = re.compile(r"^Sync sources$|^Synced \d{1,2}:\d{2}$")
#: A real clone + mock import of three/four small markdown files settles
#: in ~2 s; the budgets below are ~15× that (no client timeout on the run).
RUN_SETTLE_TIMEOUT_S = 90.0
UI_SETTLE_TIMEOUT_MS = 20_000
# ---------------------------------------------------------------------------
# The fixture repo (module-scoped — real git, two controlled commits)
# ---------------------------------------------------------------------------
def _git(cwd: Path, *args: str, env: dict[str, str] | None = None) -> str:
"""Run git in *cwd*; a non-zero exit fails the fixture loudly.
*env* overrides (``GIT_AUTHOR_DATE`` / ``GIT_COMMITTER_DATE`` — the
phase's whole point) ride on top of a copy of ``os.environ`` so the
fixture never mutates the test process's environment.
"""
full_env = dict(os.environ)
if env:
full_env.update(env)
proc = subprocess.run(
["git", *args], cwd=cwd, capture_output=True, text=True, env=full_env
)
if proc.returncode != 0:
raise AssertionError(f"git {' '.join(args)} failed: {proc.stderr.strip()}")
return proc.stdout
def _commit(cwd: Path, message: str, when: str) -> None:
"""``git add -A`` + a commit with BOTH dates pinned to *when*.
``file_commit_dates`` reads ``%cI`` (the COMMITTER date); the author
date is pinned too so the fixture carries one unambiguous date per
commit (the ``test_git_source_dates.py`` helper, verbatim in shape).
"""
_git(cwd, "add", "-A")
_git(
cwd,
"-c", "user.email=e@x", "-c", "user.name=t",
"-c", "commit.gpgsign=false", # the fixture commits never sign
"commit", "-qm", message,
env={"GIT_AUTHOR_DATE": when, "GIT_COMMITTER_DATE": when},
)
def _write(root: Path, rel: str, body: str, mtime: datetime | None = None) -> Path:
"""Write *body* to ``root/rel`` and optionally pin its mtime."""
path = root / rel
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(body, encoding="utf-8")
if mtime is not None:
os.utime(path, (mtime.timestamp(), mtime.timestamp()))
return path
@pytest.fixture(scope="module")
def stream_repo(tmp_path_factory: pytest.TempPathFactory) -> Path:
"""The real two-commit repo the sync clones over ``file://``.
Commit A (2026-01-05) adds the three committed notes; commit B
(2026-03-20) touches ``notes/changed.md`` ONLY — the file whose
stored date therefore has to be 2026-03-20 while its sibling stays
2026-01-05 (the phase-106 D2 first-sighting-wins contract).
"""
root = tmp_path_factory.mktemp("bor_git_log_stream")
repo = root / REPO_NAME
_write(repo, OLD_DOC, "# Old note\n\nWritten once in January, never touched again.\n")
_write(repo, CHANGED_DOC, "# Changed note\n\nFirst version (January).\n")
_write(repo, QUOTED_DOC, "# Café note\n\nA path with a non-ASCII character.\n")
_git(repo, "init", "-q", "-b", "main")
_commit(repo, "january notes", COMMIT_A)
_write(repo, CHANGED_DOC, "# Changed note\n\nSecond version (March) — the last commit.\n")
_commit(repo, "march edit", COMMIT_B)
assert (repo / ".git").is_dir()
assert _git(repo, "rev-list", "--count", "HEAD").strip() == "2"
return repo
# ---------------------------------------------------------------------------
# The app under test (module-scoped env, the test_git_source_dates shape)
# ---------------------------------------------------------------------------
def _sources_dir(stream_repo: Path) -> Path:
"""The module's dedicated ``BOR_SOURCES_DIR`` — ONE definition for
the app fixture and the checkout path, so the two cannot drift."""
return stream_repo.parent / "checkouts"
@pytest.fixture(scope="module")
def app_server(mock_llm: int, stream_repo: Path) -> Iterator[str]:
"""The real app under test: one ``file://`` git source (the
env-fallback row) checked out under its own fresh dir."""
if USE_REAL_LLM:
pytest.skip(
"mock-only suite — the dates come from real git and the importer, "
"not from a chat answer (E2E_REAL_LLM unsupported)"
)
env = dict(os.environ)
env.pop("DEBUGPY", None)
env["BOR_ENVIRONMENT"] = "e2e"
env["BOR_STATIC_DIR"] = str(REPO / "frontend")
env["BOR_LLM_BASE_URL"] = f"http://127.0.0.1:{mock_llm}/v1"
# Mock-calibrated threshold (conftest pattern) — no chat turn is sent
# here, but the app boots with the same env shape.
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
# The subject: one real local repo, its own checkout home.
env["BOR_GIT_SOURCES"] = f"file://{stream_repo}"
env["BOR_SOURCES_DIR"] = str(_sources_dir(stream_repo))
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
@pytest.fixture(scope="module")
def checkout(app_server: str, stream_repo: Path) -> Path:
"""Where the run clones to — ``BOR_SOURCES_DIR/<repo_name(url)>``,
resolved with the PRODUCTION helper so this module cannot drift from
the phase-28 rule (the ``test_sync_git_freeze.py`` shape)."""
assert app_server # the app must be up — its env is what owns the path
return _sources_dir(stream_repo) / repo_name(f"file://{stream_repo}")
# ---------------------------------------------------------------------------
# Isolation + the checkout seeding helper
# ---------------------------------------------------------------------------
def _truncate_all() -> None:
"""Fresh registry + KB (the E2E isolation pattern): a leftover
``git_sources`` row would win over the app's ``BOR_GIT_SOURCES`` env
fallback (``effective_sources`` prefers the DB list), and every test's
sync counts must be its own."""
with SessionLocal() as db:
db.execute(
text(
"TRUNCATE chunks, documents, query_log, kb_overview, "
"git_sources, sources_meta, folder_summaries"
)
)
db.commit() # without the commit the TRUNCATE rolls back (house pattern)
def _wait_sync_idle(app_url: str, timeout_s: float = RUN_SETTLE_TIMEOUT_S) -> None:
"""No run may leak across a test boundary (the run state lives in the
module app's memory): wait for a non-running status BEFORE truncating.
Own admin session — the endpoint is admin-only."""
with httpx.Client(base_url=app_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:
if client.get("/api/sync/status").json()["state"] != "running":
return
time.sleep(0.25)
raise AssertionError(f"a sync was still running at a test boundary: {app_url}")
@pytest.fixture(autouse=True)
def _clean(app_url: str, db_ready: None) -> Iterator[None]:
"""Per-test KB isolation, before AND after (the phase-179 pattern)."""
_wait_sync_idle(app_url)
_truncate_all()
yield
_truncate_all()
@pytest.fixture(scope="module")
def seed_checkout(
checkout: Path, stream_repo: Path
) -> Callable[[], Path]:
"""``seed_checkout()`` → the seeded checkout, ready for a sync.
Writes ``notes/untracked.md`` into the CHECKOUT (never into the
fixture repo — a clone does not carry untracked files, and the sync
only ever fetch/ff-merges the existing checkout, so the file
survives every later sync), pins its mtime, and re-pins the quoted
file's mtime. When the checkout does not exist yet (a single test run
with ``-k``, before the app has cloned anything) it is created first
with a real ``git clone`` — the same working tree the sync would
have produced.
"""
def _seed() -> Path:
if not checkout.exists():
checkout.parent.mkdir(parents=True, exist_ok=True)
_git(checkout.parent, "clone", "-q", f"file://{stream_repo}", str(checkout))
_write(checkout, UNTRACKED_DOC, UNTRACKED_BODY, UNTRACKED_MTIME)
_write(
checkout,
QUOTED_DOC,
(stream_repo / QUOTED_DOC).read_text(encoding="utf-8"),
QUOTED_MTIME,
)
# The seed is the fixture: a broken checkout fails here, loudly,
# instead of turning a date assertion into a mystery.
assert (checkout / UNTRACKED_DOC).is_file()
assert file_mtime_datetime(checkout / UNTRACKED_DOC) == UNTRACKED_MTIME
return checkout
return _seed
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _status(page: Page) -> dict[str, Any]:
r = page.request.get(f"{APP_URL}/api/sync/status")
assert r.status == 200, r.text
return r.json()
def _wait_status_terminal(
page: Page, timeout_s: float = RUN_SETTLE_TIMEOUT_S
) -> dict[str, Any]:
"""Poll until the run leaves ``running`` (the deterministic layer
under the UI's own 2 s poll)."""
deadline = time.monotonic() + timeout_s
body: dict[str, Any] = {}
while time.monotonic() < deadline:
body = _status(page)
if body["state"] != "running":
return body
time.sleep(0.1)
raise AssertionError(f"the sync never left `running`: {body}")
def _run_sync(page: Page, expected_line: str | re.Pattern[str]) -> dict[str, Any]:
"""The owner's click, end to end: Sources page → "Sync sources" →
the live "Syncing…" state → the terminal ``success`` (status API) →
the settled UI (retry-ready button, the result counts line, no error
surface). Returns the run's ``detail`` counts.
The caller is already logged in (``login`` lands on /sources.html).
"""
btn = page.locator("#sync-btn")
expect(btn).to_be_visible()
expect(btn).to_be_enabled()
expect(page.locator("#sync-label")).to_have_text(IDLE_LABEL)
btn.click()
expect(btn).to_be_disabled()
expect(page.locator("#sync-label")).to_have_text("Syncing…")
body = _wait_status_terminal(page)
assert body["state"] == "success", body
assert body["error"] is None, body
expect(page.locator("#sync-label")).to_have_text(SYNCED_LABEL, timeout=UI_SETTLE_TIMEOUT_MS)
expect(btn).to_be_enabled() # never stale at the terminal state
expect(page.locator("#sync-error-banner")).to_be_hidden()
expect(page.locator("#sync-progress-text")).to_have_text(
counts_line(expected_line), timeout=UI_SETTLE_TIMEOUT_MS
)
return dict(body["detail"] or {})
def _docs(page: Page) -> dict[str, dict[str, Any]]:
"""``GET /api/docs`` keyed by path (the browser's admin cookie rides)."""
r = page.request.get(f"{APP_URL}/api/docs")
assert r.status == 200, r.text
return {d["path"]: d for d in r.json()["documents"]}
def _content(page: Page, path: str) -> dict[str, Any]:
"""``GET /api/documents/content`` — the per-document surface (there is
no ``GET /api/docs/{id}``; this is the route the viewer itself uses)."""
r = page.request.get(
f"{APP_URL}/api/documents/content",
params={"source": REPO_NAME, "path": path},
)
assert r.status == 200, r.text
return r.json()
def _date_map(docs: dict[str, dict[str, Any]]) -> dict[str, str]:
return {p: d["created_at"] for p, d in docs.items()}
def _drill_to_notes(page: Page, rows: int) -> None:
"""Top level → the source → ``notes`` (the two real clicks) — and the
file table holds exactly *rows* documents (the whole checkout, the
``.git`` directory excluded by the hidden-path rule)."""
page.locator("#folders-tbody .folder-link").first.wait_for(state="visible")
page.click(f'#folders-tbody a.folder-link:text-is("{REPO_NAME}")')
page.click('#folders-tbody a.folder-link:text-is("notes")')
expect(page.locator("#docs-tbody tr")).to_have_count(rows)
def _go_top(page: Page) -> None:
"""Back to the catalog's top level (no-op when already there)."""
if not page.locator("#kb-crumb").is_hidden():
page.locator("#kb-crumb a.kb-crumb-link").first.click()
def _created_cell(page: Page, path: str) -> Any:
"""The row's ``Created`` cell (the column between Chunks and Indexed,
phase 106 D8)."""
row = page.locator("#docs-tbody tr", has_text=path)
expect(row).to_have_count(1)
return row.locator("td:nth-child(5)")
def _open_doc(page: Page, path: str) -> None:
"""Open the same-page viewer modal for one row (phase 26)."""
row = page.locator("#docs-tbody tr", has_text=path)
expect(row).to_have_count(1)
row.locator("td:nth-child(2) a.doc-link").click()
expect(page.locator(".doc-modal")).to_be_visible()
expect(page.locator("#doc-modal-meta .doc-created")).to_have_count(1)
# ---------------------------------------------------------------------------
# 1. The dates the owner reads ARE the per-file last-commit dates
# ---------------------------------------------------------------------------
def test_document_dates_are_the_last_commit_dates(
page: Page, app_url: str, db_ready: None
) -> None:
"""THE phase-106 D2 contract through the streaming walk: the file
nobody touched again keeps commit A (2026-01-05) and the file commit
B modified carries commit B (2026-03-20) — not the sync day, not the
clone day, not the repo tip for both. API, table cell and viewer
badge all three, with the exact ISO on every ``title``."""
page.set_default_timeout(30_000)
login(page, app_url) # lands on /sources.html
run_day = datetime.now(UTC).date().isoformat()
detail = _run_sync(page, f"{len(COMMITTED_DOCS)} added")
assert detail["added"] == len(COMMITTED_DOCS)
assert detail["errors"] == 0
docs = _docs(page)
assert set(docs) == set(COMMITTED_DOCS) # the checkout, nothing else
assert docs[OLD_DOC]["created_at"] == COMMIT_A_ISO
assert docs[CHANGED_DOC]["created_at"] == COMMIT_B_ISO
# The two files' dates DIFFER — a walk that lost the per-file
# distinction (a uniform tip/clone date) cannot pass.
assert docs[OLD_DOC]["created_at"] != docs[CHANGED_DOC]["created_at"]
for path in (OLD_DOC, CHANGED_DOC):
created = docs[path]["created_at"]
# Not the run's own day, and strictly older than the row's own
# ``indexed_at`` (which IS the clone/sync day — the two concepts
# do not blur).
assert created[:10] != run_day, f"{path} stored the sync day: {created}"
assert created < docs[path]["indexed_at"], (path, created, docs[path]["indexed_at"])
# The per-document surface the viewer itself reads.
assert _content(page, OLD_DOC)["created_at"] == COMMIT_A_ISO
assert _content(page, CHANGED_DOC)["created_at"] == COMMIT_B_ISO
# The RAG table: the full ISO on the ``Created`` cell's ``title``,
# and the two cells' visible text differs (the owner's "both docs
# show the same date" complaint, UI side).
_drill_to_notes(page, len(COMMITTED_DOCS))
old_cell = _created_cell(page, OLD_DOC)
changed_cell = _created_cell(page, CHANGED_DOC)
assert old_cell.get_attribute("title", timeout=15_000) == COMMIT_A_ISO
assert changed_cell.get_attribute("title", timeout=15_000) == COMMIT_B_ISO
old_text = (old_cell.text_content() or "").strip()
changed_text = (changed_cell.text_content() or "").strip()
assert old_text and changed_text and old_text != changed_text, (
f"the two Created cells are identical: {old_text!r}"
)
# The viewer's Created badge (the date at the top of the clicked
# document) carries the same raw ISO on its title.
_open_doc(page, CHANGED_DOC)
badge = page.locator("#doc-modal-meta .doc-created")
expect(badge).to_have_text(re.compile(r"^Created \S"))
assert badge.get_attribute("title", timeout=15_000) == COMMIT_B_ISO
page.keyboard.press("Escape")
expect(page.locator(".doc-modal")).to_be_hidden()
# ---------------------------------------------------------------------------
# 2. The file git has never committed: the mtime fallback, through the
# streaming walk (the fail-soft contract that must not change)
# ---------------------------------------------------------------------------
def test_uncommitted_file_falls_back_to_mtime(
page: Page, app_url: str, seed_checkout: Callable[[], Path], db_ready: None
) -> None:
"""``notes/untracked.md`` is in the working tree and in NO commit, so
the walk has no entry for it and the importer's ``file_mtime_datetime``
fallback applies — the stored date equals the file's own mtime
(2026-02-14, neither commit's date), the run reaches ``success``, and
no error surface appears (a date-less file is not a failure)."""
page.set_default_timeout(30_000)
checkout_dir = seed_checkout()
login(page, app_url)
detail = _run_sync(page, f"{len(COMMITTED_DOCS) + 1} added")
assert detail["added"] == len(COMMITTED_DOCS) + 1
assert detail["errors"] == 0
docs = _docs(page)
assert UNTRACKED_DOC in docs
# The exact mtime, computed the way the importer computes it (the
# D2 fallback through the streaming path — no dict entry, so
# ``normalize_doc_date(file mtime)`` is the whole story).
on_disk = normalize_doc_date(file_mtime_datetime(checkout_dir / UNTRACKED_DOC))
assert on_disk.isoformat() == UNTRACKED_MTIME_ISO
assert docs[UNTRACKED_DOC]["created_at"] == UNTRACKED_MTIME_ISO
assert docs[UNTRACKED_DOC]["created_at"] not in (COMMIT_A_ISO, COMMIT_B_ISO)
# The owner-visible half: the table cell and the viewer badge.
_drill_to_notes(page, len(COMMITTED_DOCS) + 1)
cell = _created_cell(page, UNTRACKED_DOC)
assert cell.get_attribute("title", timeout=15_000) == UNTRACKED_MTIME_ISO
_open_doc(page, UNTRACKED_DOC)
assert page.locator("#doc-modal-meta .doc-created").get_attribute(
"title", timeout=15_000
) == UNTRACKED_MTIME_ISO
page.keyboard.press("Escape")
# And the run itself stayed clean (the fail-soft contract is about
# the RESULT: a date walk with no entry never breaks a sync).
body = _status(page)
assert body["state"] == "success" and body["error"] is None
expect(page.locator("#sync-error-banner")).to_be_hidden()
# ---------------------------------------------------------------------------
# 3. The phase-106 D1 lock: the owner's correction survives a re-sync
# while the other documents keep coming from the stream
# ---------------------------------------------------------------------------
def test_manual_date_survives_a_resync(
page: Page, app_url: str, seed_checkout: Callable[[], Path], db_ready: None
) -> None:
"""Set a date in the viewer's editor → Save (the badge re-renders
from the RESPONSE) → re-sync: the manual date is untouched, the
``created_at_manual`` flag is the only flag in the source, and the
OTHER documents still read their dates off the stream (the run
reports no date drift at all — ``dates_updated=0`` with commit B
still on ``notes/changed.md``)."""
page.set_default_timeout(30_000)
seed_checkout()
login(page, app_url)
_run_sync(page, f"{len(COMMITTED_DOCS) + 1} added")
# --- SET the correction through the real UI --------------------------
_drill_to_notes(page, len(COMMITTED_DOCS) + 1)
_open_doc(page, OLD_DOC)
edit = page.locator("#doc-modal .doc-date-edit")
expect(edit).to_be_visible(timeout=15_000)
edit.click()
page.fill("#doc-modal .doc-date-input", "2021-05-05")
page.click("#doc-modal .doc-date-save")
expect(page.locator("#doc-modal .doc-date-status")).to_have_text(
f"Date saved for {REPO_NAME}/{OLD_DOC}."
)
# The badge re-renders from the server's normalized value, never the
# raw input string.
expect(page.locator("#doc-modal-meta .doc-created")).to_have_attribute(
"title", EDITED_ISO
)
expect(page.locator("#doc-modal .doc-date-editor")).to_have_count(0, timeout=8_000)
page.keyboard.press("Escape")
expect(page.locator(".doc-modal")).to_be_hidden()
assert _docs(page)[OLD_DOC]["created_at"] == EDITED_ISO
# --- RE-SYNC ---------------------------------------------------------
_go_top(page)
detail = _run_sync(page, f"0 added · {len(COMMITTED_DOCS) + 1} unchanged")
assert detail["added"] == 0 and detail["updated"] == 0
assert detail["pruned"] == 0
# Nothing moved: the stream re-derived the SAME dates for the sync-managed
# rows, and the manual row is skipped entirely (D1) — so a re-sync is a
# no-op on the date column, not a re-stamp.
assert detail["dates_updated"] == 0, detail
docs = _docs(page)
assert docs[OLD_DOC]["created_at"] == EDITED_ISO # the owner's date SURVIVES
# The neighbours keep coming from the stream, unchanged.
assert docs[CHANGED_DOC]["created_at"] == COMMIT_B_ISO
assert docs[UNTRACKED_DOC]["created_at"] == UNTRACKED_MTIME_ISO
with SessionLocal() as db:
rows = db.execute(
select(Document.path, Document.created_at_manual).where(
Document.source == REPO_NAME
)
).all()
manual = {path for path, flag in rows if flag}
assert manual == {OLD_DOC}, rows
# ---------------------------------------------------------------------------
# 4. An unchanged re-sync: everything ``unchanged``, dates hold exactly
# ---------------------------------------------------------------------------
def test_resync_is_unchanged_and_dates_hold(
page: Page, app_url: str, seed_checkout: Callable[[], Path], db_ready: None
) -> None:
"""Two syncs, nothing changed in between: the second reports every
file ``unchanged`` (0 added / 0 updated / 0 pruned / 0 dates_updated)
and the date map is BYTE-identical — the stream re-parses the same
``git log`` to the same dict, it does not drift (the phase-186
"same dates, cheaper transport" claim, owner-visible)."""
page.set_default_timeout(30_000)
seed_checkout()
login(page, app_url)
first = _run_sync(page, f"{len(COMMITTED_DOCS) + 1} added")
assert first["added"] == len(COMMITTED_DOCS) + 1
before = _date_map(_docs(page))
assert before[CHANGED_DOC] == COMMIT_B_ISO # the map is meaningful first
_go_top(page)
second = _run_sync(page, f"0 added · {len(COMMITTED_DOCS) + 1} unchanged")
assert second["added"] == 0
assert second["updated"] == 0
assert second["unchanged"] == len(COMMITTED_DOCS) + 1
assert second["pruned"] == 0
assert second["dates_updated"] == 0, second
after = _date_map(_docs(page))
assert after == before, f"the dates drifted across an unchanged re-sync:\n{before}\n{after}"
# And the cells agree with the API (the owner reads the table, not
# the JSON): every row's title still equals the stored ISO.
_drill_to_notes(page, len(COMMITTED_DOCS) + 1)
for path in (*COMMITTED_DOCS, UNTRACKED_DOC):
assert _created_cell(page, path).get_attribute("title", timeout=15_000) == after[path]
# ---------------------------------------------------------------------------
# 5. The non-ASCII path: the quoted-key behaviour is UNCHANGED by the
# streaming move (module docstring's measured record)
# ---------------------------------------------------------------------------
def test_quoted_nonascii_path_falls_back_exactly_as_before(
page: Page,
app_url: str,
seed_checkout: Callable[[], Path],
checkout: Path,
db_ready: None,
) -> None:
"""What the walk does TODAY with a ``core.quotepath``-quoted path,
pinned as UNCHANGED behaviour (the module docstring carries the
measurement): the walk's dict carries the mangled key
``'"notes/caf/303/251.md"'`` — never ``notes/café.md`` — so the
importer's lookup misses and the document shows its file mtime
instead of commit A. The phase-186 change is transport only, so both
halves (the mangled key and the mtime result) must be exactly what
the buffered walk produced; this is the pin that says so, NOT a
statement that the behaviour is correct (fixing the quoting grammar
changes stored dates for every non-ASCII document — LOCKED A1,
a future phase's call)."""
page.set_default_timeout(30_000)
checkout_dir = seed_checkout()
assert checkout == checkout_dir
# The walk itself (the deterministic layer): same bytes in, same
# mangled keys out, and NO entry for the real path.
walk = file_commit_dates(checkout_dir)
assert walk[OLD_DOC].isoformat() == COMMIT_A_ISO # the stream still works here
assert QUOTED_DOC not in walk, sorted(walk)
assert walk[QUOTED_WALK_KEY].isoformat() == COMMIT_A_ISO, sorted(walk)
login(page, app_url)
detail = _run_sync(page, f"{len(COMMITTED_DOCS) + 1} added")
assert detail["errors"] == 0
docs = _docs(page)
assert QUOTED_DOC in docs
created = docs[QUOTED_DOC]["created_at"]
# The importer looked up the REAL path, found nothing, and fell back.
assert created != COMMIT_A_ISO, (
"the quoted path suddenly resolved to its commit date — the parser's "
"quoting grammar changed, which is a DATE change for every non-ASCII "
"document already in a KB (LOCKED A1: this phase ships no such change)"
)
assert created == normalize_doc_date(
file_mtime_datetime(checkout_dir / QUOTED_DOC)
).isoformat()
assert created == QUOTED_MTIME_ISO
# The run stayed clean and the owner sees the date in both surfaces.
body = _status(page)
assert body["state"] == "success" and body["error"] is None
expect(page.locator("#sync-error-banner")).to_be_hidden()
_drill_to_notes(page, len(COMMITTED_DOCS) + 1)
assert _created_cell(page, QUOTED_DOC).get_attribute(
"title", timeout=15_000
) == QUOTED_MTIME_ISO