"""Onboarding guards (CUST-WP-0067-T04). The guards matter more than the happy path: onboarding that silently accepts an unpushed or unclassified repository is what produces hub records nobody can re-derive. """ from __future__ import annotations import subprocess from pathlib import Path import pytest from repo_manager.commands import repo_onboard as ro def _git(repo: Path, *args: str) -> None: subprocess.run(["git", "-C", str(repo), *args], check=True, capture_output=True) def _repo(tmp_path: Path, *, classification: str | None = None) -> Path: repo = tmp_path / "demo-repo" repo.mkdir() _git(repo, "init", "-q", "-b", "main") _git(repo, "config", "user.email", "t@example.com") _git(repo, "config", "user.name", "t") (repo / "README.md").write_text("x\n", encoding="utf-8") if classification is not None: (repo / ro.CLASSIFICATION_FILENAME).write_text(classification, encoding="utf-8") _git(repo, "add", "-A") _git(repo, "commit", "-qm", "init") return repo VALID = """repo_classification: category: tooling domain: infotech """ def test_missing_classification_is_blocked(tmp_path): repo = _repo(tmp_path) with pytest.raises(ro.OnboardError, match="is missing"): ro.onboard_repo(repo, api_base="http://hub.invalid", dry_run=True) def test_invalid_classification_is_blocked(tmp_path): repo = _repo(tmp_path, classification="repo_classification:\n category: nope\n domain: bogus\n") with pytest.raises(ro.OnboardError, match="invalid"): ro.onboard_repo(repo, api_base="http://hub.invalid", dry_run=True) def test_no_upstream_is_blocked(tmp_path): """Central derives from what it can fetch, so a local-only branch cannot onboard.""" repo = _repo(tmp_path, classification=VALID) with pytest.raises(ro.OnboardError, match="upstream"): ro.onboard_repo(repo, api_base="http://hub.invalid", dry_run=True) def test_uncommitted_classification_is_blocked(tmp_path): repo = _repo(tmp_path, classification=VALID) (repo / ro.CLASSIFICATION_FILENAME).write_text(VALID + " capability_tags: [extra]\n", encoding="utf-8") with pytest.raises(ro.OnboardError, match="uncommitted"): ro.onboard_repo(repo, api_base="http://hub.invalid", dry_run=True) def test_unpushed_commits_are_blocked(tmp_path): """A record whose backing file is only local cannot be re-derived by anyone.""" origin = tmp_path / "origin.git" subprocess.run(["git", "init", "-q", "--bare", str(origin)], check=True, capture_output=True) repo = _repo(tmp_path, classification=VALID) _git(repo, "remote", "add", "origin", str(origin)) _git(repo, "push", "-q", "-u", "origin", "main") (repo / "extra.txt").write_text("y\n", encoding="utf-8") _git(repo, "add", "-A") _git(repo, "commit", "-qm", "later") with pytest.raises(ro.OnboardError, match="not pushed"): ro.onboard_repo(repo, api_base="http://hub.invalid", dry_run=True) def test_dry_run_reports_without_writing(tmp_path, monkeypatch): origin = tmp_path / "origin.git" subprocess.run(["git", "init", "-q", "--bare", str(origin)], check=True, capture_output=True) repo = _repo(tmp_path, classification=VALID) _git(repo, "remote", "add", "origin", str(origin)) _git(repo, "push", "-q", "-u", "origin", "main") calls: list[tuple[str, str]] = [] def fake_hub(api_base, method, path, body=None): calls.append((method, path)) return 404, {} monkeypatch.setattr(ro, "_hub", fake_hub) report = ro.onboard_repo(repo, api_base="http://hub.invalid", dry_run=True, slug="demo") assert report["action"] == "would-create" assert all(method == "GET" for method, _ in calls), calls