test_remote_tracking_refs.py
python
sha256:ff478cfdcdd4b7fd6de89cb68896601a981f945634463275ec333bd20ca36402
Merge branch 'dev' into main
Human
21 days ago
| 1 | """Remote tracking ref resolution — issue #7. |
| 2 | |
| 3 | TDD: tests are written first and must fail before the fix lands. |
| 4 | |
| 5 | After `muse fetch origin` the remote tracking ref is stored at |
| 6 | .muse/remotes/origin/main. Three commands must resolve it idiomatically: |
| 7 | |
| 8 | RT1 muse merge origin/main — exits 0, applies delta commit |
| 9 | RT2 muse branch -a --json — lists origin/main in remote tracking refs |
| 10 | RT3 muse log --all --json — includes commit reachable via origin/main |
| 11 | |
| 12 | All tests use real local repos (no network, no mocks). |
| 13 | """ |
| 14 | from __future__ import annotations |
| 15 | |
| 16 | import json |
| 17 | import pathlib |
| 18 | |
| 19 | import pytest |
| 20 | |
| 21 | from muse.cli.config import set_remote, set_remote_head |
| 22 | from muse.core.object_store import write_object |
| 23 | from muse.core.paths import init_repo_dirs, muse_dir |
| 24 | from muse.core.ids import hash_commit as compute_commit_id, hash_snapshot as compute_snapshot_id |
| 25 | from muse.core.refs import write_branch_ref |
| 26 | from muse.core.commits import ( |
| 27 | CommitRecord, |
| 28 | write_commit, |
| 29 | ) |
| 30 | from muse.core.snapshots import ( |
| 31 | SnapshotRecord, |
| 32 | write_snapshot, |
| 33 | ) |
| 34 | from muse.core.types import blob_id |
| 35 | from tests.cli_test_helper import CliRunner, InvokeResult |
| 36 | |
| 37 | runner = CliRunner() |
| 38 | |
| 39 | _REPO_ID = blob_id(b"test-remote-tracking-refs") |
| 40 | |
| 41 | |
| 42 | # --------------------------------------------------------------------------- |
| 43 | # Helpers |
| 44 | # --------------------------------------------------------------------------- |
| 45 | |
| 46 | def _make_repo(path: pathlib.Path) -> pathlib.Path: |
| 47 | root = init_repo_dirs(path) |
| 48 | dot = muse_dir(root) |
| 49 | (dot / "repo.json").write_text(json.dumps({"repo_id": _REPO_ID, "owner": "gabriel"})) |
| 50 | (dot / "HEAD").write_text("ref: refs/heads/main\n") |
| 51 | (dot / "config.toml").write_text("") |
| 52 | return root |
| 53 | |
| 54 | |
| 55 | import datetime as _dt |
| 56 | |
| 57 | def _write_commit( |
| 58 | root: pathlib.Path, |
| 59 | message: str, |
| 60 | filename: str, |
| 61 | content: bytes, |
| 62 | parent: str | None = None, |
| 63 | ) -> tuple[str, str]: |
| 64 | """Write one commit with one file. Returns (commit_id, object_id).""" |
| 65 | oid = blob_id(content) |
| 66 | write_object(root, oid, content) |
| 67 | manifest = {filename: oid} |
| 68 | sid = compute_snapshot_id(manifest) |
| 69 | snap = SnapshotRecord(snapshot_id=sid, manifest=manifest) |
| 70 | write_snapshot(root, snap) |
| 71 | ts = _dt.datetime(2026, 1, 1, tzinfo=_dt.timezone.utc) |
| 72 | cid = compute_commit_id( |
| 73 | parent_ids=[parent] if parent else [], |
| 74 | snapshot_id=sid, |
| 75 | message=message, |
| 76 | committed_at_iso=ts.isoformat(), |
| 77 | author="gabriel", |
| 78 | ) |
| 79 | rec = CommitRecord( |
| 80 | commit_id=cid, |
| 81 | branch="main", |
| 82 | snapshot_id=sid, |
| 83 | message=message, |
| 84 | committed_at=ts, |
| 85 | parent_commit_id=parent, |
| 86 | parent2_commit_id=None, |
| 87 | author="gabriel", |
| 88 | metadata={}, |
| 89 | structured_delta=None, |
| 90 | sem_ver_bump="none", |
| 91 | breaking_changes=[], |
| 92 | agent_id="test", |
| 93 | model_id="test", |
| 94 | toolchain_id="", |
| 95 | prompt_hash="", |
| 96 | signature="", |
| 97 | signer_key_id="", |
| 98 | ) |
| 99 | write_commit(root, rec) |
| 100 | return cid, oid |
| 101 | |
| 102 | |
| 103 | def _setup_fetch_state(tmp_path: pathlib.Path) -> tuple[pathlib.Path, str, str]: |
| 104 | """ |
| 105 | Build a local repo that looks as if a fetch just completed: |
| 106 | |
| 107 | - local main → commit A (1 file: base.txt) |
| 108 | - remote tracking origin/main → commit B (adds delta.txt on top of A) |
| 109 | |
| 110 | Returns (root, commit_b_id, delta_oid). |
| 111 | """ |
| 112 | root = _make_repo(tmp_path) |
| 113 | |
| 114 | # commit A — local HEAD |
| 115 | cid_a, _ = _write_commit(root, "initial commit", "base.txt", b"base content") |
| 116 | write_branch_ref(root, "main", cid_a) |
| 117 | # Write file to working tree so the merge dirty-tree check passes. |
| 118 | (root / "base.txt").write_bytes(b"base content") |
| 119 | |
| 120 | # commit B — fetched, not yet merged (only the remote tracking ref points here) |
| 121 | cid_b, delta_oid = _write_commit( |
| 122 | root, "delta commit", "delta.txt", b"delta content", parent=cid_a |
| 123 | ) |
| 124 | |
| 125 | # Wire remote and set the remote tracking ref (simulates `muse fetch origin`) |
| 126 | set_remote("origin", "https://localhost:1337/gabriel/test-repo", root) |
| 127 | set_remote_head("origin", "main", cid_b, root) |
| 128 | |
| 129 | return root, cid_b, delta_oid |
| 130 | |
| 131 | |
| 132 | # --------------------------------------------------------------------------- |
| 133 | # RT1 — muse merge origin/main exits 0 and applies the delta commit |
| 134 | # --------------------------------------------------------------------------- |
| 135 | |
| 136 | class TestRT1MergeRemoteTrackingRef: |
| 137 | def test_merge_origin_main_exits_zero(self, tmp_path: pathlib.Path) -> None: |
| 138 | """muse merge origin/main must exit 0 after a fetch.""" |
| 139 | root, cid_b, _ = _setup_fetch_state(tmp_path) |
| 140 | result: InvokeResult = runner.invoke(None, ["merge", "origin/main"], cwd=root) |
| 141 | assert result.exit_code == 0, ( |
| 142 | f"muse merge origin/main failed (exit {result.exit_code})\n" |
| 143 | f"stdout: {result.stdout[:400]}\n" |
| 144 | f"stderr: {result.stderr[:400]}" |
| 145 | ) |
| 146 | |
| 147 | def test_merge_origin_main_advances_head(self, tmp_path: pathlib.Path) -> None: |
| 148 | """After muse merge origin/main, local main must point to commit B.""" |
| 149 | from muse.core.refs import get_head_commit_id |
| 150 | root, cid_b, _ = _setup_fetch_state(tmp_path) |
| 151 | result: InvokeResult = runner.invoke(None, ["merge", "origin/main"], cwd=root) |
| 152 | assert result.exit_code == 0, f"merge failed:\n{result.stderr[:300]}" |
| 153 | head = get_head_commit_id(root, "main") |
| 154 | assert head == cid_b, ( |
| 155 | f"local main not advanced to remote commit\n" |
| 156 | f" expected: {cid_b}\n" |
| 157 | f" got: {head}" |
| 158 | ) |
| 159 | |
| 160 | def test_merge_origin_main_applies_delta_file(self, tmp_path: pathlib.Path) -> None: |
| 161 | """After muse merge origin/main, delta.txt must appear in the working tree.""" |
| 162 | root, _, _ = _setup_fetch_state(tmp_path) |
| 163 | result: InvokeResult = runner.invoke(None, ["merge", "origin/main"], cwd=root) |
| 164 | assert result.exit_code == 0, f"merge failed:\n{result.stderr[:300]}" |
| 165 | assert (root / "delta.txt").exists(), "delta.txt missing after merge" |
| 166 | assert (root / "delta.txt").read_bytes() == b"delta content" |
| 167 | |
| 168 | |
| 169 | # --------------------------------------------------------------------------- |
| 170 | # RT2 — muse branch -a --json lists remote tracking branches |
| 171 | # --------------------------------------------------------------------------- |
| 172 | |
| 173 | class TestRT2BranchListsRemoteTrackingRefs: |
| 174 | def test_branch_all_includes_origin_main(self, tmp_path: pathlib.Path) -> None: |
| 175 | """muse branch -a --json must include origin/main in the output.""" |
| 176 | root, cid_b, _ = _setup_fetch_state(tmp_path) |
| 177 | result: InvokeResult = runner.invoke(None, ["branch", "-a", "--json"], cwd=root) |
| 178 | assert result.exit_code == 0, f"muse branch -a failed:\n{result.stderr[:300]}" |
| 179 | data = json.loads(result.stdout) |
| 180 | names = [b["name"] for b in data] |
| 181 | assert "remotes/origin/main" in names, ( |
| 182 | f"remotes/origin/main not listed in branch -a output\n" |
| 183 | f" branches: {names}" |
| 184 | ) |
| 185 | |
| 186 | def test_branch_all_remote_has_correct_commit(self, tmp_path: pathlib.Path) -> None: |
| 187 | """origin/main entry must carry the fetched commit ID.""" |
| 188 | root, cid_b, _ = _setup_fetch_state(tmp_path) |
| 189 | result: InvokeResult = runner.invoke(None, ["branch", "-a", "--json"], cwd=root) |
| 190 | assert result.exit_code == 0, f"muse branch -a failed:\n{result.stderr[:300]}" |
| 191 | data = json.loads(result.stdout) |
| 192 | remote_entry = next((b for b in data if b["name"] == "remotes/origin/main"), None) |
| 193 | assert remote_entry is not None, "remotes/origin/main entry missing" |
| 194 | assert remote_entry["commit_id"] == cid_b, ( |
| 195 | f"origin/main commit_id wrong\n" |
| 196 | f" expected: {cid_b}\n" |
| 197 | f" got: {remote_entry.get('commit_id')}" |
| 198 | ) |
| 199 | |
| 200 | |
| 201 | # --------------------------------------------------------------------------- |
| 202 | # RT3 — muse log --all --json includes commits reachable via origin/main |
| 203 | # --------------------------------------------------------------------------- |
| 204 | |
| 205 | class TestRT3LogAllIncludesRemoteCommits: |
| 206 | def test_log_all_includes_fetched_commit(self, tmp_path: pathlib.Path) -> None: |
| 207 | """muse log --all --json must include the commit pointed to by origin/main.""" |
| 208 | root, cid_b, _ = _setup_fetch_state(tmp_path) |
| 209 | result: InvokeResult = runner.invoke(None, ["log", "--all", "--json"], cwd=root) |
| 210 | assert result.exit_code == 0, f"muse log --all failed:\n{result.stderr[:300]}" |
| 211 | data = json.loads(result.stdout) |
| 212 | commit_ids = [c["commit_id"] for c in data["commits"]] |
| 213 | assert cid_b in commit_ids, ( |
| 214 | f"fetched commit not in log --all output\n" |
| 215 | f" expected: {cid_b}\n" |
| 216 | f" found: {commit_ids}" |
| 217 | ) |
| 218 | |
| 219 | def test_log_all_shows_both_local_and_remote_commits(self, tmp_path: pathlib.Path) -> None: |
| 220 | """muse log --all must show commits from both local branch and remote tracking refs.""" |
| 221 | root, cid_b, _ = _setup_fetch_state(tmp_path) |
| 222 | from muse.core.refs import get_head_commit_id |
| 223 | cid_a = get_head_commit_id(root, "main") |
| 224 | |
| 225 | result: InvokeResult = runner.invoke(None, ["log", "--all", "--json"], cwd=root) |
| 226 | assert result.exit_code == 0, f"muse log --all failed:\n{result.stderr[:300]}" |
| 227 | data = json.loads(result.stdout) |
| 228 | commit_ids = {c["commit_id"] for c in data["commits"]} |
| 229 | assert cid_a in commit_ids, f"local commit A missing from log --all" |
| 230 | assert cid_b in commit_ids, f"remote commit B missing from log --all" |
File History
1 commit
sha256:ff478cfdcdd4b7fd6de89cb68896601a981f945634463275ec333bd20ca36402
Merge branch 'dev' into main
Human
21 days ago