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