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>
71 lines
2.3 KiB
Python
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()
|