Files
brain-of-reese/tests/integration/test_git_file_dates.py
T
ducoterra 644f7de696
Build and Push Containers / build-and-push-app (push) Successful in 1m42s
Build and Push Containers / build-and-push-db (push) Successful in 14s
phase: 186_git_log_streaming_dates
## Final report — phase 186 `git_log_streaming_dates`

**State found:** all three task files were already implemented and in `.agents/phases/complete/186_git_log_streaming_dates/`, so this run was the final verification + documentation pass.

### Verification (all green)
| Gate | Result |
|---|---|
| `pytest --cov=app --cov-report=term-missing` | **4531 passed, exit 0** · coverage `TOTAL 6872 stmts → 99%` (gate >90%) |
| `tests/e2e/test_git_log_stream_dates.py -v --no-cov` (isolation) | **5 passed** |
| `ruff check .` | All checks passed |
| `pyright` | 0 errors |

Implementation shape confirmed against the phase contracts: `_parse_commit_dates(str | Iterable[str])` (existing string pins untouched — only 3 non-test lines removed from `test_git_sync.py`), `stream_git_lines` (`Popen` + daemon stderr drain + per-line stdout + terminate-on-abandon, `run_git`-shaped `GitSyncError`), `file_commit_dates` on the stream with byte-identical dates and the fail-soft funnel unchanged, `async_sync` propagating the `to_thread` errors to the existing `except`. A11 holds: two `subprocess.` sites, both in `scripts/git_sync.py`, both off-loop.

### Documentation-consistency pass
Verified (clean): all ~100 `BOR_*` README defaults against a bare `Settings()` (the local `.env` produced false positives — none real), `.env.example` completeness, every module/route/page/test path referenced, `compose.yaml`/`Containerfile`/`entrypoint.sh` claims, E2E port conventions (8901 / 8123), palette count (17), chip cap (90), token format, payload limits, retrieval battery + audit commands.

Fixed (both files, prose only):
- **Page-naming drift** — README/AGENTS called the sync/catalog page "the Sources page" in 5 places, contradicting the README's own convention (and `frontend/index.html`): the **Sync sources** button and the document catalogue are the **RAG** page (`/sources.html`); "Sources" is `/git-sources.html`. Now `RAG page (/sources.html)` in README:186, 376, 418, 682, 713, 745, 1006, 1057 and AGENTS:19, 38, 183.
- **Phase-186 AST-pin wording** — "fails if a third spawn appears anywhere in `app/` or `scripts/`" overstated it (the pin *enumerates* the spawns, including the non-git Valkey probe in `app/core/caching.py`); rewritten to describe the enumeration.
- Moved `00_phase.md` into `complete/186_git_log_streaming_dates/` (AGENTS rule 2 moves the whole directory); `todo/` now starts at 187.

⚠️ **One mistake to disclose:** while tidying line wraps I ran `git checkout README.md`, which discarded task 03's uncommitted phase-186 README bullet. I restored it verbatim from the full `git diff` captured earlier in this session (plus the two corrections above) and re-verified the section renders as intended — no other README content was uncommitted, and AGENTS.md was untouched by the checkout. A blanket line-length rewrap also reflowed 4 unrelated lines; all four were restored to their exact original text (`git diff` now shows only intended changes).

Nothing was committed or staged — the working tree is left for the harness: modified `scripts/git_sync.py`, `app/api/sync.py`, `app/rag/git_sources.py`, `tests/unit/test_git_sync.py`, `tests/integration/test_git_file_dates.py`, `README.md`, `AGENTS.md`; new `tests/e2e/test_git_log_stream_dates.py`, `.agents/phases/complete/186_git_log_streaming_dates/`, `.agents/reports/186_git_log_streaming_dates/`.
2026-10-16 07:50:16 -04:00

383 lines
15 KiB
Python

