gabriel / muse public
test_remote_tracking_refs.py python
230 lines 9.3 KB
Raw
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