"""Phase 4 TDD tests for ``muse bridge git-status`` — drift counts. NOTE: git subprocess calls in this file are INTENTIONAL — they create real git repositories used as bridge sources. The muse codebase otherwise never uses git. """ from __future__ import annotations import json import os import pathlib import subprocess import pytest from muse.core.paths import init_repo_dirs from muse.core.types import fake_id from muse.core.bridge.state import BridgeState from tests.cli_test_helper import CliRunner runner = CliRunner() # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _invoke(*args: str, cwd: pathlib.Path | None = None) -> "CliRunner": return runner.invoke(None, list(args), cwd=cwd) def _make_muse_repo(path: pathlib.Path) -> pathlib.Path: path.mkdir(parents=True, exist_ok=True) result = _invoke("init", cwd=path) assert result.exit_code == 0, f"muse init failed: {result.stderr}" return path def _make_git_repo(path: pathlib.Path) -> pathlib.Path: """Create a minimal git repo with one initial commit.""" path.mkdir(parents=True, exist_ok=True) subprocess.run(["git", "init", str(path)], check=True, capture_output=True) subprocess.run( ["git", "-C", str(path), "config", "user.email", "test@test.com"], check=True, capture_output=True, ) subprocess.run( ["git", "-C", str(path), "config", "user.name", "Test"], check=True, capture_output=True, ) (path / "README.md").write_text("init") subprocess.run(["git", "-C", str(path), "add", "."], check=True, capture_output=True) subprocess.run( ["git", "-C", str(path), "commit", "-m", "init"], check=True, capture_output=True, ) return path def _git_head_sha(git_dir: pathlib.Path) -> str: return subprocess.check_output( ["git", "-C", str(git_dir), "rev-parse", "HEAD"], text=True, ).strip() def _git_add_commit(git_dir: pathlib.Path, filename: str, content: str, msg: str) -> str: (git_dir / filename).write_text(content) subprocess.run(["git", "-C", str(git_dir), "add", "."], check=True, capture_output=True) subprocess.run( ["git", "-C", str(git_dir), "commit", "-m", msg], check=True, capture_output=True, ) return _git_head_sha(git_dir) def _write_bridge_state(muse_root: pathlib.Path, state: BridgeState) -> None: from muse.core.bridge.state import write_bridge_state write_bridge_state(muse_root, state) # =========================================================================== # Tests # =========================================================================== class TestGitStatusNoBridgeState: """git-status with no bridge state at all.""" def test_git_status_no_bridge_state_shows_none(self, tmp_path: pathlib.Path) -> None: """Fresh muse repo: git-status prints '(none)' for both last-import and last-export.""" muse_root = _make_muse_repo(tmp_path / "muse") result = _invoke("bridge", "git-status", cwd=muse_root) assert result.exit_code == 0, result.stderr out = result.output assert "(none)" in out def test_git_status_no_bridge_state_json_has_keys(self, tmp_path: pathlib.Path) -> None: """JSON output always has last_import and last_export keys.""" muse_root = _make_muse_repo(tmp_path / "muse") result = _invoke("bridge", "git-status", "--json", cwd=muse_root) assert result.exit_code == 0, result.stderr data = json.loads(result.output.strip()) assert "last_import" in data assert "last_export" in data class TestGitStatusJsonOutput: """JSON output shape for git-status.""" def test_git_status_json_has_last_import_last_export(self, tmp_path: pathlib.Path) -> None: """JSON output has both last_import and last_export keys even when empty.""" muse_root = _make_muse_repo(tmp_path / "muse") result = _invoke("bridge", "git-status", "--json", cwd=muse_root) assert result.exit_code == 0, result.stderr data = json.loads(result.output.strip()) assert isinstance(data["last_import"], dict) assert isinstance(data["last_export"], dict) def test_git_status_json_drift_absent_without_git_dir(self, tmp_path: pathlib.Path) -> None: """Without --git-dir, JSON output has no 'drift' key.""" muse_root = _make_muse_repo(tmp_path / "muse") result = _invoke("bridge", "git-status", "--json", cwd=muse_root) assert result.exit_code == 0, result.stderr data = json.loads(result.output.strip()) assert "drift" not in data def test_git_status_json_drift_present_with_git_dir(self, tmp_path: pathlib.Path) -> None: """With --git-dir pointing at a real git repo, 'drift' key is present in JSON.""" muse_root = _make_muse_repo(tmp_path / "muse") git_dir = _make_git_repo(tmp_path / "git") result = _invoke( "bridge", "git-status", "--git-dir", str(git_dir), "--json", cwd=muse_root, ) assert result.exit_code == 0, result.stderr data = json.loads(result.output.strip()) assert "drift" in data assert "git_commits_since_import" in data["drift"] assert "muse_commits_since_export" in data["drift"] class TestGitStatusAfterImport: """git-status reflects state written by a successful import.""" def test_git_status_after_import_shows_git_sha(self, tmp_path: pathlib.Path) -> None: """After writing bridge state with last_import, git-status shows the git SHA.""" muse_root = _make_muse_repo(tmp_path / "muse") fake_sha = "a" * 40 fake_cid = fake_id("commit-1") _write_bridge_state(muse_root, { "last_import": { "git_sha": fake_sha, "git_ref": "main", "git_remote": "origin", "muse_branch": "main", "muse_commit_id": fake_cid, "imported_at": "2026-04-14T10:00:00Z", "commits_written": 5, }, "last_export": {}, }) result = _invoke("bridge", "git-status", cwd=muse_root) assert result.exit_code == 0, result.stderr out = result.output assert "aaaaaaaaaaaaaaaa" in out # first 16 chars of SHA def test_git_status_after_import_json_shows_git_sha(self, tmp_path: pathlib.Path) -> None: """After writing import state, JSON last_import has git_sha field.""" muse_root = _make_muse_repo(tmp_path / "muse") fake_sha = "b" * 40 fake_cid = fake_id("commit-2") _write_bridge_state(muse_root, { "last_import": { "git_sha": fake_sha, "git_ref": "main", "git_remote": "origin", "muse_branch": "main", "muse_commit_id": fake_cid, "imported_at": "2026-04-14T10:00:00Z", "commits_written": 2, }, "last_export": {}, }) result = _invoke("bridge", "git-status", "--json", cwd=muse_root) assert result.exit_code == 0, result.stderr data = json.loads(result.output.strip()) assert data["last_import"]["git_sha"] == fake_sha class TestGitStatusDrift: """Drift count computation in git-status.""" def test_git_status_with_git_dir_shows_drift(self, tmp_path: pathlib.Path) -> None: """git repo with 2 commits since last-import git_sha → drift=2.""" muse_root = _make_muse_repo(tmp_path / "muse") git_dir = _make_git_repo(tmp_path / "git") # The initial commit is the "sync point" base_sha = _git_head_sha(git_dir) # Add 2 more commits _git_add_commit(git_dir, "a.txt", "a", "commit A") _git_add_commit(git_dir, "b.txt", "b", "commit B") fake_cid = fake_id("commit-muse") _write_bridge_state(muse_root, { "last_import": { "git_sha": base_sha, "git_ref": "main", "git_remote": "origin", "muse_branch": "main", "muse_commit_id": fake_cid, "imported_at": "2026-04-14T10:00:00Z", "commits_written": 1, }, "last_export": {}, }) result = _invoke( "bridge", "git-status", "--git-dir", str(git_dir), "--json", cwd=muse_root, ) assert result.exit_code == 0, result.stderr data = json.loads(result.output.strip()) assert data["drift"]["git_commits_since_import"] == 2 def test_git_status_zero_drift_when_up_to_date(self, tmp_path: pathlib.Path) -> None: """git repo at same commit as last-import → drift=0.""" muse_root = _make_muse_repo(tmp_path / "muse") git_dir = _make_git_repo(tmp_path / "git") current_sha = _git_head_sha(git_dir) fake_cid = fake_id("commit-muse") _write_bridge_state(muse_root, { "last_import": { "git_sha": current_sha, "git_ref": "main", "git_remote": "origin", "muse_branch": "main", "muse_commit_id": fake_cid, "imported_at": "2026-04-14T10:00:00Z", "commits_written": 1, }, "last_export": {}, }) result = _invoke( "bridge", "git-status", "--git-dir", str(git_dir), "--json", cwd=muse_root, ) assert result.exit_code == 0, result.stderr data = json.loads(result.output.strip()) assert data["drift"]["git_commits_since_import"] == 0 def test_git_status_drift_none_without_import_state(self, tmp_path: pathlib.Path) -> None: """No import state → git_commits_since_import is None.""" muse_root = _make_muse_repo(tmp_path / "muse") git_dir = _make_git_repo(tmp_path / "git") result = _invoke( "bridge", "git-status", "--git-dir", str(git_dir), "--json", cwd=muse_root, ) assert result.exit_code == 0, result.stderr data = json.loads(result.output.strip()) assert data["drift"]["git_commits_since_import"] is None class TestGitStatusTextOutput: """Text output format for git-status.""" def test_git_status_text_output_has_drift_section(self, tmp_path: pathlib.Path) -> None: """Text output with --git-dir contains 'Drift:' section.""" muse_root = _make_muse_repo(tmp_path / "muse") git_dir = _make_git_repo(tmp_path / "git") base_sha = _git_head_sha(git_dir) _git_add_commit(git_dir, "x.txt", "x", "commit X") fake_cid = fake_id("commit-muse-export") _write_bridge_state(muse_root, { "last_import": { "git_sha": base_sha, "git_ref": "main", "git_remote": "origin", "muse_branch": "main", "muse_commit_id": fake_cid, "imported_at": "2026-04-14T10:00:00Z", "commits_written": 1, }, "last_export": {}, }) result = _invoke( "bridge", "git-status", "--git-dir", str(git_dir), cwd=muse_root, ) assert result.exit_code == 0, result.stderr assert "Drift:" in result.output def test_git_status_text_shows_commits_to_import(self, tmp_path: pathlib.Path) -> None: """Text output shows 'commits to import' in Drift section.""" muse_root = _make_muse_repo(tmp_path / "muse") git_dir = _make_git_repo(tmp_path / "git") base_sha = _git_head_sha(git_dir) _git_add_commit(git_dir, "c.txt", "c", "commit C") fake_cid = fake_id("commit-muse-x") _write_bridge_state(muse_root, { "last_import": { "git_sha": base_sha, "git_ref": "main", "git_remote": "origin", "muse_branch": "main", "muse_commit_id": fake_cid, "imported_at": "2026-04-14T10:00:00Z", "commits_written": 1, }, "last_export": {}, }) result = _invoke( "bridge", "git-status", "--git-dir", str(git_dir), cwd=muse_root, ) assert result.exit_code == 0, result.stderr assert "to import" in result.output def test_git_status_text_no_drift_without_git_dir(self, tmp_path: pathlib.Path) -> None: """Without --git-dir, text output does NOT show 'Drift:' section.""" muse_root = _make_muse_repo(tmp_path / "muse") result = _invoke("bridge", "git-status", cwd=muse_root) assert result.exit_code == 0, result.stderr assert "Drift:" not in result.output def test_git_status_text_shows_header(self, tmp_path: pathlib.Path) -> None: """Text output always starts with 'Muse Bridge Status'.""" muse_root = _make_muse_repo(tmp_path / "muse") result = _invoke("bridge", "git-status", cwd=muse_root) assert result.exit_code == 0, result.stderr assert "Muse Bridge Status" in result.output