Files
brain-of-reese/tests/unit/test_git_sync.py
T
ducoterra d3f8c7f2c8
Build and Push Containers / build-and-push-app (push) Successful in 2m34s
Build and Push Containers / build-and-push-db (push) Successful in 14s
phase: 135_git_source_branch
All verification complete. No edits were needed by this pass — the phase work and docs were already consistent and green.

**Phase 135 final verification pass — report**

- Verified all 5 tasks' work present & correct: migration 0024 (reversible), `GitSource.branch` + 4 schema shapes, `validate_branch_name` (full check order per design), `clone_or_pull(…, branch)` (HEAD guard first, `--branch` clone, fetch+ff merge), both call sites pass `row.branch`, UI (add-form field, editor field, row tag), `file://` URL transport.
- Completion criteria: (1) branch-mirrored through Sync + CLI — **pass** (E2E + integration); (2) `branch=None` byte-identical — **pass** (`test_branch_none_argv_is_byte_identical_pre_phase_sequence` green); (3) fixed 422s / local-row 422 / tri-state — **pass**; (4) mismatch fails loudly with remove-and-re-add — **pass** (E2E); (5) coverage 99% >90%, E2E isolated green — **pass**; (6) no regressions — **pass**.
- Tests: `uv run pytest --cov=app --cov-report=term-missing` → 3223 passed, app/ 99%; `uv run pytest tests/e2e/test_git_source_branch.py -v --no-cov` → 4 passed (16.4 s); `uv run ruff check .` clean; `uv run pyright` → 0 errors, 0 warnings.
- Documentation: README.md reviewed in full — phase-135 branch section accurate; the phase's README diff also correctly fixed a pre-existing stale line (shallow `--depth 1` clone vs full-history, phase 107). Routes, env defaults, feature lists verified against code. AGENTS.md — all 10 rules + quick-reference verified (referenced tests, PLAN.md, phases dirs, commands all accurate). **Verified consistent — no changes needed.**
- No live-infrastructure changes (dev DB already running and healthy; no live steps in this phase).
- Next pending phase: `.agents/phases/todo/136_force_push_reset`.
2026-09-28 21:15:03 -04:00

501 lines
18 KiB
Python