"""Integration: phase 106 task 03 — ``file_commit_dates`` against real
git scratch repos (D2; phase 107 D11 supersedes phase 106 D10).
Builds scratch repositories with controlled ``GIT_COMMITTER_DATE``s
(the 2026-09-13 verification recipe: file ``a.md`` committed once in
2020, file ``b.md`` committed in 2020 and touched again in 2024, a
``docs/deep.md`` subdirectory file committed once in 2020) and pins
the VERIFIED checkout behavior (phase 107):
* a LOCAL-PATH ``clone_or_pull`` checkout is FULL history → TRUE
per-file last-commit dates (first-sighting wins: ``a.md`` 2020,
``b.md`` 2024, ``docs/deep.md`` 2020);
* a URL-TRANSPORT (``file://``) ``clone_or_pull`` checkout is FULL
history TOO (D11: no ``--depth`` on the clone) → the SAME true
per-file dates — the regression pin for the 2026-09-16 bug
(pre-fix, D10's shallow ``--depth 1`` clone returned the TIP date
for EVERY file, uniform per repo);
* an EXISTING shallow checkout (made ``--depth 1`` directly by the
test harness — simulating every pre-phase-107 deployed checkout)
self-heals on the next ``clone_or_pull``: the
``--is-shallow-repository`` probe finds it shallow → ``git fetch
--unshallow`` → ``git pull --ff-only`` → no longer shallow, true
per-file dates;
* fail-soft: a directory without ``.git``, an empty repo (no
commits), a git failure, and a malformed log output all yield
``{}`` — a date walk must never break a sync (the importer, task
04, falls back to file mtimes).
Phase 186 adds the two pins for the walk moving off ``run_git`` onto
the streaming site :func:`scripts.git_sync.stream_git_lines` (the
incident of 2026-10-15: a full-history ``git log`` held as ONE string
plus the parser's ``splitlines()`` copy, tens of MB of transient peak
per source): ``test_streamed_walk_returns_the_buffered_dates`` proves
the streamed dict EQUALS the buffered one on the four cases that
matter (commit-1-only file, commit-2-modified file, subdirectory file,
never-committed file → the mtime fallback), and
``test_file_commit_dates_fail_soft_unchanged`` pins that both fail-soft
arms and their exact warning lines survived the move.
DB-free by design: ``file_commit_dates`` takes a path, no session.
Skipped (not failed) on a machine without the git CLI (the
``test_doc_drafts_api.py`` guard).
"""
from __future__ import annotations
import os
import subprocess
from datetime import UTC, datetime
from pathlib import Path
import pytest
from scripts.git_sync import (
GitSyncError,
_parse_commit_dates, # pyright: ignore[reportPrivateUsage]
clone_or_pull,
file_commit_dates,
run_git,
)
def _git_available() -> bool:
try:
proc = subprocess.run(["git", "--version"], capture_output=True, check=False)
return proc.returncode == 0
except (FileNotFoundError, OSError):
return False
#: Real ``git`` in the test environment — skipped cleanly without it
#: (the ``test_doc_drafts_api.py`` house pattern).
GIT = _git_available()
pytestmark = pytest.mark.skipif(not GIT, reason="git CLI not available")
#: The two controlled commit dates (the 2026-09-13 verification recipe).
DATE_A = datetime(2020, 1, 2, 3, 4, 6, tzinfo=UTC) # commit one (2020)
DATE_B = datetime(2024, 6, 15, 10, 0, 0, tzinfo=UTC) # commit two = the tip (2024)
def _git(cwd: Path, *argv: str, when: datetime | None = None) -> None:
"""Run one git command for the test harness (fixture setup); a
non-zero exit fails the fixture, not the test under test."""
env = os.environ.copy()
if when is not None:
iso = when.isoformat()
env["GIT_AUTHOR_DATE"] = iso
env["GIT_COMMITTER_DATE"] = iso
env["GIT_AUTHOR_NAME"] = "T"
env["GIT_AUTHOR_EMAIL"] = "t@example.com"
env["GIT_COMMITTER_NAME"] = "T"
env["GIT_COMMITTER_EMAIL"] = "t@example.com"
proc = subprocess.run(
["git", *argv], cwd=cwd, env=env, capture_output=True, text=True, check=False
)
assert proc.returncode == 0, f"git {' '.join(argv)} failed: {proc.stderr}"
@pytest.fixture()
def scratch_repo(tmp_path: Path) -> Path:
"""The 2026-09-13 recipe: commit one (2020-01-02) adds ``a.md``,
``b.md``, ``docs/deep.md``; commit two (2024-06-15, the tip)
touches ONLY ``b.md``."""
repo = tmp_path / "repo"
repo.mkdir()
_git(repo, "init", "-q")
_git(repo, "config", "user.email", "t@example.com")
_git(repo, "config", "user.name", "T")
_git(repo, "config", "commit.gpgsign", "false")
(repo / "docs").mkdir()
(repo / "a.md").write_text("# A\nstable since 2020\n", encoding="utf-8")
(repo / "b.md").write_text("# B\nfirst version\n", encoding="utf-8")
(repo / "docs" / "deep.md").write_text("# Deep\nalso 2020\n", encoding="utf-8")
_git(repo, "add", "-A", when=DATE_A)
_git(repo, "commit", "-qm", "one", when=DATE_A)
(repo / "b.md").write_text("# B\nupdated 2024\n", encoding="utf-8")
_git(repo, "add", "-A", when=DATE_B)
_git(repo, "commit", "-qm", "two", when=DATE_B)
return repo
def test_local_clone_yields_true_per_file_dates(scratch_repo: Path, tmp_path: Path) -> None:
"""(a) LOCAL-PATH ``clone_or_pull`` → full history (no ``--depth``
at all, phase 107 D11) → TRUE per-file last-commit dates: the
first (newest) sighting of each path wins — ``b.md`` the 2024
touch, the rest the 2020 commit."""
dest = tmp_path / "local"
clone_or_pull(str(scratch_repo), dest)
assert (dest / ".git").exists() # a real checkout
assert file_commit_dates(dest) == {
"a.md": DATE_A,
"b.md": DATE_B, # touched again by the tip commit
"docs/deep.md": DATE_A,
}
def test_url_clone_yields_true_per_file_dates(scratch_repo: Path, tmp_path: Path) -> None:
"""(b) URL-TRANSPORT ``clone_or_pull`` (``file://`` through the REAL
function — the 2026-09-16 regression pin): the checkout is FULL
history (no ``--depth`` — phase 107 D11 supersedes phase 106 D10)
→ NOT shallow, and TRUE per-file last-commit dates: the 2020 files
stay 2020 and ``b.md`` (touched again at the tip) gets the 2024
tip date. PRE-FIX this returned DATE_B (the uniform tip date) for
all three files — the exact bug the owner reported.
"""
dest = tmp_path / "url-clone"
clone_or_pull(f"file://{scratch_repo}", dest)
assert (dest / ".git").exists() # a real checkout
assert (
run_git(["git", "rev-parse", "--is-shallow-repository"], cwd=dest).strip()
== "false"
) # a full-history checkout, not shallow
assert file_commit_dates(dest) == {
"a.md": DATE_A,
"b.md": DATE_B, # touched again by the tip commit
"docs/deep.md": DATE_A,
}
def test_existing_shallow_checkout_self_heals(scratch_repo: Path, tmp_path: Path) -> None:
"""(c) an EXISTING shallow checkout — built ``--depth 1`` directly
by the harness, simulating every pre-phase-107 deployed checkout
(live + dev homelab included) — self-heals on the next
``clone_or_pull`` (D11): the ``--is-shallow-repository`` probe
finds it shallow → the ONE-TIME ``git fetch --unshallow`` restores
the full history (no re-clone) → the usual ``--ff-only`` pull. Pre-
heal: shallow + the uniform tip date for all files; post-heal:
full history + true per-file dates."""
dest = tmp_path / "shallow"
_git(tmp_path, "clone", "-q", "--depth", "1", f"file://{scratch_repo}", str(dest))
# Pre-heal: the deployed state the fix must cure.
assert (
run_git(["git", "rev-parse", "--is-shallow-repository"], cwd=dest).strip()
== "true"
)
assert file_commit_dates(dest) == {
"a.md": DATE_B,
"b.md": DATE_B,
"docs/deep.md": DATE_B,
}
clone_or_pull(f"file://{scratch_repo}", dest)
# Post-heal: full history and true per-file dates, same as (b).
assert (
run_git(["git", "rev-parse", "--is-shallow-repository"], cwd=dest).strip()
== "false"
)
assert file_commit_dates(dest) == {
"a.md": DATE_A,
"b.md": DATE_B,
"docs/deep.md": DATE_A,
}
def test_directory_without_dotgit_fails_soft(tmp_path: Path) -> None:
"""(c) a plain directory (no ``.git``) → ``git log`` exits
non-zero → ``{}`` (fail-soft, no raise) — the importer falls back
to file mtimes."""
plain = tmp_path / "notarepo"
plain.mkdir()
(plain / "a.md").write_text("# A\nnot a git repo\n", encoding="utf-8")
assert file_commit_dates(plain) == {}
def test_nonexistent_directory_fails_soft(tmp_path: Path) -> None:
"""(c) a missing checkout directory → ``{}`` without even
invoking git (no raise)."""
assert file_commit_dates(tmp_path / "gone") == {}
def test_empty_repo_fails_soft(tmp_path: Path) -> None:
"""(c) an initialized repo with NO commits → ``git log`` fails
(nothing to log) → ``{}`` (a cloned-but-empty source must not
break the sync)."""
empty = tmp_path / "emptyrepo"
empty.mkdir()
_git(empty, "init", "-q")
_git(empty, "config", "commit.gpgsign", "false")
assert file_commit_dates(empty) == {}
def test_git_error_fails_soft(
scratch_repo: Path, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
) -> None:
"""(c) a ``GitSyncError`` from the walk (git missing/failed) →
``{}`` + a logged warning naming the fallback — the fail-soft
contract (pinned). Phase 186 moved the walk off ``run_git`` onto
:func:`stream_git_lines`, so that is the site the stub replaces."""
def boom(argv: list[str], cwd: Path) -> str:
raise GitSyncError("git log failed (exit 128): fatal: bad object")
monkeypatch.setattr("scripts.git_sync.stream_git_lines", boom)
with caplog.at_level("WARNING"):
assert file_commit_dates(scratch_repo) == {}
assert any("file_commit_dates" in record.message for record in caplog.records)
def test_malformed_log_output_fails_soft(
scratch_repo: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""(c) ANY parse anomaly (a commit date ``fromisoformat`` cannot
read) → ``{}`` (fail-soft) — the walk is all-or-nothing: a
partially parsed date map would be worse than none."""
monkeypatch.setattr(
"scripts.git_sync.stream_git_lines",
lambda argv, cwd: iter(["@@not-a-date", "b.md"]),
)
assert file_commit_dates(scratch_repo) == {}
# --- phase 186: the streamed walk returns what the buffered one did ---------
def test_streamed_walk_returns_the_buffered_dates(
scratch_repo: Path, tmp_path: Path
) -> None:
"""Phase 186 D4 — DATE PARITY. The same checkout is walked twice:
through the shipped streaming path (``file_commit_dates``) and
through the pre-phase buffered path (``run_git`` → one ``str`` →
``_parse_commit_dates``). The two dicts are EQUAL, and they carry
the four cases the phase names:
(i) a file only in commit 1 (``a.md``) → commit 1's date;
(ii) a file modified in commit 2 (``b.md``) → commit 2's date, NOT
commit 1's — first-sighting-wins over a newest-first walk;
(iii) a file in a subdirectory (``docs/deep.md``) → its own
last-commit date, keyed repo-relative with ``/``;
(iv) a file in the working tree that git never committed → no entry
at all, so the importer's mtime fallback applies (asserted on
the LOOKUP here — the resulting date is the importer's
contract, tested in ``test_importer_dates.py``)."""
dest = tmp_path / "clone"
clone_or_pull(str(scratch_repo), dest)
(dest / "never-committed.md").write_text("# untracked\n", encoding="utf-8")
streamed = file_commit_dates(dest)
buffered = _parse_commit_dates(
run_git(["git", "log", "--name-only", "--format=@@%cI"], cwd=dest)
)
assert streamed == buffered
assert streamed["a.md"] == DATE_A # (i) only in commit 1
assert streamed["b.md"] == DATE_B # (ii) modified in commit 2
assert streamed["docs/deep.md"] == DATE_A # (iii) subdirectory
assert "never-committed.md" not in streamed # (iv) mtime fallback
assert streamed.get("never-committed.md") is None
def test_file_commit_dates_fail_soft_unchanged(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""Phase 186 D3: both fail-soft arms and their EXACT warning lines
survive the switch to streaming — (a) a checkout that is not a
directory (git is never invoked), (b) a directory that is not a
repository (the stream raises the ``run_git``-shaped
``GitSyncError`` mid-call). Both return ``{}``; neither raises.
The wording is byte-pinned because the ops dashboards grep it."""
missing = tmp_path / "gone"
not_a_dir = tmp_path / "a-file"
not_a_dir.write_text("# not a directory\n", encoding="utf-8")
plain = tmp_path / "notarepo"
plain.mkdir()
(plain / "a.md").write_text("# A\n", encoding="utf-8")
with caplog.at_level("WARNING", logger="scripts.git_sync"):
assert file_commit_dates(missing) == {}
assert file_commit_dates(not_a_dir) == {}
assert file_commit_dates(plain) == {}
messages = [r.getMessage() for r in caplog.records if r.name == "scripts.git_sync"]
assert len(messages) == 3
assert messages[0] == (
f"file_commit_dates: {missing} is not a directory — no git dates "
"(the importer will fall back to file mtimes)"
)
assert messages[1] == (
f"file_commit_dates: {not_a_dir} is not a directory — no git dates "
"(the importer will fall back to file mtimes)"
)
assert messages[2].startswith(f"file_commit_dates: git log failed for {plain}: git log ")
assert "failed (exit " in messages[2]
assert messages[2].endswith(
"— the importer will fall back to file mtimes"
)
# --- the pure parser (canned git output — no git, no DB) -------------------
def test_parser_first_sighting_wins_newest_first() -> None:
"""The walk is newest-first, so the FIRST sighting of a path is
its last-commit date: ``b.md`` appears under both commits and keeps
the 2024 (newest) date; the 2020 ``a.md`` keeps 2020. Blank lines
(git's commit separators) are skipped."""
output = "\n".join(
[
"@@2024-06-15T10:00:00+00:00",
"",
"b.md",
"@@2020-01-02T03:04:06+00:00",
"",
"a.md",
"b.md",
]
)
assert _parse_commit_dates(output) == {"a.md": DATE_A, "b.md": DATE_B}
def test_parser_normalizes_paths() -> None:
"""Path lines are whitespace-split (defensively - git's name-only
output is one path per line), backslash-normalized to ``/``, and a
leading ``/`` is stripped (repo-relative POSIX keys); the line is
stripped first."""
output = "\n".join(
[
"@@2024-06-15T10:00:00+00:00",
"",
"docs\\deep.md",
"/rooted.md",
" padded.md ",
]
)
assert _parse_commit_dates(output) == {
"docs/deep.md": DATE_B,
"rooted.md": DATE_B,
"padded.md": DATE_B,
}
def test_parser_rejects_path_before_header() -> None:
"""A path line before ANY commit header is a malformed walk →
``ValueError`` (the caller's fail-soft path turns it into
``{}``)."""
with pytest.raises(ValueError, match="before any commit header"):
_parse_commit_dates("stray.md\n@@2024-06-15T10:00:00+00:00\n")
def test_parser_rejects_bad_date() -> None:
"""A commit date ``fromisoformat`` cannot read → ``ValueError``
(ISO-strict ``%cI`` always parses — this is the anomaly guard)."""
with pytest.raises(ValueError):
_parse_commit_dates("@@yesterday\nb.md\n")