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