"""Unit tests: git clone/pull utility (phase 28; phase 107 D11/D12;
phase 135 adds the branch pin — the argv matrix).
``clone_or_pull`` dispatches to ``git clone`` (full history, fresh dest)
or a shallow-probe + optional ``git fetch --unshallow`` +
``git pull --ff-only`` (existing checkout) with ``subprocess`` fully
mocked — the real git CLI is never invoked, so the tests run anywhere.
Phase 135: with a pinned ``branch`` the fresh clone gains ``--branch``
and the existing checkout runs the HEAD guard → probe → (unshallow) →
``fetch origin <b>`` → ``merge --ff-only FETCH_HEAD`` sequence;
``branch=None`` (and the two-argument call) keeps the pre-phase argv
BYTE-IDENTICAL — the argv pins below are the regression gate for
every existing deployment.
"""
from __future__ import annotations
import subprocess
from pathlib import Path
import pytest
import scripts.git_sync as git_sync
from scripts.git_sync import GitSyncError, clone_or_pull
class _FakeProc:
def __init__(self, returncode: int = 0, stdout: str = "", stderr: str = "") -> None:
self.returncode = returncode
self.stdout = stdout
self.stderr = stderr
def _fake_run(
monkeypatch: pytest.MonkeyPatch,
returncode: int = 0,
stdout: str = "",
stderr: str = "",
missing_git: bool = False,
responses: dict[tuple[str, ...], tuple[int, str, str]] | None = None,
) -> list[dict[str, object]]:
"""Monkeypatch scripts.git_sync.subprocess.run; record each call.
``responses`` dispatches by the full argv (clone / shallow-probe /
``fetch --unshallow`` / pull) — a per-command
``(returncode, stdout, stderr)``; any argv not in the map gets the
default ``returncode``/``stdout``/``stderr``.
"""
calls: list[dict[str, object]] = []
def fake_run(argv: list[str], cwd: Path | None = None, **kwargs: object) -> _FakeProc:
calls.append({"argv": list(argv), "cwd": cwd, **kwargs})
if missing_git:
raise FileNotFoundError("git")
if responses is not None:
rc, out, err = responses.get(tuple(argv), (returncode, stdout, stderr))
return _FakeProc(rc, out, err)
return _FakeProc(returncode, stdout, stderr)
monkeypatch.setattr(subprocess, "run", fake_run)
return calls
def test_clone_or_pull_clones_when_dest_has_no_git_dir(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""First run: dest absent → ``git clone`` (full history — NO
``--depth``, phase 107 D11) from the parent dir."""
url = "https://github.com/user/homelab.git"
dest = tmp_path / "homelab"
calls = _fake_run(monkeypatch)
result = clone_or_pull(url, dest)
assert result == dest
assert len(calls) == 1
call = calls[0]
assert call["argv"] == ["git", "clone", url, str(dest)]
assert call["cwd"] == dest.parent
assert call["capture_output"] is True
assert call["text"] is True
def test_clone_or_pull_pulls_when_git_dir_exists(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Steady state: dest/.git present and the probe says NOT shallow
(``false``) → straight to ``git pull --ff-only`` in-place (one cheap
local probe, no network fetch)."""
dest = tmp_path / "homelab"
(dest / ".git").mkdir(parents=True)
calls = _fake_run(
monkeypatch,
responses={("git", "rev-parse", "--is-shallow-repository"): (0, "false\n", "")},
)
result = clone_or_pull("https://github.com/user/homelab.git", dest)
assert result == dest
assert len(calls) == 2
assert calls[0]["argv"] == ["git", "rev-parse", "--is-shallow-repository"]
assert calls[0]["cwd"] == dest
assert calls[1]["argv"] == ["git", "pull", "--ff-only"]
assert calls[1]["cwd"] == dest
def test_clone_or_pull_unshallows_existing_shallow_checkout(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Existing shallow checkout (probe ``true`` — the one-time D11
self-heal of deployed checkouts) → ``git fetch --unshallow``
BEFORE the usual ``git pull --ff-only`` (order pinned), all in
dest."""
dest = tmp_path / "homelab"
(dest / ".git").mkdir(parents=True)
calls = _fake_run(
monkeypatch,
responses={
("git", "rev-parse", "--is-shallow-repository"): (0, "true\n", ""),
("git", "fetch", "--unshallow"): (0, "Fetching full history\n", ""),
},
)
clone_or_pull("https://github.com/user/homelab.git", dest)
assert len(calls) == 3
assert calls[0]["argv"] == ["git", "rev-parse", "--is-shallow-repository"]
assert calls[1]["argv"] == ["git", "fetch", "--unshallow"]
assert calls[2]["argv"] == ["git", "pull", "--ff-only"]
for call in calls:
assert call["cwd"] == dest
def test_clone_or_pull_unshallow_failure_propagates(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""D12 fail-loud: a failed ``git fetch --unshallow`` raises
:class:`GitSyncError` (with git's stderr) and the pull NEVER runs —
continuing a failed self-heal would silently re-serve the uniform
tip date (the bug D11 fixes)."""
dest = tmp_path / "homelab"
(dest / ".git").mkdir(parents=True)
calls = _fake_run(
monkeypatch,
responses={
("git", "rev-parse", "--is-shallow-repository"): (0, "true\n", ""),
("git", "fetch", "--unshallow"): (128, "", "fatal: could not fetch\n"),
},
)
with pytest.raises(
GitSyncError, match=r"git fetch --unshallow failed \(exit 128\): fatal: could not fetch"
):
clone_or_pull("https://github.com/user/homelab.git", dest)
assert [call["argv"] for call in calls] == [
["git", "rev-parse", "--is-shallow-repository"],
["git", "fetch", "--unshallow"],
] # the pull never ran
def test_clone_or_pull_creates_missing_parent_before_clone(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""The (nested) parent of the destination is mkdir'd before git runs."""
dest = tmp_path / "nested" / "deeper" / "homelab"
assert not dest.parent.exists()
calls = _fake_run(monkeypatch)
clone_or_pull("https://github.com/user/homelab.git", dest)
assert dest.parent.is_dir()
assert calls[0]["argv"] == [
"git",
"clone",
"https://github.com/user/homelab.git",
str(dest),
]
def test_existing_dir_without_git_dir_is_treated_as_fresh(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A leftover non-repo dir at the dest still takes the clone path —
git fails loudly (non-zero exit) if the dir is not empty."""
dest = tmp_path / "homelab"
dest.mkdir()
calls = _fake_run(monkeypatch)
clone_or_pull("https://github.com/user/homelab.git", dest)
assert calls[0]["argv"] == [
"git",
"clone",
"https://github.com/user/homelab.git",
str(dest),
]
def test_failing_git_raises_error_carrying_stderr(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Non-zero exit → GitSyncError naming the subcommand, exit code, stderr."""
dest = tmp_path / "homelab"
_fake_run(monkeypatch, returncode=128, stderr="fatal: repository not found\n")
with pytest.raises(
GitSyncError,
match=r"git clone .* failed \(exit 128\): fatal: repository not found",
):
clone_or_pull("https://example.com/nope.git", dest)
# Same for the pull path (broken checkout, e.g. diverged history) —
# the probe answers "false" (not shallow) so the pull runs.
(dest / ".git").mkdir(parents=True)
_fake_run(
monkeypatch,
returncode=1,
stderr="error: cannot pull with rebase",
responses={("git", "rev-parse", "--is-shallow-repository"): (0, "false\n", "")},
)
with pytest.raises(
GitSyncError, match=r"git pull --ff-only failed \(exit 1\): error: cannot pull with rebase"
):
clone_or_pull("https://example.com/homelab.git", dest)
def test_missing_git_raises_named_error(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""No git binary on PATH → GitSyncError telling the user to install it."""
_fake_run(monkeypatch, missing_git=True)
with pytest.raises(GitSyncError, match="git was not found on PATH"):
clone_or_pull("https://example.com/homelab.git", tmp_path / "homelab")
def test_run_git_captures_and_returns_stdout(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""run_git returns the captured stdout on success (git output is not lost)."""
calls = _fake_run(monkeypatch, stdout="From example.com\n + abc..def main")
assert git_sync.run_git(["git", "status"], cwd=tmp_path) == "From example.com\n + abc..def main"
assert len(calls) == 1
# --- phase 135: the branch pin (the argv matrix) ----------------------------
def test_branch_none_argv_is_byte_identical_pre_phase_sequence(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""The regression gate: ``branch=None`` (explicit, the call sites'
form for every pre-phase-135 row) produces the EXACT pre-phase argv
— fresh: one ``git clone`` without ``--branch``; existing (not
shallow): probe → ``git pull --ff-only``; existing (shallow):
probe → ``fetch --unshallow`` → ``git pull --ff-only``. A future
refactor that rewrites the default path breaks this pin."""
url = "https://github.com/user/homelab.git"
dest = tmp_path / "homelab"
# fresh (dest absent)
calls = _fake_run(monkeypatch)
clone_or_pull(url, dest, None)
assert [call["argv"] for call in calls] == [["git", "clone", url, str(dest)]]
# existing, not shallow
(dest / ".git").mkdir(parents=True)
calls = _fake_run(
monkeypatch,
responses={("git", "rev-parse", "--is-shallow-repository"): (0, "false\n", "")},
)
clone_or_pull(url, dest, None)
assert [call["argv"] for call in calls] == [
["git", "rev-parse", "--is-shallow-repository"],
["git", "pull", "--ff-only"],
]
# existing, shallow (the one-time D11 self-heal between probe and pull)
calls = _fake_run(
monkeypatch,
responses={
("git", "rev-parse", "--is-shallow-repository"): (0, "true\n", ""),
("git", "fetch", "--unshallow"): (0, "", ""),
},
)
clone_or_pull(url, dest, None)
assert [call["argv"] for call in calls] == [
["git", "rev-parse", "--is-shallow-repository"],
["git", "fetch", "--unshallow"],
["git", "pull", "--ff-only"],
]
def test_fresh_clone_with_branch_uses_clone_branch(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Phase 135: fresh dest + pinned branch → ``git clone --branch <b>
url dest`` (the branch is checked out from the first commit — no
post-clone checkout; still full history, no ``--depth``)."""
url = "https://github.com/user/homelab.git"
dest = tmp_path / "homelab"
calls = _fake_run(monkeypatch)
result = clone_or_pull(url, dest, "release")
assert result == dest
assert [call["argv"] for call in calls] == [
["git", "clone", "--branch", "release", url, str(dest)]
]
assert calls[0]["cwd"] == dest.parent
def test_existing_checkout_matching_branch_fetches_and_ff_merges_the_ref(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Phase 135: existing checkout ALREADY on the pinned branch → the
full sequence in order: the HEAD guard (``rev-parse --abbrev-ref
HEAD``) → the shallow probe → the single-fetch ``fetch origin <b>``
→ ``merge --ff-only FETCH_HEAD`` (not ``git pull`` — the default
path's argv is reserved for ``branch=None``)."""
dest = tmp_path / "homelab"
(dest / ".git").mkdir(parents=True)
calls = _fake_run(
monkeypatch,
responses={
("git", "rev-parse", "--abbrev-ref", "HEAD"): (0, "release\n", ""),
("git", "rev-parse", "--is-shallow-repository"): (0, "false\n", ""),
("git", "fetch", "origin", "release"): (0, " * branch release -> FETCH_HEAD", ""),
("git", "merge", "--ff-only", "FETCH_HEAD"): (0, "Already up to date.\n", ""),
},
)
result = clone_or_pull("https://github.com/user/homelab.git", dest, "release")
assert result == dest
assert [call["argv"] for call in calls] == [
["git", "rev-parse", "--abbrev-ref", "HEAD"],
["git", "rev-parse", "--is-shallow-repository"],
["git", "fetch", "origin", "release"],
["git", "merge", "--ff-only", "FETCH_HEAD"],
]
for call in calls:
assert call["cwd"] == dest
def test_existing_shallow_checkout_matching_branch_unshallows_before_fetch(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Phase 135 + phase 107 D11: the branch path runs the SAME one-time
self-heal — a shallow matching checkout is unshallowed BETWEEN the
guard/probe and the branch fetch (order pinned)."""
dest = tmp_path / "homelab"
(dest / ".git").mkdir(parents=True)
calls = _fake_run(
monkeypatch,
responses={
("git", "rev-parse", "--abbrev-ref", "HEAD"): (0, "release\n", ""),
("git", "rev-parse", "--is-shallow-repository"): (0, "true\n", ""),
("git", "fetch", "--unshallow"): (0, "", ""),
("git", "fetch", "origin", "release"): (0, "", ""),
("git", "merge", "--ff-only", "FETCH_HEAD"): (0, "", ""),
},
)
clone_or_pull("https://github.com/user/homelab.git", dest, "release")
assert [call["argv"] for call in calls] == [
["git", "rev-parse", "--abbrev-ref", "HEAD"],
["git", "rev-parse", "--is-shallow-repository"],
["git", "fetch", "--unshallow"],
["git", "fetch", "origin", "release"],
["git", "merge", "--ff-only", "FETCH_HEAD"],
]
def test_existing_checkout_branch_mismatch_fails_loud_before_any_fetch(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Phase 135 (ASSUMPTION B5, D12): a checkout on a DIFFERENT branch
raises the remove-and-re-add ``GitSyncError`` — naming the checkout
and BOTH branch names — and the guard is FIRST: no shallow probe,
no fetch, no merge argv was invoked (no silent branch switch)."""
url = "https://github.com/user/homelab.git"
dest = tmp_path / "homelab"
(dest / ".git").mkdir(parents=True)
calls = _fake_run(
monkeypatch,
responses={("git", "rev-parse", "--abbrev-ref", "HEAD"): (0, "main\n", "")},
)
with pytest.raises(GitSyncError) as excinfo:
clone_or_pull(url, dest, "release")
msg = str(excinfo.value)
assert f"checkout {dest}" in msg # the checkout is named
assert "'main'" in msg # the checkout's branch
assert "'release'" in msg # the source's pinned branch
assert "remove and re-add the source to switch branches" in msg
# credential safety: the message carries NO URL (the only place a
# token could ride) — even for a plain URL.
assert url not in msg
assert "github" not in msg
# the guard is first: exactly ONE argv (the guard probe) ran.
assert [call["argv"] for call in calls] == [
["git", "rev-parse", "--abbrev-ref", "HEAD"]
]
def test_branch_mismatch_message_never_contains_url_even_with_token(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Phase 135 credential-safety: even when the CLONE URL carries the
injected token (the phase-121 ``clone_url_for`` form), the guard
message is URL-free — the checkout path and the two branch names
only."""
url = "https://x-access-token:ghp_supersecret@git.example.com/user/repo.git"
dest = tmp_path / "repo"
(dest / ".git").mkdir(parents=True)
calls = _fake_run(
monkeypatch,
responses={("git", "rev-parse", "--abbrev-ref", "HEAD"): (0, "main\n", "")},
)
with pytest.raises(GitSyncError) as excinfo:
clone_or_pull(url, dest, "release")
msg = str(excinfo.value)
assert url not in msg
assert "ghp_supersecret" not in msg
assert "git.example.com" not in msg
assert f"checkout {dest} is on branch 'main' but the source " \
"specifies branch 'release'" in msg
assert [call["argv"] for call in calls] == [
["git", "rev-parse", "--abbrev-ref", "HEAD"]
]
def test_existing_checkout_branch_fetch_failure_propagates_before_merge(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Phase 135 D12 fail-loud: a failed ``fetch origin <b>`` raises
(with git's stderr) and the merge NEVER runs."""
dest = tmp_path / "homelab"
(dest / ".git").mkdir(parents=True)
calls = _fake_run(
monkeypatch,
responses={
("git", "rev-parse", "--abbrev-ref", "HEAD"): (0, "release\n", ""),
("git", "rev-parse", "--is-shallow-repository"): (0, "false\n", ""),
("git", "fetch", "origin", "release"): (128, "", "fatal: couldn't find remote ref\n"),
},
)
with pytest.raises(
GitSyncError,
match=r"git fetch origin release failed \(exit 128\): fatal: couldn't find remote ref",
):
clone_or_pull("https://github.com/user/homelab.git", dest, "release")
assert [call["argv"] for call in calls] == [
["git", "rev-parse", "--abbrev-ref", "HEAD"],
["git", "rev-parse", "--is-shallow-repository"],
["git", "fetch", "origin", "release"],
] # the merge never ran
def test_existing_checkout_branch_non_ff_merge_fails_loud(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Phase 135: a non-ff ``merge --ff-only FETCH_HEAD`` (diverged /
force-pushed origin) fails loudly HERE with git's stderr — the
reset fallback is phase 136, not a silent fix in this phase."""
dest = tmp_path / "homelab"
(dest / ".git").mkdir(parents=True)
calls = _fake_run(
monkeypatch,
responses={
("git", "rev-parse", "--abbrev-ref", "HEAD"): (0, "release\n", ""),
("git", "rev-parse", "--is-shallow-repository"): (0, "false\n", ""),
("git", "fetch", "origin", "release"): (0, "", ""),
("git", "merge", "--ff-only", "FETCH_HEAD"): (
1,
"",
"fatal: Not possible to fast-forward, aborting.\n",
),
},
)
with pytest.raises(
GitSyncError,
match=(
r"git merge --ff-only FETCH_HEAD failed \(exit 1\): "
r"fatal: Not possible to fast-forward"
),
):
clone_or_pull("https://github.com/user/homelab.git", dest, "release")
assert len(calls) == 4 # all four argv ran; the merge failed loud