"""Remote tracking ref resolution — issue #7. TDD: tests are written first and must fail before the fix lands. After `muse fetch origin` the remote tracking ref is stored at .muse/remotes/origin/main. Three commands must resolve it idiomatically: RT1 muse merge origin/main — exits 0, applies delta commit RT2 muse branch -a --json — lists origin/main in remote tracking refs RT3 muse log --all --json — includes commit reachable via origin/main All tests use real local repos (no network, no mocks). """ from __future__ import annotations import json import pathlib import pytest from muse.cli.config import set_remote, set_remote_head from muse.core.object_store import write_object from muse.core.paths import init_repo_dirs, muse_dir from muse.core.ids import hash_commit as compute_commit_id, hash_snapshot as compute_snapshot_id from muse.core.refs import write_branch_ref from muse.core.commits import ( CommitRecord, write_commit, ) from muse.core.snapshots import ( SnapshotRecord, write_snapshot, ) from muse.core.types import blob_id from tests.cli_test_helper import CliRunner, InvokeResult runner = CliRunner() _REPO_ID = blob_id(b"test-remote-tracking-refs") # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _make_repo(path: pathlib.Path) -> pathlib.Path: root = init_repo_dirs(path) dot = muse_dir(root) (dot / "repo.json").write_text(json.dumps({"repo_id": _REPO_ID, "owner": "gabriel"})) (dot / "HEAD").write_text("ref: refs/heads/main\n") (dot / "config.toml").write_text("") return root import datetime as _dt def _write_commit( root: pathlib.Path, message: str, filename: str, content: bytes, parent: str | None = None, ) -> tuple[str, str]: """Write one commit with one file. Returns (commit_id, object_id).""" oid = blob_id(content) write_object(root, oid, content) manifest = {filename: oid} sid = compute_snapshot_id(manifest) snap = SnapshotRecord(snapshot_id=sid, manifest=manifest) write_snapshot(root, snap) ts = _dt.datetime(2026, 1, 1, tzinfo=_dt.timezone.utc) cid = compute_commit_id( parent_ids=[parent] if parent else [], snapshot_id=sid, message=message, committed_at_iso=ts.isoformat(), author="gabriel", ) rec = CommitRecord( commit_id=cid, branch="main", snapshot_id=sid, message=message, committed_at=ts, parent_commit_id=parent, parent2_commit_id=None, author="gabriel", metadata={}, structured_delta=None, sem_ver_bump="none", breaking_changes=[], agent_id="test", model_id="test", toolchain_id="", prompt_hash="", signature="", signer_key_id="", ) write_commit(root, rec) return cid, oid def _setup_fetch_state(tmp_path: pathlib.Path) -> tuple[pathlib.Path, str, str]: """ Build a local repo that looks as if a fetch just completed: - local main → commit A (1 file: base.txt) - remote tracking origin/main → commit B (adds delta.txt on top of A) Returns (root, commit_b_id, delta_oid). """ root = _make_repo(tmp_path) # commit A — local HEAD cid_a, _ = _write_commit(root, "initial commit", "base.txt", b"base content") write_branch_ref(root, "main", cid_a) # Write file to working tree so the merge dirty-tree check passes. (root / "base.txt").write_bytes(b"base content") # commit B — fetched, not yet merged (only the remote tracking ref points here) cid_b, delta_oid = _write_commit( root, "delta commit", "delta.txt", b"delta content", parent=cid_a ) # Wire remote and set the remote tracking ref (simulates `muse fetch origin`) set_remote("origin", "https://localhost:1337/gabriel/test-repo", root) set_remote_head("origin", "main", cid_b, root) return root, cid_b, delta_oid # --------------------------------------------------------------------------- # RT1 — muse merge origin/main exits 0 and applies the delta commit # --------------------------------------------------------------------------- class TestRT1MergeRemoteTrackingRef: def test_merge_origin_main_exits_zero(self, tmp_path: pathlib.Path) -> None: """muse merge origin/main must exit 0 after a fetch.""" root, cid_b, _ = _setup_fetch_state(tmp_path) result: InvokeResult = runner.invoke(None, ["merge", "origin/main"], cwd=root) assert result.exit_code == 0, ( f"muse merge origin/main failed (exit {result.exit_code})\n" f"stdout: {result.stdout[:400]}\n" f"stderr: {result.stderr[:400]}" ) def test_merge_origin_main_advances_head(self, tmp_path: pathlib.Path) -> None: """After muse merge origin/main, local main must point to commit B.""" from muse.core.refs import get_head_commit_id root, cid_b, _ = _setup_fetch_state(tmp_path) result: InvokeResult = runner.invoke(None, ["merge", "origin/main"], cwd=root) assert result.exit_code == 0, f"merge failed:\n{result.stderr[:300]}" head = get_head_commit_id(root, "main") assert head == cid_b, ( f"local main not advanced to remote commit\n" f" expected: {cid_b}\n" f" got: {head}" ) def test_merge_origin_main_applies_delta_file(self, tmp_path: pathlib.Path) -> None: """After muse merge origin/main, delta.txt must appear in the working tree.""" root, _, _ = _setup_fetch_state(tmp_path) result: InvokeResult = runner.invoke(None, ["merge", "origin/main"], cwd=root) assert result.exit_code == 0, f"merge failed:\n{result.stderr[:300]}" assert (root / "delta.txt").exists(), "delta.txt missing after merge" assert (root / "delta.txt").read_bytes() == b"delta content" # --------------------------------------------------------------------------- # RT2 — muse branch -a --json lists remote tracking branches # --------------------------------------------------------------------------- class TestRT2BranchListsRemoteTrackingRefs: def test_branch_all_includes_origin_main(self, tmp_path: pathlib.Path) -> None: """muse branch -a --json must include origin/main in the output.""" root, cid_b, _ = _setup_fetch_state(tmp_path) result: InvokeResult = runner.invoke(None, ["branch", "-a", "--json"], cwd=root) assert result.exit_code == 0, f"muse branch -a failed:\n{result.stderr[:300]}" data = json.loads(result.stdout) names = [b["name"] for b in data] assert "remotes/origin/main" in names, ( f"remotes/origin/main not listed in branch -a output\n" f" branches: {names}" ) def test_branch_all_remote_has_correct_commit(self, tmp_path: pathlib.Path) -> None: """origin/main entry must carry the fetched commit ID.""" root, cid_b, _ = _setup_fetch_state(tmp_path) result: InvokeResult = runner.invoke(None, ["branch", "-a", "--json"], cwd=root) assert result.exit_code == 0, f"muse branch -a failed:\n{result.stderr[:300]}" data = json.loads(result.stdout) remote_entry = next((b for b in data if b["name"] == "remotes/origin/main"), None) assert remote_entry is not None, "remotes/origin/main entry missing" assert remote_entry["commit_id"] == cid_b, ( f"origin/main commit_id wrong\n" f" expected: {cid_b}\n" f" got: {remote_entry.get('commit_id')}" ) # --------------------------------------------------------------------------- # RT3 — muse log --all --json includes commits reachable via origin/main # --------------------------------------------------------------------------- class TestRT3LogAllIncludesRemoteCommits: def test_log_all_includes_fetched_commit(self, tmp_path: pathlib.Path) -> None: """muse log --all --json must include the commit pointed to by origin/main.""" root, cid_b, _ = _setup_fetch_state(tmp_path) result: InvokeResult = runner.invoke(None, ["log", "--all", "--json"], cwd=root) assert result.exit_code == 0, f"muse log --all failed:\n{result.stderr[:300]}" data = json.loads(result.stdout) commit_ids = [c["commit_id"] for c in data["commits"]] assert cid_b in commit_ids, ( f"fetched commit not in log --all output\n" f" expected: {cid_b}\n" f" found: {commit_ids}" ) def test_log_all_shows_both_local_and_remote_commits(self, tmp_path: pathlib.Path) -> None: """muse log --all must show commits from both local branch and remote tracking refs.""" root, cid_b, _ = _setup_fetch_state(tmp_path) from muse.core.refs import get_head_commit_id cid_a = get_head_commit_id(root, "main") result: InvokeResult = runner.invoke(None, ["log", "--all", "--json"], cwd=root) assert result.exit_code == 0, f"muse log --all failed:\n{result.stderr[:300]}" data = json.loads(result.stdout) commit_ids = {c["commit_id"] for c in data["commits"]} assert cid_a in commit_ids, f"local commit A missing from log --all" assert cid_b in commit_ids, f"remote commit B missing from log --all"