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