Files
nisse/nisse-dev/tests/test_git.py
T
dongarandClaude Opus 5 2071e6ab6d
CI / checks (push) Successful in 16s
CI / checks (pull_request) Successful in 14s
Add Gitea client and git operations (B2) — step 6a, part 1
The standalone half of the Gitea layer; orchestrator wiring follows
once the executor loop lands, to avoid colliding with it in
control.py.

- gitea.py: GiteaClient over httpx — open PRs, list and post issue
  comments, close PRs. find_pull_request is the A3 external-truth
  query that makes opening a PR idempotent across a crash, and it
  checks closed PRs too, since a crash after opening must not be
  answered with "nothing is open, so open another"
- pull_request_reconciler: an unreachable Gitea returns None rather
  than a guess, so the agent gates to the human instead of risking a
  duplicate side effect
- git.py: branch, commit, push for the workspace. The token is passed
  per-invocation rather than stored as a remote, so it never lands in
  .git/config, and anything raised is scrubbed of it
- commit_all returns None when there is nothing to commit: an empty
  milestone is a fact to report, not an error
- Live test does the real round trip against the sandbox repo and
  cleans up after itself

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 20:27:34 +00:00

71 lines
2.3 KiB
Python

from pathlib import Path
import pytest
from nisse_dev.git import GitError, GitRepo, _scrub, authenticated_url
@pytest.fixture
def repo(tmp_path: Path) -> GitRepo:
r = GitRepo(tmp_path / "work")
r.init()
r.configure_identity("nisse-dev-testing", "nisse@example.test")
return r
def test_init_creates_repo_on_default_branch(repo: GitRepo) -> None:
assert repo.current_branch() == "main"
def test_commit_all_returns_sha_and_branch_switching(repo: GitRepo) -> None:
(repo.root / "a.txt").write_text("a")
sha = repo.commit_all("first")
assert sha is not None
assert sha == repo.head_sha()
repo.checkout_new_branch("milestone-1")
assert repo.current_branch() == "milestone-1"
def test_commit_with_nothing_staged_returns_none(repo: GitRepo) -> None:
(repo.root / "a.txt").write_text("a")
repo.commit_all("first")
# An empty milestone is a fact to report, not an error to raise.
assert repo.commit_all("nothing changed") is None
def test_has_changes_tracks_the_worktree(repo: GitRepo) -> None:
assert not repo.has_changes()
(repo.root / "a.txt").write_text("a")
assert repo.has_changes()
def test_authenticated_url_shape() -> None:
url = authenticated_url("https://gitea.test/", "o/r", "bot", "secret")
assert url == "https://bot:secret@gitea.test/o/r.git"
def test_push_failure_does_not_leak_the_token(repo: GitRepo) -> None:
(repo.root / "a.txt").write_text("a")
repo.commit_all("first")
url = authenticated_url("https://127.0.0.1:1", "o/r", "bot", "sup3rsecret")
with pytest.raises(GitError) as excinfo:
repo.push(url, "main", token="sup3rsecret")
# git redacts credentials from its own messages, but that is git's
# behaviour, not ours; the scrub below is the guarantee we control.
assert "sup3rsecret" not in str(excinfo.value)
def test_scrub_removes_the_token_when_git_does_echo_it() -> None:
assert _scrub("failed to push to https://bot:s3cret@h/o/r.git", "s3cret") == (
"failed to push to https://bot:***@h/o/r.git"
)
assert _scrub("no secret here", None) == "no secret here"
def test_git_error_on_bad_command(tmp_path: Path) -> None:
repo = GitRepo(tmp_path / "not-a-repo")
repo.root.mkdir()
with pytest.raises(GitError):
repo.head_sha()