"""TDD — CLI flag coverage for muse push. Gap 4: --dry-run, --delete, --force-with-lease, --set-upstream are implemented in run() but have no tests. Test plan --------- DRY1 --dry-run exits 0 and makes no HTTP calls to the remote. DRY2 --dry-run output includes commit and object count. DRY3 --dry-run with no branch commits exits 1. DEL1 --delete calls transport.delete_branch_remote with the branch name. DEL2 --delete unknown branch (404 from remote) exits 0 with "already absent". DEL3 --delete dry-run exits 0 without calling delete_branch_remote. DEL4 --delete default-branch rejection (409 from remote) exits 1. LEASE1 --force-with-lease rejected (exit 1) when remote has advanced since last fetch (cached_head != live remote_head). LEASE2 --force-with-lease proceeds when cached_head == live remote_head. UP1 --set-upstream records tracking after successful push. """ from __future__ import annotations import datetime import json import pathlib from unittest.mock import MagicMock, patch, AsyncMock import msgpack import pytest from muse._version import __version__ from muse.core.mpack import PushResult, RemoteInfo from muse.core.object_store import write_object from muse.core.paths import heads_dir, muse_dir, remotes_dir from muse.core.ids import hash_commit as compute_commit_id, hash_snapshot as compute_snapshot_id _Headers = dict[str, str] # HTTP header map from muse.core.refs import get_head_commit_id from muse.core.commits import ( CommitRecord, write_commit, ) from muse.core.snapshots import ( SnapshotRecord, write_snapshot, ) from muse.core.transport import TransportError from muse.core.types import Manifest, blob_id from tests.cli_test_helper import CliRunner cli = None runner = CliRunner() # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _bare_repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path: muse = muse_dir(tmp_path) for d in ("commits", "snapshots", "objects", "refs/heads", "remotes"): (muse / d).mkdir(parents=True, exist_ok=True) (muse / "HEAD").write_text("ref: refs/heads/main\n") (muse / "repo.json").write_text( json.dumps({"repo_id": "test-repo", "schema_version": __version__, "domain": "code"}) ) (muse / "config.toml").write_text('[remotes.origin]\nurl = "https://hub.example.com/r"\n') monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path)) monkeypatch.chdir(tmp_path) return tmp_path def _make_commit( root: pathlib.Path, label: str, parent_id: str | None = None, content: bytes | None = None, ) -> CommitRecord: raw = content if content is not None else f"content-{label}".encode() oid = blob_id(raw) write_object(root, oid, raw) manifest: Manifest = {"file.txt": oid} snap_id = compute_snapshot_id(manifest) write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest)) committed_at = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc) parent_ids = [parent_id] if parent_id else [] cid = compute_commit_id( parent_ids=parent_ids, snapshot_id=snap_id, message=f"commit {label}", committed_at_iso=committed_at.isoformat(), ) commit = CommitRecord( commit_id=cid, branch="main", snapshot_id=snap_id, message=f"commit {label}", committed_at=committed_at, parent_commit_id=parent_id, ) write_commit(root, commit) return commit def _fake_resp(body: bytes, status: int = 200) -> MagicMock: r = MagicMock() r.status_code = status r.content = body r.headers = {"content-type": "application/x-msgpack"} r.text = "" return r def _mpack_push_transport(local_head: str) -> MagicMock: """Transport mock that makes _push_mpack succeed.""" transport = MagicMock() transport.fetch_remote_info.return_value = RemoteInfo( domain="code", default_branch="main", branch_heads={}, ) mock_req = MagicMock() mock_req.headers = {"Authorization": "MSign stub", "Content-Type": "application/x-msgpack"} transport._build_request.return_value = mock_req return transport def _fake_httpx_client(local_head: str) -> MagicMock: """Fake httpx AsyncClient that makes _run_mpack_path succeed.""" client = MagicMock() client.__aenter__ = AsyncMock(return_value=client) client.__aexit__ = AsyncMock(return_value=False) async def _post(url: str, *, content: bytes, headers: _Headers) -> MagicMock: if "mpack-presign" in url: return _fake_resp(msgpack.packb( {"upload_url": "https://minio.example.com/put?sig=x"}, use_bin_type=True, )) return _fake_resp(msgpack.packb( {"job_id": "j", "head": local_head, "branch": "main", "objects_in_mpack": 0, "commits_in_mpack": 0}, use_bin_type=True, )) async def _put(url: str, *, content: bytes) -> MagicMock: return _fake_resp(b"", 200) client.post = _post client.put = _put return client # =========================================================================== # DRY — --dry-run # =========================================================================== class TestDryRun: def test_dry1_no_http_calls( self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch ) -> None: """--dry-run must exit 0 without opening any HTTP connection.""" root = _bare_repo(tmp_path, monkeypatch) commit = _make_commit(root, "dry1") (heads_dir(root) / "main").write_text(commit.commit_id) transport = MagicMock() transport.fetch_remote_info.side_effect = AssertionError( "--dry-run must not call fetch_remote_info" ) with patch("muse.cli.commands.push.make_transport", return_value=transport): result = runner.invoke(cli, ["push", "origin", "--dry-run"], catch_exceptions=False) assert result.exit_code == 0, result.output transport.fetch_remote_info.assert_not_called() def test_dry2_output_includes_counts( self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch ) -> None: """--dry-run output must mention commit and object counts.""" root = _bare_repo(tmp_path, monkeypatch) commit = _make_commit(root, "dry2") (heads_dir(root) / "main").write_text(commit.commit_id) with patch("muse.cli.commands.push.make_transport", return_value=MagicMock()): result = runner.invoke(cli, ["push", "origin", "--dry-run"], catch_exceptions=False) assert result.exit_code == 0, result.output # The output must mention the dry-run nature assert "dry" in result.output.lower() or "would" in result.output.lower(), ( f"Dry-run output should mention 'dry' or 'would': {result.output!r}" ) def test_dry3_json_status_is_dry_run( self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch ) -> None: """--dry-run --json must return status='dry_run'.""" root = _bare_repo(tmp_path, monkeypatch) commit = _make_commit(root, "dry3") (heads_dir(root) / "main").write_text(commit.commit_id) with patch("muse.cli.commands.push.make_transport", return_value=MagicMock()): result = runner.invoke( cli, ["push", "origin", "--dry-run", "--json"], catch_exceptions=False ) assert result.exit_code == 0, result.output data = json.loads(result.output) assert data["status"] == "dry_run" assert data["dry_run"] is True # =========================================================================== # DEL — --delete # =========================================================================== class TestDelete: def test_del1_calls_delete_branch_remote( self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch ) -> None: """--delete must invoke transport.delete_branch_remote with the branch name.""" root = _bare_repo(tmp_path, monkeypatch) _make_commit(root, "del1") # give main a commit so branch exists transport = MagicMock() transport.delete_branch_remote.return_value = None # success with patch("muse.cli.commands.push.make_transport", return_value=transport): result = runner.invoke( cli, ["push", "origin", "main", "--delete"], catch_exceptions=False ) assert result.exit_code == 0, result.output transport.delete_branch_remote.assert_called_once() _, args, _ = transport.delete_branch_remote.mock_calls[0] # Second positional arg is the branch name assert "main" in args or "main" in str(transport.delete_branch_remote.call_args) def test_del2_404_treated_as_already_absent( self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch ) -> None: """--delete on a branch that doesn't exist (404) must exit 0.""" root = _bare_repo(tmp_path, monkeypatch) transport = MagicMock() transport.delete_branch_remote.side_effect = TransportError("not found", 404) with patch("muse.cli.commands.push.make_transport", return_value=transport): result = runner.invoke( cli, ["push", "origin", "stale-branch", "--delete"], catch_exceptions=False ) assert result.exit_code == 0, f"404 delete should succeed: {result.output}" assert "absent" in result.output.lower() or "already" in result.output.lower(), ( f"Output should note branch was already absent: {result.output!r}" ) def test_del3_dry_run_skips_transport( self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch ) -> None: """--delete --dry-run must exit 0 without calling delete_branch_remote.""" root = _bare_repo(tmp_path, monkeypatch) transport = MagicMock() with patch("muse.cli.commands.push.make_transport", return_value=transport): result = runner.invoke( cli, ["push", "origin", "my-branch", "--delete", "--dry-run"], catch_exceptions=False, ) assert result.exit_code == 0, result.output transport.delete_branch_remote.assert_not_called() def test_del4_default_branch_rejected( self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch ) -> None: """--delete on the default branch (409 from server) must exit 1.""" root = _bare_repo(tmp_path, monkeypatch) transport = MagicMock() transport.delete_branch_remote.side_effect = TransportError("default branch", 409) with patch("muse.cli.commands.push.make_transport", return_value=transport): result = runner.invoke( cli, ["push", "origin", "main", "--delete"], catch_exceptions=False ) assert result.exit_code != 0, "Deleting default branch must exit non-zero" # =========================================================================== # LEASE — --force-with-lease # =========================================================================== class TestForceWithLease: def test_lease1_rejected_when_remote_advanced( self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch ) -> None: """--force-with-lease must exit 1 when the remote HEAD has advanced past the locally cached tracking ref.""" root = _bare_repo(tmp_path, monkeypatch) c1 = _make_commit(root, "lease-base") c2 = _make_commit(root, "lease-local", parent_id=c1.commit_id) c_remote_new = _make_commit(root, "remote-advanced", parent_id=c1.commit_id) (heads_dir(root) / "main").write_text(c2.commit_id) # Tracking ref says c1 was the last-fetched remote HEAD origin_dir = remotes_dir(root) / "origin" origin_dir.mkdir(parents=True, exist_ok=True) (origin_dir / "main").write_text(c1.commit_id) # Live remote reports c_remote_new (someone else pushed) transport = MagicMock() transport.fetch_remote_info.return_value = RemoteInfo( domain="code", default_branch="main", branch_heads={"main": c_remote_new.commit_id}, ) with patch("muse.cli.commands.push.make_transport", return_value=transport): result = runner.invoke( cli, ["push", "origin", "--force-with-lease"], catch_exceptions=False ) assert result.exit_code != 0, ( "--force-with-lease must be rejected when remote advanced" ) def test_lease2_proceeds_when_cache_matches_remote( self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch ) -> None: """--force-with-lease proceeds when cached_head matches live remote_head.""" root = _bare_repo(tmp_path, monkeypatch) c1 = _make_commit(root, "lease2-base") c2 = _make_commit(root, "lease2-new", parent_id=c1.commit_id) (heads_dir(root) / "main").write_text(c2.commit_id) # Tracking ref says c1 — same as what the live remote reports origin_dir = remotes_dir(root) / "origin" origin_dir.mkdir(parents=True, exist_ok=True) (origin_dir / "main").write_text(c1.commit_id) transport = _mpack_push_transport(c2.commit_id) transport.fetch_remote_info.return_value = RemoteInfo( domain="code", default_branch="main", branch_heads={"main": c1.commit_id}, # matches cached tracking ref ) fake_client = _fake_httpx_client(c2.commit_id) with ( patch("muse.cli.commands.push.make_transport", return_value=transport), patch("muse.cli.commands.push._httpx.AsyncClient", return_value=fake_client), patch("muse.cli.commands.push._make_r2_client", return_value=fake_client), ): result = runner.invoke( cli, ["push", "origin", "--force-with-lease"], catch_exceptions=False ) assert result.exit_code == 0, ( f"--force-with-lease must proceed when cache matches remote: {result.output}" ) # =========================================================================== # UP — --set-upstream # =========================================================================== class TestSetUpstream: def test_up1_set_upstream_after_successful_push( self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch ) -> None: """--set-upstream must record the upstream tracking relationship after push.""" root = _bare_repo(tmp_path, monkeypatch) commit = _make_commit(root, "up1") (heads_dir(root) / "main").write_text(commit.commit_id) transport = _mpack_push_transport(commit.commit_id) fake_client = _fake_httpx_client(commit.commit_id) with ( patch("muse.cli.commands.push.make_transport", return_value=transport), patch("muse.cli.commands.push._httpx.AsyncClient", return_value=fake_client), patch("muse.cli.commands.push._make_r2_client", return_value=fake_client), patch("muse.cli.commands.push.set_upstream") as mock_set_upstream, ): result = runner.invoke( cli, ["push", "origin", "-u"], catch_exceptions=False ) assert result.exit_code == 0, result.output mock_set_upstream.assert_called_once_with("main", "origin", root)