gabriel / muse public

test_update_ref_supercharge.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 """SUPERCHARGE tests for ``muse update-ref``.
2
3 Coverage tiers
4 --------------
5 - U (Unit): duration_ms / exit_code in every JSON success path
6 - E (Error): JSON errors β†’ stdout when --json; stderr in text mode
7 - S (Schema): error payload has {error, message, duration_ms, exit_code}
8 - D (Data): exit_code semantics, previous/deleted field accuracy
9 - CAS: compare-and-swap timing and error payloads
10 - P (Perf): duration_ms stays under a sane ceiling
11 - Sec (Security): no traceback on any error path; path traversal rejected
12 - C (Concurrency): independent branches updated safely in parallel threads
13
14 Utilities used
15 --------------
16 - ``long_id(hex)`` β€” ``sha256:<64-hex>`` from a bare hex string
17 - ``short_id(id)`` β€” ``sha256:<12-hex>`` abbreviated form
18 - ``blob_id(data)`` β€” ``sha256:<hex>`` of arbitrary bytes (unique IDs)
19 """
20 from __future__ import annotations
21 from collections.abc import Mapping
22
23 import datetime
24 import json
25 import pathlib
26 import threading
27 from unittest import mock
28
29 import pytest
30
31 from muse.core.errors import ExitCode
32 from muse.core.paths import muse_dir, ref_path
33 from muse.core.ids import hash_commit as compute_commit_id, hash_snapshot as compute_snapshot_id
34 from muse.core.commits import (
35 CommitRecord,
36 write_commit,
37 )
38 from muse.core.snapshots import (
39 SnapshotRecord,
40 write_snapshot,
41 )
42 from muse.core.types import blob_id, long_id, short_id
43 from tests.cli_test_helper import CliRunner, InvokeResult
44
45 runner = CliRunner()
46
47 _SNAP_ID: str = compute_snapshot_id({})
48 _COMMITTED_AT: datetime.datetime = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
49
50
51 # ---------------------------------------------------------------------------
52 # Helpers
53 # ---------------------------------------------------------------------------
54
55
56 def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path:
57 repo = tmp_path / "repo"
58 dot_muse = muse_dir(repo)
59 for sub in ("objects", "commits", "snapshots", "refs/heads"):
60 (dot_muse / sub).mkdir(parents=True)
61 (dot_muse / "HEAD").write_text("ref: refs/heads/main")
62 (dot_muse / "repo.json").write_text(json.dumps({"repo_id": "test-repo", "domain": "code"}))
63 return repo
64
65
66 def _snap(repo: pathlib.Path) -> str:
67 write_snapshot(repo, SnapshotRecord(
68 snapshot_id=_SNAP_ID,
69 manifest={},
70 created_at=_COMMITTED_AT,
71 ))
72 return _SNAP_ID
73
74
75 def _commit(repo: pathlib.Path, message: str = "test") -> str:
76 snap_id = _snap(repo)
77 commit_id = compute_commit_id(
78 parent_ids=[],
79 snapshot_id=snap_id,
80 message=message,
81 committed_at_iso=_COMMITTED_AT.isoformat(),
82 )
83 write_commit(repo, CommitRecord(
84 commit_id=commit_id,
85 branch="main",
86 snapshot_id=snap_id,
87 message=message,
88 committed_at=_COMMITTED_AT,
89 ))
90 return commit_id
91
92
93 def _write_ref(repo: pathlib.Path, branch: str, commit_id: str) -> None:
94 branch_ref = ref_path(repo, branch)
95 branch_ref.parent.mkdir(parents=True, exist_ok=True)
96 branch_ref.write_text(commit_id)
97
98
99 def _ur(repo: pathlib.Path, *args: str) -> InvokeResult:
100 from muse.cli.app import main as cli
101 return runner.invoke(
102 cli,
103 ["update-ref", *args],
104 env={"MUSE_REPO_ROOT": str(repo)},
105 )
106
107
108 # ---------------------------------------------------------------------------
109 # U β€” Unit: duration_ms and exit_code in every success JSON path
110 # ---------------------------------------------------------------------------
111
112
113 class TestElapsedMsExitCode:
114 """U1–U8: every successful JSON response carries duration_ms and exit_code."""
115
116 def test_u1_create_ref_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
117 """U1: creating a new ref emits duration_ms."""
118 repo = _make_repo(tmp_path)
119 cid = _commit(repo)
120 r = _ur(repo, "feat/alpha", cid, "--json")
121 assert r.exit_code == 0
122 data = json.loads(r.output)
123 assert "duration_ms" in data, "duration_ms missing from create-ref JSON"
124
125 def test_u2_create_ref_has_exit_code_zero(self, tmp_path: pathlib.Path) -> None:
126 """U2: creating a new ref has exit_code 0."""
127 repo = _make_repo(tmp_path)
128 cid = _commit(repo)
129 r = _ur(repo, "feat/beta", cid, "--json")
130 assert r.exit_code == 0
131 data = json.loads(r.output)
132 assert data["exit_code"] == 0
133
134 def test_u3_update_ref_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
135 """U3: updating an existing ref emits duration_ms."""
136 repo = _make_repo(tmp_path)
137 old = _commit(repo, "old")
138 new = _commit(repo, "new")
139 _write_ref(repo, "main", old)
140 r = _ur(repo, "main", new, "--json")
141 assert r.exit_code == 0
142 data = json.loads(r.output)
143 assert "duration_ms" in data
144
145 def test_u4_update_ref_has_exit_code_zero(self, tmp_path: pathlib.Path) -> None:
146 """U4: updating an existing ref has exit_code 0."""
147 repo = _make_repo(tmp_path)
148 old = _commit(repo, "old")
149 new = _commit(repo, "new")
150 _write_ref(repo, "main", old)
151 r = _ur(repo, "main", new, "--json")
152 assert r.exit_code == 0
153 assert json.loads(r.output)["exit_code"] == 0
154
155 def test_u5_delete_ref_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
156 """U5: deleting a ref emits duration_ms."""
157 repo = _make_repo(tmp_path)
158 cid = long_id("a" * 64)
159 _write_ref(repo, "to-del", cid)
160 r = _ur(repo, "--delete", "to-del", "--json")
161 assert r.exit_code == 0
162 data = json.loads(r.output)
163 assert "duration_ms" in data, "duration_ms missing from delete-ref JSON"
164
165 def test_u6_delete_ref_has_exit_code_zero(self, tmp_path: pathlib.Path) -> None:
166 """U6: deleting a ref has exit_code 0."""
167 repo = _make_repo(tmp_path)
168 cid = long_id("b" * 64)
169 _write_ref(repo, "to-del2", cid)
170 r = _ur(repo, "--delete", "to-del2", "--json")
171 assert r.exit_code == 0
172 assert json.loads(r.output)["exit_code"] == 0
173
174 def test_u7_duration_ms_is_float(self, tmp_path: pathlib.Path) -> None:
175 """U7: duration_ms is a float (not int, not string)."""
176 repo = _make_repo(tmp_path)
177 cid = _commit(repo)
178 r = _ur(repo, "timing-branch", cid, "--json")
179 assert r.exit_code == 0
180 val = json.loads(r.output)["duration_ms"]
181 assert isinstance(val, float), f"expected float, got {type(val)}"
182
183 def test_u8_duration_ms_non_negative(self, tmp_path: pathlib.Path) -> None:
184 """U8: duration_ms >= 0."""
185 repo = _make_repo(tmp_path)
186 cid = _commit(repo)
187 r = _ur(repo, "timing-branch2", cid, "--json")
188 assert r.exit_code == 0
189 assert json.loads(r.output)["duration_ms"] >= 0.0
190
191 def test_u9_no_verify_success_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
192 """U9: --no-verify path also emits duration_ms."""
193 repo = _make_repo(tmp_path)
194 cid = long_id("f" * 64) # not in store β€” valid format, skip verification
195 r = _ur(repo, "--no-verify", "staging", cid, "--json")
196 assert r.exit_code == 0
197 data = json.loads(r.output)
198 assert "duration_ms" in data
199
200 def test_u10_short_id_in_output_is_not_expected(self, tmp_path: pathlib.Path) -> None:
201 """U10: commit_id in JSON output is the full long_id (not short_id)."""
202 repo = _make_repo(tmp_path)
203 cid = _commit(repo)
204 r = _ur(repo, "full-id-branch", cid, "--json")
205 assert r.exit_code == 0
206 data = json.loads(r.output)
207 # Full id must be present β€” short_id (12 hex chars) would be truncated
208 assert data["commit_id"] == cid
209 assert len(data["commit_id"]) == 71 # sha256: + 64 hex
210
211
212 # ---------------------------------------------------------------------------
213 # E β€” Error routing: JSON errors β†’ stdout when --json, stderr in text mode
214 # ---------------------------------------------------------------------------
215
216
217 class TestJsonErrorsToStdout:
218 """E1–E7: all error paths emit JSON to stdout when --json is active."""
219
220 def test_e1_invalid_branch_json_error_on_stdout(self, tmp_path: pathlib.Path) -> None:
221 """E1: invalid branch name β†’ JSON error on stdout, stderr empty."""
222 repo = _make_repo(tmp_path)
223 r = _ur(repo, "branch\x00null", long_id("a" * 64), "--json")
224 assert r.exit_code != 0
225 assert r.stderr.strip() == "", f"stderr should be empty: {r.stderr!r}"
226 data = json.loads(r.output)
227 assert "error" in data
228
229 def test_e2_commit_not_found_json_error_on_stdout(self, tmp_path: pathlib.Path) -> None:
230 """E2: commit not in store β†’ JSON error on stdout, stderr empty."""
231 repo = _make_repo(tmp_path)
232 cid = long_id("9" * 64) # not in store, valid format
233 r = _ur(repo, "main", cid, "--json")
234 assert r.exit_code != 0
235 assert r.stderr.strip() == "", f"stderr should be empty: {r.stderr!r}"
236 data = json.loads(r.output)
237 assert "error" in data
238
239 def test_e3_invalid_commit_id_json_error_on_stdout(self, tmp_path: pathlib.Path) -> None:
240 """E3: malformed commit ID β†’ JSON error on stdout, stderr empty."""
241 repo = _make_repo(tmp_path)
242 r = _ur(repo, "main", "not-a-valid-id", "--json")
243 assert r.exit_code != 0
244 assert r.stderr.strip() == "", f"stderr should be empty: {r.stderr!r}"
245 data = json.loads(r.output)
246 assert "error" in data
247
248 def test_e4_delete_nonexistent_json_error_on_stdout(self, tmp_path: pathlib.Path) -> None:
249 """E4: --delete on nonexistent ref β†’ JSON error on stdout."""
250 repo = _make_repo(tmp_path)
251 r = _ur(repo, "--delete", "ghost-branch", "--json")
252 assert r.exit_code != 0
253 assert r.stderr.strip() == "", f"stderr should be empty: {r.stderr!r}"
254 data = json.loads(r.output)
255 assert "error" in data
256
257 def test_e5_cas_mismatch_json_error_on_stdout(self, tmp_path: pathlib.Path) -> None:
258 """E5: CAS mismatch β†’ JSON error on stdout with current/expected fields."""
259 repo = _make_repo(tmp_path)
260 actual = _commit(repo, "actual")
261 new_id = _commit(repo, "new")
262 other = blob_id(b"other-id") # valid ID, not the actual value
263 _write_ref(repo, "main", actual)
264 r = _ur(repo, "--old-value", other, "main", new_id, "--json")
265 assert r.exit_code != 0
266 assert r.stderr.strip() == "", f"stderr should be empty: {r.stderr!r}"
267 data = json.loads(r.output)
268 assert "error" in data
269
270 def test_e6_no_commit_id_json_error_on_stdout(self, tmp_path: pathlib.Path) -> None:
271 """E6: missing commit_id (no --delete) β†’ JSON error on stdout."""
272 repo = _make_repo(tmp_path)
273 r = _ur(repo, "main", "--json")
274 assert r.exit_code != 0
275 assert r.stderr.strip() == "", f"stderr should be empty: {r.stderr!r}"
276 data = json.loads(r.output)
277 assert "error" in data
278
279 def test_e7_text_mode_errors_on_stderr(self, tmp_path: pathlib.Path) -> None:
280 """E7: text mode errors go to stderr (stdout_bytes empty)."""
281 repo = _make_repo(tmp_path)
282 r = _ur(repo, "branch\x00bad", long_id("a" * 64), "--format", "text")
283 assert r.exit_code != 0
284 assert r.stdout_bytes == b"", f"stdout_bytes should be empty in text mode: {r.stdout_bytes!r}"
285 assert r.stderr.strip() != "", "stderr should have error text in text mode"
286
287 def test_e8_write_failure_json_error_on_stdout(self, tmp_path: pathlib.Path) -> None:
288 """E8: OSError from write_branch_ref β†’ exit 3, JSON error on stdout."""
289 repo = _make_repo(tmp_path)
290 cid = _commit(repo)
291 with mock.patch(
292 "muse.cli.commands.update_ref.write_branch_ref",
293 side_effect=OSError("disk full"),
294 ):
295 r = _ur(repo, "main", cid, "--json")
296 assert r.exit_code == ExitCode.INTERNAL_ERROR
297 assert r.stderr.strip() == "", f"stderr should be empty: {r.stderr!r}"
298 data = json.loads(r.output)
299 assert "error" in data
300 assert "disk full" in data.get("message", "")
301
302
303 # ---------------------------------------------------------------------------
304 # S β€” Schema completeness for error payloads
305 # ---------------------------------------------------------------------------
306
307
308 class TestErrorJsonSchema:
309 """S1–S5: every JSON error payload has {error, message, duration_ms, exit_code}."""
310
311 def _parse_error(self, r: InvokeResult) -> Mapping[str, object]:
312 return json.loads(r.output)
313
314 def _required_keys(self) -> set[str]:
315 return {"error", "message", "duration_ms", "exit_code"}
316
317 def test_s1_invalid_branch_error_schema(self, tmp_path: pathlib.Path) -> None:
318 """S1: invalid branch name error has all required keys."""
319 repo = _make_repo(tmp_path)
320 r = _ur(repo, "bad\x00branch", long_id("a" * 64), "--json")
321 data = self._parse_error(r)
322 missing = self._required_keys() - data.keys()
323 assert not missing, f"missing keys: {missing}"
324
325 def test_s2_commit_not_found_error_schema(self, tmp_path: pathlib.Path) -> None:
326 """S2: commit-not-found error has all required keys."""
327 repo = _make_repo(tmp_path)
328 r = _ur(repo, "main", long_id("e" * 64), "--json")
329 data = self._parse_error(r)
330 missing = self._required_keys() - data.keys()
331 assert not missing, f"missing keys: {missing}"
332
333 def test_s3_cas_mismatch_error_schema(self, tmp_path: pathlib.Path) -> None:
334 """S3: CAS mismatch error has all required keys."""
335 repo = _make_repo(tmp_path)
336 actual = _commit(repo, "actual")
337 new_id = _commit(repo, "new")
338 wrong = blob_id(b"wrong-old-value")
339 _write_ref(repo, "main", actual)
340 r = _ur(repo, "--old-value", wrong, "main", new_id, "--json")
341 data = self._parse_error(r)
342 missing = self._required_keys() - data.keys()
343 assert not missing, f"missing keys: {missing}"
344
345 def test_s4_write_failure_error_schema(self, tmp_path: pathlib.Path) -> None:
346 """S4: write-failure error has all required keys."""
347 repo = _make_repo(tmp_path)
348 cid = _commit(repo)
349 with mock.patch(
350 "muse.cli.commands.update_ref.write_branch_ref",
351 side_effect=OSError("ENOSPC"),
352 ):
353 r = _ur(repo, "main", cid, "--json")
354 data = self._parse_error(r)
355 missing = self._required_keys() - data.keys()
356 assert not missing, f"missing keys: {missing}"
357
358 def test_s5_error_duration_ms_is_float_non_negative(self, tmp_path: pathlib.Path) -> None:
359 """S5: duration_ms in error JSON is a float >= 0."""
360 repo = _make_repo(tmp_path)
361 r = _ur(repo, "bad\x00name", long_id("a" * 64), "--json")
362 data = self._parse_error(r)
363 assert isinstance(data["duration_ms"], float)
364 assert data["duration_ms"] >= 0.0
365
366 def test_s6_error_exit_code_matches_process_exit(self, tmp_path: pathlib.Path) -> None:
367 """S6: exit_code in JSON matches actual process exit code."""
368 repo = _make_repo(tmp_path)
369 r = _ur(repo, "bad\x00name", long_id("a" * 64), "--json")
370 data = self._parse_error(r)
371 assert data["exit_code"] == r.exit_code
372
373 def test_s7_success_json_all_fields_present(self, tmp_path: pathlib.Path) -> None:
374 """S7: create-ref success JSON has branch, commit_id, previous, duration_ms, exit_code."""
375 repo = _make_repo(tmp_path)
376 cid = _commit(repo)
377 r = _ur(repo, "schema-check", cid, "--json")
378 assert r.exit_code == 0
379 data = json.loads(r.output)
380 for key in ("branch", "commit_id", "previous", "duration_ms", "exit_code"):
381 assert key in data, f"missing key {key!r} in success JSON"
382
383 def test_s8_delete_json_all_fields_present(self, tmp_path: pathlib.Path) -> None:
384 """S8: delete-ref success JSON has branch, deleted, duration_ms, exit_code."""
385 repo = _make_repo(tmp_path)
386 cid = long_id("d" * 64)
387 _write_ref(repo, "del-schema", cid)
388 r = _ur(repo, "--delete", "del-schema", "--json")
389 assert r.exit_code == 0
390 data = json.loads(r.output)
391 for key in ("branch", "deleted", "duration_ms", "exit_code"):
392 assert key in data, f"missing key {key!r} in delete JSON"
393
394
395 # ---------------------------------------------------------------------------
396 # D β€” Data integrity
397 # ---------------------------------------------------------------------------
398
399
400 class TestDataIntegrity:
401 """D1–D8: output values are semantically correct."""
402
403 def test_d1_previous_is_none_for_new_ref(self, tmp_path: pathlib.Path) -> None:
404 """D1: previous is null when no ref existed before."""
405 repo = _make_repo(tmp_path)
406 cid = _commit(repo)
407 data = json.loads(_ur(repo, "fresh-branch", cid, "--json").output)
408 assert data["previous"] is None
409
410 def test_d2_previous_matches_old_commit(self, tmp_path: pathlib.Path) -> None:
411 """D2: previous matches the commit_id that was there before."""
412 repo = _make_repo(tmp_path)
413 old = _commit(repo, "old commit")
414 new = _commit(repo, "new commit")
415 _write_ref(repo, "main", old)
416 data = json.loads(_ur(repo, "main", new, "--json").output)
417 assert data["previous"] == old
418 assert data["commit_id"] == new
419
420 def test_d3_branch_field_matches_arg(self, tmp_path: pathlib.Path) -> None:
421 """D3: branch field in JSON matches the branch argument."""
422 repo = _make_repo(tmp_path)
423 cid = _commit(repo)
424 data = json.loads(_ur(repo, "my-feature", cid, "--json").output)
425 assert data["branch"] == "my-feature"
426
427 def test_d4_deleted_true_on_delete(self, tmp_path: pathlib.Path) -> None:
428 """D4: deleted field is boolean true on successful delete."""
429 repo = _make_repo(tmp_path)
430 _write_ref(repo, "ephemeral", long_id("e" * 64))
431 data = json.loads(_ur(repo, "--delete", "ephemeral", "--json").output)
432 assert data["deleted"] is True
433
434 def test_d5_exit_code_1_for_user_errors(self, tmp_path: pathlib.Path) -> None:
435 """D5: user-visible errors (bad branch, not-found commit) use exit_code 1."""
436 repo = _make_repo(tmp_path)
437 r = _ur(repo, "main", long_id("9" * 64), "--json") # not in store
438 data = json.loads(r.output)
439 assert data["exit_code"] == ExitCode.USER_ERROR
440
441 def test_d6_exit_code_3_for_write_failure(self, tmp_path: pathlib.Path) -> None:
442 """D6: write failure uses exit_code 3 (internal error)."""
443 repo = _make_repo(tmp_path)
444 cid = _commit(repo)
445 with mock.patch(
446 "muse.cli.commands.update_ref.write_branch_ref",
447 side_effect=OSError("ENOSPC"),
448 ):
449 r = _ur(repo, "main", cid, "--json")
450 data = json.loads(r.output)
451 assert data["exit_code"] == ExitCode.INTERNAL_ERROR
452
453 def test_d7_cas_mismatch_includes_current_and_expected(self, tmp_path: pathlib.Path) -> None:
454 """D7: CAS error JSON includes current ref value and what was expected."""
455 repo = _make_repo(tmp_path)
456 actual = _commit(repo, "actual-commit")
457 new_id = _commit(repo, "new-commit")
458 wrong_old = blob_id(b"wrong-expected-value")
459 _write_ref(repo, "main", actual)
460 r = _ur(repo, "--old-value", wrong_old, "main", new_id, "--json")
461 assert r.exit_code == ExitCode.USER_ERROR
462 data = json.loads(r.output)
463 # current and expected give agents enough context to retry correctly
464 assert "current" in data, "CAS error must include current ref value"
465 assert "expected" in data or wrong_old in str(data), "CAS error must include expected value"
466
467 def test_d8_blob_id_produces_unique_valid_ids(self, tmp_path: pathlib.Path) -> None:
468 """D8: blob_id() always produces distinct valid sha256-prefixed IDs."""
469 ids = {blob_id(__import__("os").urandom(32)) for _ in range(20)}
470 assert len(ids) == 20, "blob_id must produce unique IDs"
471 for bid in ids:
472 assert bid.startswith("sha256:")
473 assert len(bid) == 71
474
475 def test_d9_long_id_produces_correct_prefix(self, tmp_path: pathlib.Path) -> None:
476 """D9: long_id() round-trips correctly through update-ref."""
477 repo = _make_repo(tmp_path)
478 bare = "c" * 64
479 cid = long_id(bare)
480 assert cid == f"sha256:{bare}"
481 # Use it as a ref value (bypassing store check)
482 r = _ur(repo, "--no-verify", "long-id-test", cid, "--json")
483 assert r.exit_code == 0
484 data = json.loads(r.output)
485 assert data["commit_id"] == cid
486
487 def test_d10_short_id_is_prefix_of_long_id(self) -> None:
488 """D10: short_id is the first 19 chars of long_id (sha256: + 12 hex)."""
489 cid = long_id("abcdef1234567890" * 4)
490 sid = short_id(cid)
491 assert cid.startswith(sid)
492 assert sid.startswith("sha256:")
493 assert len(sid) == 19 # "sha256:" (7) + 12 hex chars
494
495
496 # ---------------------------------------------------------------------------
497 # CAS β€” compare-and-swap error payloads and timing
498 # ---------------------------------------------------------------------------
499
500
501 class TestCASSchema:
502 """CAS-specific: error routing and schema when CAS fires."""
503
504 def test_cas1_null_guard_mismatch_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
505 """CAS1: --old-value null mismatch (ref exists) error has duration_ms."""
506 repo = _make_repo(tmp_path)
507 existing = _commit(repo, "existing")
508 new_id = _commit(repo, "new")
509 _write_ref(repo, "contested", existing)
510 r = _ur(repo, "--old-value", "null", "contested", new_id, "--json")
511 assert r.exit_code != 0
512 data = json.loads(r.output)
513 assert "duration_ms" in data
514
515 def test_cas2_mismatch_error_to_stdout_not_stderr(self, tmp_path: pathlib.Path) -> None:
516 """CAS2: CAS mismatch with --json β†’ error on stdout, stderr empty."""
517 repo = _make_repo(tmp_path)
518 actual = _commit(repo, "actual")
519 new_id = _commit(repo, "new")
520 wrong = blob_id(b"wrong-cas-value")
521 _write_ref(repo, "main", actual)
522 r = _ur(repo, "--old-value", wrong, "main", new_id, "--json")
523 assert r.exit_code != 0
524 assert r.stderr.strip() == ""
525 data = json.loads(r.output)
526 assert "error" in data
527 assert "duration_ms" in data
528 assert "exit_code" in data
529
530 def test_cas3_success_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
531 """CAS3: successful CAS also emits duration_ms."""
532 repo = _make_repo(tmp_path)
533 old = _commit(repo, "old")
534 new = _commit(repo, "new")
535 _write_ref(repo, "main", old)
536 r = _ur(repo, "--old-value", old, "main", new, "--json")
537 assert r.exit_code == 0
538 data = json.loads(r.output)
539 assert "duration_ms" in data
540
541 def test_cas4_invalid_old_value_format_error_on_stdout(self, tmp_path: pathlib.Path) -> None:
542 """CAS4: bare hex (missing sha256: prefix) in --old-value β†’ JSON error on stdout."""
543 repo = _make_repo(tmp_path)
544 cid = _commit(repo)
545 bare_hex = "a" * 64 # missing sha256: prefix
546 r = _ur(repo, "--old-value", bare_hex, "main", cid, "--json")
547 assert r.exit_code != 0
548 assert r.stderr.strip() == ""
549 data = json.loads(r.output)
550 assert "error" in data
551
552
553 # ---------------------------------------------------------------------------
554 # P β€” Performance
555 # ---------------------------------------------------------------------------
556
557
558 class TestPerformance:
559 """P1–P3: duration_ms is a realistic duration."""
560
561 def test_p1_single_update_under_2000ms(self, tmp_path: pathlib.Path) -> None:
562 """P1: a single ref update finishes in < 2 seconds."""
563 repo = _make_repo(tmp_path)
564 cid = _commit(repo)
565 r = _ur(repo, "perf-branch", cid, "--json")
566 assert r.exit_code == 0
567 assert json.loads(r.output)["duration_ms"] < 2000.0
568
569 def test_p2_delete_under_2000ms(self, tmp_path: pathlib.Path) -> None:
570 """P2: a ref delete finishes in < 2 seconds."""
571 repo = _make_repo(tmp_path)
572 _write_ref(repo, "perf-del", long_id("f" * 64))
573 r = _ur(repo, "--delete", "perf-del", "--json")
574 assert r.exit_code == 0
575 assert json.loads(r.output)["duration_ms"] < 2000.0
576
577 def test_p3_200_sequential_updates_all_have_duration_ms(self, tmp_path: pathlib.Path) -> None:
578 """P3: 200 sequential updates all emit duration_ms."""
579 repo = _make_repo(tmp_path)
580 cid = _commit(repo)
581 for i in range(200):
582 r = _ur(repo, "perf-stress", cid, "--json")
583 assert r.exit_code == 0, f"failed at iteration {i}"
584 assert "duration_ms" in json.loads(r.output), f"missing duration_ms at iteration {i}"
585
586
587 # ---------------------------------------------------------------------------
588 # Sec β€” Security: no traceback on any error path
589 # ---------------------------------------------------------------------------
590
591
592 class TestSecurity:
593 """Sec1–Sec5: error paths never produce raw Python tracebacks."""
594
595 def test_sec1_no_traceback_invalid_branch_json_mode(self, tmp_path: pathlib.Path) -> None:
596 """Sec1: invalid branch name with --json β†’ no Traceback."""
597 repo = _make_repo(tmp_path)
598 r = _ur(repo, "bad\x00branch", long_id("a" * 64), "--json")
599 assert r.exit_code != 0
600 assert "Traceback" not in r.output
601 assert "Traceback" not in r.stderr
602
603 def test_sec2_no_traceback_write_failure_json_mode(self, tmp_path: pathlib.Path) -> None:
604 """Sec2: mocked write failure with --json β†’ no Traceback."""
605 repo = _make_repo(tmp_path)
606 cid = _commit(repo)
607 with mock.patch(
608 "muse.cli.commands.update_ref.write_branch_ref",
609 side_effect=OSError("permission denied"),
610 ):
611 r = _ur(repo, "main", cid, "--json")
612 assert r.exit_code != 0
613 assert "Traceback" not in r.output
614 assert "Traceback" not in r.stderr
615
616 def test_sec3_no_traceback_write_failure_text_mode(self, tmp_path: pathlib.Path) -> None:
617 """Sec3: mocked write failure in text mode β†’ no Traceback."""
618 repo = _make_repo(tmp_path)
619 cid = _commit(repo)
620 with mock.patch(
621 "muse.cli.commands.update_ref.write_branch_ref",
622 side_effect=OSError("permission denied"),
623 ):
624 r = _ur(repo, "main", cid, "--format", "text")
625 assert r.exit_code != 0
626 assert "Traceback" not in r.output
627 assert "Traceback" not in r.stderr
628
629 def test_sec4_path_traversal_in_branch_rejected(self, tmp_path: pathlib.Path) -> None:
630 """Sec4: branch names with ../ path traversal are rejected."""
631 repo = _make_repo(tmp_path)
632 cid = _commit(repo)
633 r = _ur(repo, "../../../etc/cron.d/malicious", cid, "--json")
634 assert r.exit_code != 0
635 assert r.stderr.strip() == "" # error in stdout (json mode)
636 data = json.loads(r.output)
637 assert "error" in data
638
639 def test_sec5_ansi_in_branch_rejected_json_mode(self, tmp_path: pathlib.Path) -> None:
640 """Sec5: ANSI escape codes in branch name are rejected; error to stdout."""
641 repo = _make_repo(tmp_path)
642 r = _ur(repo, "\x1b[31mbranch", long_id("a" * 64), "--json")
643 assert r.exit_code != 0
644 assert r.stderr.strip() == ""
645 data = json.loads(r.output)
646 assert "error" in data
647
648
649 # ---------------------------------------------------------------------------
650 # C β€” Concurrency: parallel updates to independent branches
651 # ---------------------------------------------------------------------------
652
653
654 class TestConcurrency:
655 """C1: N threads each update a distinct branch β€” no cross-contamination."""
656
657 def test_c1_parallel_independent_branch_updates(self, tmp_path: pathlib.Path) -> None:
658 """C1: 16 threads each write to their own branch β€” all succeed."""
659 repo = _make_repo(tmp_path)
660 cid = _commit(repo, "shared commit")
661 N = 16
662 results: list[InvokeResult | None] = [None] * N
663 errors: list[str] = []
664
665 def _worker(idx: int) -> None:
666 branch = f"concurrent-branch-{idx}"
667 r = _ur(repo, "--no-verify", branch, cid, "--json")
668 results[idx] = r
669 if r.exit_code != 0:
670 errors.append(f"thread {idx} failed: exit_code={r.exit_code}")
671
672 threads = [threading.Thread(target=_worker, args=(i,)) for i in range(N)]
673 for t in threads:
674 t.start()
675 for t in threads:
676 t.join()
677
678 assert not errors, "\n".join(errors)
679 for i, r in enumerate(results):
680 assert r is not None
681 assert r.exit_code == 0, f"thread {i} non-zero exit"
682 data = json.loads(r.output)
683 assert data["branch"] == f"concurrent-branch-{i}"
684 assert "duration_ms" in data