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

151 lines
5.1 KiB
Python

"""Unit: the branch-name validator (phase 135, task 02).
``validate_branch_name`` is the single choke point for every branch
value that enters the API (POST create + PATCH set/change legs —
ASSUMPTION B2). The contract, checked in the documented order (trim →
empty → length → charset → ``..``/``//`` → leading → trailing →
HEAD):
* TRIM first — the returned name is the trimmed input, so a
schema-trimmed value is a fixed point;
* empty (or whitespace-only) → ``ValueError``;
* > 200 chars → ``ValueError`` (exactly 200 passes — the schema's
``max_length=200`` is the first line, the validator the choke point
a future caller must not bypass);
* outside the safe charset ``[A-Za-z0-9._/-]+`` → ``ValueError``
(whitespace, ``@``, ``~``, ``^``, ``:``, ``?``, ``*``, ``[``,
backslash — all out);
* contains ``..`` or ``//`` → ``ValueError`` (path-traversal-shaped
names, even though the charset would allow the pieces);
* leading ``-`` / ``.`` / ``/`` → ``ValueError``;
* trailing ``/`` or ``.`` → ``ValueError``;
* ``HEAD`` (any case) → ``ValueError`` (git's refname — a branch
literally named ``head`` is a documented cost of the simple rule).
The raised ``ValueError`` messages are FIXED reason strings — the
API maps them to the fixed-detail 422 and the raw input is never
echoed (the house credential-safety discipline, applied for
consistency): the tests pin that no message repeats the rejected
input.
"""
from __future__ import annotations
import pytest
from app.rag.git_sources import validate_branch_name
# ---------------------------------------------------------------------------
# Accept
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
"raw",
[
"main",
"release/1.2",
"feature_x",
"a.b/c",
"v2-rc.1",
],
)
def test_valid_names_pass_through_trimmed(raw: str) -> None:
"""The accepted shapes — plain, slashed, dotted, dashed,
underscored — return exactly the trimmed input."""
assert validate_branch_name(raw) == raw
def test_trim_applied_before_validation_and_returned() -> None:
"""Surrounding whitespace is stripped: the returned value is the
trimmed name (``" main "`` → ``main``)."""
assert validate_branch_name(" main ") == "main"
assert validate_branch_name("\trelease/1.2\n") == "release/1.2"
def test_exactly_200_chars_passes() -> None:
"""The length boundary is > 200: a 200-char name is valid
(the charset allows it — a long but well-formed branch path)."""
name = "a" * 196 + "/x.1"
assert len(name) == 200
assert validate_branch_name(name) == name
# ---------------------------------------------------------------------------
# Reject
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
"raw",
[
"",
" ",
"a b", # whitespace inside
"a@b",
"a~b",
"a:b",
"a?b",
"a*b",
"a[b",
"a\\b",
"a^b",
],
)
def test_empty_and_charset_violations_rejected(raw: str) -> None:
"""Empty/whitespace-only and non-charset characters all raise."""
with pytest.raises(ValueError):
validate_branch_name(raw)
def test_201_chars_rejected() -> None:
"""One past the length limit raises (even though the charset
allows every character)."""
with pytest.raises(ValueError):
validate_branch_name("a" * 201)
@pytest.mark.parametrize("raw", ["a..b", "a//b", "..", "//"])
def test_path_sequences_rejected(raw: str) -> None:
"""``..`` and ``//`` are disallowed — traversal-shaped names,
including the two-character bare forms."""
with pytest.raises(ValueError):
validate_branch_name(raw)
@pytest.mark.parametrize("raw", ["-x", ".x", "/x"])
def test_leading_characters_rejected(raw: str) -> None:
"""A leading ``-`` (git would read it as an option), ``.``, or
``/`` raises."""
with pytest.raises(ValueError):
validate_branch_name(raw)
@pytest.mark.parametrize("raw", ["x/", "x."])
def test_trailing_characters_rejected(raw: str) -> None:
"""A trailing ``/`` or ``.`` raises."""
with pytest.raises(ValueError):
validate_branch_name(raw)
@pytest.mark.parametrize("raw", ["HEAD", "head", "HeAd", " hEaD "])
def test_head_rejected_case_insensitively(raw: str) -> None:
"""``HEAD`` in any case is git's refname — rejected (the trim runs
first, so even `` hEaD `` is the reserved name)."""
with pytest.raises(ValueError):
validate_branch_name(raw)
def test_reject_messages_are_fixed_and_input_free() -> None:
"""The ``ValueError`` reasons are fixed strings — none of them
echoes the rejected input (the API maps to the fixed-detail 422
on the same discipline; a secret-ish name typed into the field
must not surface in a log line either)."""
for raw in ("a b", "a..b", "x/", "HEAD", "a" * 201, ""):
try:
validate_branch_name(raw)
except ValueError as e:
if raw:
assert raw not in str(e)
else:
pytest.fail(f"{raw!r} must be rejected")