gabriel / muse public

test_cmd_update_ref.py file-level

at sha256:8 · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:4 Merge branch 'dev' into main · gabriel · Jun 17, 2026
1 """Comprehensive tests for ``muse update-ref``.
2
3 Coverage tiers
4 --------------
5 - Unit: _FORMAT_CHOICES
6 - Integration: create ref, update ref, delete ref, --no-verify, text format
7 - CAS: --old-value happy path, mismatch, null guard
8 - Security: ANSI/null in branch name rejected, errors to stderr, no traceback
9 - Stress: 200 sequential updates
10 """
11 from __future__ import annotations
12
13 import datetime
14 import json
15 import pathlib
16
17 from muse.core.errors import ExitCode
18 from muse.core.types import fake_id, long_id
19 from muse.core.ids import hash_commit, hash_snapshot
20 from muse.core.commits import (
21 CommitRecord,
22 write_commit,
23 )
24 from muse.core.snapshots import (
25 SnapshotRecord,
26 write_snapshot,
27 )
28 from muse.core.paths import heads_dir, muse_dir, ref_path
29 from tests.cli_test_helper import CliRunner, InvokeResult
30
31 runner = CliRunner()
32
33 _SNAP_ID: str = hash_snapshot({})
34 _COMMITTED_AT: datetime.datetime = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
35
36
37 # ---------------------------------------------------------------------------
38 # Helpers
39 # ---------------------------------------------------------------------------
40
41 def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path:
42 repo = tmp_path / "repo"
43 dot_muse = muse_dir(repo)
44 for sub in ("objects", "commits", "snapshots", "refs/heads"):
45 (dot_muse / sub).mkdir(parents=True)
46 (dot_muse / "HEAD").write_text("ref: refs/heads/main")
47 (dot_muse / "repo.json").write_text(json.dumps({"repo_id": "test-repo", "domain": "code"}))
48 return repo
49
50
51 def _snap(repo: pathlib.Path) -> str:
52 """Write an empty-manifest snapshot; return its content-addressed ID."""
53 write_snapshot(repo, SnapshotRecord(
54 snapshot_id=_SNAP_ID,
55 manifest={},
56 created_at=_COMMITTED_AT,
57 ))
58 return _SNAP_ID
59
60
61 def _commit(repo: pathlib.Path, message: str = "test") -> str:
62 """Write a commit with a real content-addressed ID; return the commit_id."""
63 snap_id = _snap(repo)
64 commit_id = hash_commit(
65 parent_ids=[],
66 snapshot_id=snap_id,
67 message=message,
68 committed_at_iso=_COMMITTED_AT.isoformat(),
69 )
70 write_commit(repo, CommitRecord(
71 commit_id=commit_id,
72 branch="main",
73 snapshot_id=snap_id,
74 message=message,
75 committed_at=_COMMITTED_AT,
76 ))
77 return commit_id
78
79
80 def _write_ref(repo: pathlib.Path, branch: str, commit_id: str) -> None:
81 ref = ref_path(repo, branch)
82 ref.parent.mkdir(parents=True, exist_ok=True)
83 ref.write_text(commit_id)
84
85
86 def _ur(repo: pathlib.Path, *args: str) -> InvokeResult:
87 from muse.cli.app import main as cli
88 return runner.invoke(
89 cli,
90 ["update-ref", *args],
91 env={"MUSE_REPO_ROOT": str(repo)},
92 )
93
94
95 # ---------------------------------------------------------------------------
96 # Unit
97 # ---------------------------------------------------------------------------
98
99
100 class TestUnit:
101 def test_json_flag_registered(self) -> None:
102 import argparse
103 from muse.cli.commands.update_ref import register
104 p = argparse.ArgumentParser()
105 sub = p.add_subparsers()
106 register(sub)
107 ns = p.parse_args(["update-ref", "--json", "main"])
108 assert ns.json_out is True
109
110
111 # ---------------------------------------------------------------------------
112 # Integration — create and update
113 # ---------------------------------------------------------------------------
114
115
116 class TestCreateUpdate:
117 def test_creates_new_ref(self, tmp_path: pathlib.Path) -> None:
118 repo = _make_repo(tmp_path)
119 cid = _commit(repo, "create ref test")
120 result = _ur(repo, "--json", "feature", cid)
121 assert result.exit_code == 0
122 data = json.loads(result.output)
123 assert data["branch"] == "feature"
124 assert data["commit_id"] == cid
125 assert (heads_dir(repo) / "feature").read_text() == cid
126
127 def test_previous_is_null_for_new_ref(self, tmp_path: pathlib.Path) -> None:
128 repo = _make_repo(tmp_path)
129 cid = _commit(repo, "new ref test")
130 data = json.loads(_ur(repo, "--json", "new-branch", cid).output)
131 assert data["previous"] is None
132
133 def test_updates_existing_ref(self, tmp_path: pathlib.Path) -> None:
134 repo = _make_repo(tmp_path)
135 old_id = _commit(repo, "old commit")
136 new_id = _commit(repo, "new commit")
137 _write_ref(repo, "main", old_id)
138 data = json.loads(_ur(repo, "--json", "main", new_id).output)
139 assert data["previous"] == old_id
140 assert data["commit_id"] == new_id
141
142 def test_json_shorthand(self, tmp_path: pathlib.Path) -> None:
143 repo = _make_repo(tmp_path)
144 cid = _commit(repo, "json shorthand test")
145 result = _ur(repo, "--json", "main", cid)
146 assert result.exit_code == 0
147 assert "commit_id" in json.loads(result.output)
148
149 def test_text_mode_silent_on_success(self, tmp_path: pathlib.Path) -> None:
150 repo = _make_repo(tmp_path)
151 cid = _commit(repo, "text format test")
152 result = _ur(repo, "main", cid)
153 assert result.exit_code == 0
154 assert result.output.strip() == ""
155
156
157 # ---------------------------------------------------------------------------
158 # Integration — delete
159 # ---------------------------------------------------------------------------
160
161
162 class TestDeleteRef:
163 def test_delete_existing_ref(self, tmp_path: pathlib.Path) -> None:
164 repo = _make_repo(tmp_path)
165 _write_ref(repo, "todelete", "5" * 64)
166 result = _ur(repo, "--json", "--delete", "todelete")
167 assert result.exit_code == 0
168 data = json.loads(result.output)
169 assert data["deleted"] is True
170 assert not (heads_dir(repo) / "todelete").exists()
171
172 def test_delete_nonexistent_ref_errors(self, tmp_path: pathlib.Path) -> None:
173 repo = _make_repo(tmp_path)
174 result = _ur(repo, "--delete", "ghost-branch")
175 assert result.exit_code == ExitCode.USER_ERROR
176
177 def test_delete_text_mode_silent(self, tmp_path: pathlib.Path) -> None:
178 repo = _make_repo(tmp_path)
179 _write_ref(repo, "to-del", "6" * 64)
180 result = _ur(repo, "--delete", "to-del")
181 assert result.exit_code == 0
182 assert result.output.strip() == ""
183
184
185 # ---------------------------------------------------------------------------
186 # Integration — --no-verify
187 # ---------------------------------------------------------------------------
188
189
190 class TestNoVerify:
191 def test_no_verify_accepts_unknown_commit(self, tmp_path: pathlib.Path) -> None:
192 repo = _make_repo(tmp_path)
193 cid = long_id("7" * 64) # valid format but not in store
194 result = _ur(repo, "--no-verify", "staging", cid)
195 assert result.exit_code == 0
196 assert (heads_dir(repo) / "staging").read_text() == cid
197
198 def test_verify_rejects_unknown_commit(self, tmp_path: pathlib.Path) -> None:
199 repo = _make_repo(tmp_path)
200 cid = long_id("8" * 64) # valid format but not in store
201 result = _ur(repo, "main", cid)
202 assert result.exit_code == ExitCode.USER_ERROR
203
204
205 # ---------------------------------------------------------------------------
206 # CAS — compare-and-swap
207 # ---------------------------------------------------------------------------
208
209
210 class TestCAS:
211 def test_cas_succeeds_when_current_matches(self, tmp_path: pathlib.Path) -> None:
212 repo = _make_repo(tmp_path)
213 old_id = _commit(repo, "cas old commit")
214 new_id = _commit(repo, "cas new commit")
215 _write_ref(repo, "main", old_id)
216 result = _ur(repo, "--json", "--old-value", old_id, "main", new_id)
217 assert result.exit_code == 0
218 data = json.loads(result.output)
219 assert data["commit_id"] == new_id
220
221 def test_cas_fails_when_current_differs(self, tmp_path: pathlib.Path) -> None:
222 repo = _make_repo(tmp_path)
223 actual = _commit(repo, "actual commit")
224 new_id = _commit(repo, "new commit")
225 # Use a different (non-stored) ID as the expected old value.
226 expected = f"c1{'0' * 62}"
227 _write_ref(repo, "main", actual)
228 result = _ur(repo, "--old-value", expected, "main", new_id)
229 assert result.exit_code == ExitCode.USER_ERROR
230
231 def test_cas_null_succeeds_when_ref_absent(self, tmp_path: pathlib.Path) -> None:
232 """--old-value null asserts the ref does not yet exist."""
233 repo = _make_repo(tmp_path)
234 cid = _commit(repo, "cas null test")
235 result = _ur(repo, "--no-verify", "--old-value", "null", "brand-new", cid)
236 assert result.exit_code == 0
237
238 def test_cas_null_fails_when_ref_exists(self, tmp_path: pathlib.Path) -> None:
239 repo = _make_repo(tmp_path)
240 existing = _commit(repo, "existing commit")
241 new_id = _commit(repo, "new commit for null cas")
242 _write_ref(repo, "contested", existing)
243 result = _ur(repo, "--old-value", "null", "contested", new_id)
244 assert result.exit_code == ExitCode.USER_ERROR
245
246 def test_cas_delete_succeeds_when_matches(self, tmp_path: pathlib.Path) -> None:
247 repo = _make_repo(tmp_path)
248 cid = fake_id("aa")
249 _write_ref(repo, "conditioned", cid)
250 result = _ur(repo, "--delete", "--old-value", cid, "conditioned")
251 assert result.exit_code == 0
252
253 def test_cas_delete_fails_when_differs(self, tmp_path: pathlib.Path) -> None:
254 repo = _make_repo(tmp_path)
255 actual = long_id(f"bb{'0' * 62}")
256 wrong = long_id(f"cc{'0' * 62}")
257 _write_ref(repo, "conditioned", actual)
258 result = _ur(repo, "--delete", "--old-value", wrong, "conditioned")
259 assert result.exit_code == ExitCode.USER_ERROR
260 assert (heads_dir(repo) / "conditioned").exists()
261
262
263 # ---------------------------------------------------------------------------
264 # Error cases
265 # ---------------------------------------------------------------------------
266
267
268 class TestErrors:
269 def test_invalid_branch_name_rejected(self, tmp_path: pathlib.Path) -> None:
270 repo = _make_repo(tmp_path)
271 result = _ur(repo, "branch\x00null", "a" * 64)
272 assert result.exit_code == ExitCode.USER_ERROR
273
274 def test_invalid_commit_id_rejected(self, tmp_path: pathlib.Path) -> None:
275 repo = _make_repo(tmp_path)
276 result = _ur(repo, "main", "not-hex")
277 assert result.exit_code == ExitCode.USER_ERROR
278
279 def test_no_commit_id_without_delete_errors(self, tmp_path: pathlib.Path) -> None:
280 repo = _make_repo(tmp_path)
281 result = _ur(repo, "main")
282 assert result.exit_code == ExitCode.USER_ERROR
283
284
285 # ---------------------------------------------------------------------------
286 # Security
287 # ---------------------------------------------------------------------------
288
289
290 class TestSecurity:
291 def test_ansi_in_branch_rejected(self, tmp_path: pathlib.Path) -> None:
292 repo = _make_repo(tmp_path)
293 result = _ur(repo, "\x1b[31mbranch", "a" * 64)
294 assert result.exit_code == ExitCode.USER_ERROR
295
296 def test_no_traceback_on_bad_branch(self, tmp_path: pathlib.Path) -> None:
297 repo = _make_repo(tmp_path)
298 result = _ur(repo, "bad\x00branch", "a" * 64)
299 assert "Traceback" not in result.output
300
301
302 # ---------------------------------------------------------------------------
303 # Stress
304 # ---------------------------------------------------------------------------
305
306
307 class TestStress:
308 def test_200_sequential_updates(self, tmp_path: pathlib.Path) -> None:
309 repo = _make_repo(tmp_path)
310 cid = _commit(repo, "stress test commit")
311 for i in range(200):
312 result = _ur(repo, "--json", "stress-branch", cid)
313 assert result.exit_code == 0, f"failed at iteration {i}"
314 data = json.loads(result.output)
315 assert data["commit_id"] == cid
316
317
318 # ---------------------------------------------------------------------------
319 # Flag registration
320 # ---------------------------------------------------------------------------
321
322
323 class TestRegisterFlags:
324 def _parse(self, *args: str) -> "argparse.Namespace":
325 import argparse
326 from muse.cli.commands.update_ref import register
327 p = argparse.ArgumentParser()
328 sub = p.add_subparsers()
329 register(sub)
330 return p.parse_args(["update-ref", *args])
331
332 def test_default_json_out_is_false(self) -> None:
333 ns = self._parse("main")
334 assert ns.json_out is False
335
336 def test_json_flag_sets_json_out(self) -> None:
337 ns = self._parse("--json", "main")
338 assert ns.json_out is True
339
340 def test_j_shorthand_sets_json_out(self) -> None:
341 ns = self._parse("-j", "main")
342 assert ns.json_out is True