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