gabriel / muse public
test_cmd_update_ref.py python
336 lines 12.5 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 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 tests.cli_test_helper import CliRunner, InvokeResult
22
23 runner = CliRunner()
24
25 _SNAP_ID: str = compute_snapshot_id({})
26 _COMMITTED_AT: datetime.datetime = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
27
28
29 # ---------------------------------------------------------------------------
30 # Helpers
31 # ---------------------------------------------------------------------------
32
33 def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path:
34 repo = tmp_path / "repo"
35 muse = repo / ".muse"
36 for sub in ("objects", "commits", "snapshots", "refs/heads"):
37 (muse / sub).mkdir(parents=True)
38 (muse / "HEAD").write_text("ref: refs/heads/main")
39 (muse / "repo.json").write_text(json.dumps({"repo_id": "test-repo", "domain": "code"}))
40 return repo
41
42
43 def _snap(repo: pathlib.Path) -> str:
44 """Write an empty-manifest snapshot; return its content-addressed ID."""
45 write_snapshot(repo, SnapshotRecord(
46 snapshot_id=_SNAP_ID,
47 manifest={},
48 created_at=_COMMITTED_AT,
49 ))
50 return _SNAP_ID
51
52
53 def _commit(repo: pathlib.Path, message: str = "test") -> str:
54 """Write a commit with a real content-addressed ID; return the commit_id."""
55 snap_id = _snap(repo)
56 commit_id = compute_commit_id(
57 repo_id="test-repo",
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 commit_id=commit_id,
65 repo_id="test-repo",
66 created_on_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 = repo / ".muse" / "refs" / "heads" / 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 (repo / ".muse" / "refs" / "heads" / "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 (repo / ".muse" / "refs" / "heads" / "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 (repo / ".muse" / "refs" / "heads" / "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 = "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("bb" + "0" * 62)
250 wrong = long_id("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 (repo / ".muse" / "refs" / "heads" / "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):
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 3 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 137 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 140 days ago