gabriel / muse public
test_verify_supercharge.py python
564 lines 21.5 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
1 """Supercharge tests for ``muse verify``.
2
3 Every JSON success response must carry ``duration_ms`` (float, ms) and
4 ``exit_code`` (int). Every JSON error response routes to *stdout*, not stderr,
5 so agent pipelines never receive mixed-mode output.
6
7 Coverage tiers
8 --------------
9 U — duration_ms / exit_code on all code paths (clean, failures, no-objects,
10 branch-scoped, fail-fast)
11 E — JSON error routing: _emit_error() writes to stdout in JSON mode, stderr in
12 text mode; no traceback on any error path
13 S — Schema completeness: all _VerifyJson fields present in every success
14 response; all _VerifyErrorJson fields present in every error response
15 D — Data integrity: exit_code=0 ↔ all_ok=True; exit_code=1 ↔ all_ok=False;
16 duration_ms > 0; duration_ms is float; counters are non-negative
17 IO — OSError during run_verify → exit_code=3 in JSON, stderr in text mode
18 P — Performance: duration_ms < 5 000 ms for a 50-commit chain; monotone
19 (two runs on same repo differ only by noise)
20 Sec — No traceback on any error; no raw Python exception in stdout
21 C — Concurrent readers produce valid JSON (10 threads, same repo)
22 """
23
24 from __future__ import annotations
25 from collections.abc import Mapping
26
27 import datetime
28 import json
29 import pathlib
30 import threading
31 import unittest.mock as mock
32
33 import pytest
34 from tests.cli_test_helper import CliRunner, InvokeResult
35
36 from muse.core._types import blob_id, long_id, short_id
37 from muse.core.object_store import object_path, write_object
38 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
39 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
40 from muse.core.verify import run_verify
41
42 runner = CliRunner()
43 cli = None # argparse migration — CliRunner ignores this arg
44
45 _REPO_ID = "verify-supercharge-test"
46
47
48 # ---------------------------------------------------------------------------
49 # Helpers
50 # ---------------------------------------------------------------------------
51
52
53 def _sha(data: bytes) -> str:
54 return blob_id(data)
55
56
57 def _init_repo(path: pathlib.Path) -> pathlib.Path:
58 muse = path / ".muse"
59 for d in ("commits", "snapshots", "objects", "refs/heads"):
60 (muse / d).mkdir(parents=True, exist_ok=True)
61 (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
62 (muse / "repo.json").write_text(
63 json.dumps({"repo_id": _REPO_ID, "domain": "midi"}), encoding="utf-8"
64 )
65 return path
66
67
68 def _env(repo: pathlib.Path) -> Mapping[str, str]:
69 return {"MUSE_REPO_ROOT": str(repo)}
70
71
72 def _make_commit(
73 root: pathlib.Path,
74 parent_id: str | None = None,
75 content: bytes = b"data",
76 branch: str = "main",
77 idx: int = 0,
78 ) -> str:
79 raw = content + str(idx).encode()
80 obj_id = _sha(raw)
81 write_object(root, obj_id, raw)
82 manifest = {f"file_{idx}.txt": obj_id}
83 snap_id = compute_snapshot_id(manifest)
84 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
85 committed_at = (
86 datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
87 + datetime.timedelta(hours=idx)
88 )
89 parent_ids = [parent_id] if parent_id else []
90 commit_id = compute_commit_id(
91 repo_id=_REPO_ID,
92 parent_ids=parent_ids,
93 snapshot_id=snap_id,
94 message=f"commit {idx}",
95 committed_at_iso=committed_at.isoformat(),
96 )
97 write_commit(
98 root,
99 CommitRecord(
100 commit_id=commit_id,
101 repo_id=_REPO_ID,
102 created_on_branch=branch,
103 snapshot_id=snap_id,
104 message=f"commit {idx}",
105 committed_at=committed_at,
106 parent_commit_id=parent_id,
107 ),
108 )
109 (root / ".muse" / "refs" / "heads" / branch).write_text(commit_id, encoding="utf-8")
110 return commit_id
111
112
113 def _invoke(repo: pathlib.Path, *args: str) -> InvokeResult:
114 from muse.cli.app import main as cli_main
115 return runner.invoke(cli_main, ["verify", *args], env=_env(repo))
116
117
118 # ---------------------------------------------------------------------------
119 # U — duration_ms and exit_code on all code paths
120 # ---------------------------------------------------------------------------
121
122
123 class TestElapsedAndExitCode:
124 def test_clean_repo_exit_code_zero(self, tmp_path: pathlib.Path) -> None:
125 repo = _init_repo(tmp_path)
126 _make_commit(repo, idx=0)
127 r = _invoke(repo, "--json")
128 assert r.exit_code == 0
129 d = json.loads(r.output)
130 assert d["exit_code"] == 0
131
132 def test_clean_repo_duration_ms_present(self, tmp_path: pathlib.Path) -> None:
133 repo = _init_repo(tmp_path)
134 _make_commit(repo, idx=0)
135 r = _invoke(repo, "--json")
136 d = json.loads(r.output)
137 assert "duration_ms" in d
138 assert isinstance(d["duration_ms"], float)
139 assert d["duration_ms"] > 0
140
141 def test_failures_exit_code_one(self, tmp_path: pathlib.Path) -> None:
142 repo = _init_repo(tmp_path)
143 # Write a ref pointing at a non-existent commit (bare hex — invalid ref format)
144 (repo / ".muse" / "refs" / "heads" / "main").write_text("b" * 64)
145 r = _invoke(repo, "--json")
146 assert r.exit_code == 1
147 d = json.loads(r.output)
148 assert d["exit_code"] == 1
149 assert d["all_ok"] is False
150
151 def test_failures_duration_ms_present(self, tmp_path: pathlib.Path) -> None:
152 repo = _init_repo(tmp_path)
153 (repo / ".muse" / "refs" / "heads" / "main").write_text("c" * 64)
154 r = _invoke(repo, "--json")
155 d = json.loads(r.output)
156 assert isinstance(d["duration_ms"], float)
157 assert d["duration_ms"] > 0
158
159 def test_no_objects_exit_code_zero(self, tmp_path: pathlib.Path) -> None:
160 repo = _init_repo(tmp_path)
161 _make_commit(repo, idx=1)
162 r = _invoke(repo, "--json", "--no-objects")
163 assert r.exit_code == 0
164 d = json.loads(r.output)
165 assert d["exit_code"] == 0
166 assert d["duration_ms"] > 0
167
168 def test_branch_scoped_exit_code_zero(self, tmp_path: pathlib.Path) -> None:
169 repo = _init_repo(tmp_path)
170 _make_commit(repo, idx=2)
171 r = _invoke(repo, "--json", "--branch", "main")
172 assert r.exit_code == 0
173 d = json.loads(r.output)
174 assert d["exit_code"] == 0
175 assert d["duration_ms"] > 0
176
177 def test_fail_fast_exit_code_one(self, tmp_path: pathlib.Path) -> None:
178 repo = _init_repo(tmp_path)
179 (repo / ".muse" / "refs" / "heads" / "main").write_text("d" * 64)
180 r = _invoke(repo, "--json", "--fail-fast")
181 assert r.exit_code == 1
182 d = json.loads(r.output)
183 assert d["exit_code"] == 1
184 assert d["duration_ms"] > 0
185
186
187 # ---------------------------------------------------------------------------
188 # E — Error routing: JSON mode → stdout; text mode → stderr
189 # ---------------------------------------------------------------------------
190
191
192 class TestErrorRouting:
193 def test_io_error_json_to_stdout(self, tmp_path: pathlib.Path) -> None:
194 """OSError during run_verify → JSON error on stdout in JSON mode."""
195 repo = _init_repo(tmp_path)
196 _make_commit(repo, idx=0)
197 with mock.patch(
198 "muse.cli.commands.verify.run_verify",
199 side_effect=OSError("disk full"),
200 ):
201 r = _invoke(repo, "--json")
202 assert r.stderr.strip() == "", f"Expected empty stderr, got: {r.stderr!r}"
203 d = json.loads(r.output)
204 assert d["error"] == "io_error"
205 assert "disk full" in d["message"]
206 assert d["exit_code"] == 3
207 assert isinstance(d["duration_ms"], float)
208
209 def test_io_error_text_to_stderr(self, tmp_path: pathlib.Path) -> None:
210 """OSError during run_verify → error on stderr in text mode."""
211 repo = _init_repo(tmp_path)
212 _make_commit(repo, idx=0)
213 with mock.patch(
214 "muse.cli.commands.verify.run_verify",
215 side_effect=OSError("disk full"),
216 ):
217 r = _invoke(repo)
218 assert r.exit_code == 3
219 assert r.stderr.strip() != ""
220 assert "disk full" in r.stderr
221
222 def test_io_error_quiet_no_output(self, tmp_path: pathlib.Path) -> None:
223 """OSError in quiet mode → no output at all, exit 3."""
224 repo = _init_repo(tmp_path)
225 _make_commit(repo, idx=0)
226 with mock.patch(
227 "muse.cli.commands.verify.run_verify",
228 side_effect=OSError("disk full"),
229 ):
230 r = _invoke(repo, "--quiet")
231 assert r.exit_code == 3
232 assert r.output.strip() == ""
233 assert r.stderr.strip() == ""
234
235 def test_io_error_json_no_traceback(self, tmp_path: pathlib.Path) -> None:
236 """No Python traceback lands on stdout in JSON mode."""
237 repo = _init_repo(tmp_path)
238 _make_commit(repo, idx=0)
239 with mock.patch(
240 "muse.cli.commands.verify.run_verify",
241 side_effect=OSError("broken pipe"),
242 ):
243 r = _invoke(repo, "--json")
244 assert "Traceback" not in r.output
245 assert "Traceback" not in r.stderr
246
247 def test_io_error_json_schema(self, tmp_path: pathlib.Path) -> None:
248 """JSON error payload has exactly the documented keys."""
249 repo = _init_repo(tmp_path)
250 _make_commit(repo, idx=0)
251 with mock.patch(
252 "muse.cli.commands.verify.run_verify",
253 side_effect=OSError("nfs timeout"),
254 ):
255 r = _invoke(repo, "--json")
256 d = json.loads(r.output)
257 assert set(d) >= {"error", "message", "duration_ms", "exit_code"}
258
259
260 # ---------------------------------------------------------------------------
261 # S — Schema completeness
262 # ---------------------------------------------------------------------------
263
264 _SUCCESS_KEYS = {
265 "repo_id", "refs_checked", "commits_checked", "snapshots_checked",
266 "objects_checked", "signatures_checked", "all_ok", "nothing_checked",
267 "check_objects", "branch", "fail_fast", "duration_ms", "exit_code",
268 "failures",
269 }
270
271 _ERROR_KEYS = {"error", "message", "duration_ms", "exit_code"}
272
273
274 class TestSchemaCompleteness:
275 def test_all_success_keys_present_clean(self, tmp_path: pathlib.Path) -> None:
276 repo = _init_repo(tmp_path)
277 _make_commit(repo, idx=0)
278 d = json.loads(_invoke(repo, "--json").output)
279 assert _SUCCESS_KEYS <= set(d), f"Missing keys: {_SUCCESS_KEYS - set(d)}"
280
281 def test_all_success_keys_present_with_failures(self, tmp_path: pathlib.Path) -> None:
282 repo = _init_repo(tmp_path)
283 (repo / ".muse" / "refs" / "heads" / "main").write_text("e" * 64)
284 d = json.loads(_invoke(repo, "--json").output)
285 assert _SUCCESS_KEYS <= set(d), f"Missing keys: {_SUCCESS_KEYS - set(d)}"
286
287 def test_all_error_keys_present(self, tmp_path: pathlib.Path) -> None:
288 repo = _init_repo(tmp_path)
289 with mock.patch(
290 "muse.cli.commands.verify.run_verify", side_effect=OSError("fail")
291 ):
292 d = json.loads(_invoke(repo, "--json").output)
293 assert _ERROR_KEYS <= set(d), f"Missing keys: {_ERROR_KEYS - set(d)}"
294
295 def test_failures_list_schema(self, tmp_path: pathlib.Path) -> None:
296 repo = _init_repo(tmp_path)
297 # Missing commit: write a ref pointing to a valid-format but missing commit.
298 cid = long_id("f" * 64)
299 (repo / ".muse" / "refs" / "heads" / "main").write_text(cid)
300 d = json.loads(_invoke(repo, "--json").output)
301 assert len(d["failures"]) >= 1
302 for f in d["failures"]:
303 assert {"kind", "id", "error"} <= set(f)
304
305 def test_failures_kind_is_documented_literal(self, tmp_path: pathlib.Path) -> None:
306 repo = _init_repo(tmp_path)
307 cid = long_id("a" * 64)
308 (repo / ".muse" / "refs" / "heads" / "main").write_text(cid)
309 d = json.loads(_invoke(repo, "--json").output)
310 valid_kinds = {"ref", "commit", "snapshot", "object", "signature", "key_missing"}
311 for f in d["failures"]:
312 assert f["kind"] in valid_kinds, f"Unexpected kind: {f['kind']!r}"
313
314
315 # ---------------------------------------------------------------------------
316 # D — Data integrity
317 # ---------------------------------------------------------------------------
318
319
320 class TestDataIntegrity:
321 def test_exit_code_zero_iff_all_ok_true(self, tmp_path: pathlib.Path) -> None:
322 repo = _init_repo(tmp_path)
323 _make_commit(repo, idx=0)
324 d = json.loads(_invoke(repo, "--json").output)
325 assert (d["exit_code"] == 0) == (d["all_ok"] is True)
326
327 def test_exit_code_one_iff_all_ok_false(self, tmp_path: pathlib.Path) -> None:
328 repo = _init_repo(tmp_path)
329 cid = long_id("b" * 64)
330 (repo / ".muse" / "refs" / "heads" / "main").write_text(cid)
331 d = json.loads(_invoke(repo, "--json").output)
332 assert d["exit_code"] == 1
333 assert d["all_ok"] is False
334
335 def test_counters_non_negative(self, tmp_path: pathlib.Path) -> None:
336 repo = _init_repo(tmp_path)
337 _make_commit(repo, idx=0)
338 d = json.loads(_invoke(repo, "--json").output)
339 for key in ("refs_checked", "commits_checked", "snapshots_checked",
340 "objects_checked", "signatures_checked"):
341 assert d[key] >= 0, f"{key} is negative: {d[key]}"
342
343 def test_check_objects_reflected_true(self, tmp_path: pathlib.Path) -> None:
344 repo = _init_repo(tmp_path)
345 _make_commit(repo, idx=0)
346 d = json.loads(_invoke(repo, "--json").output)
347 assert d["check_objects"] is True
348
349 def test_check_objects_reflected_false(self, tmp_path: pathlib.Path) -> None:
350 repo = _init_repo(tmp_path)
351 _make_commit(repo, idx=0)
352 d = json.loads(_invoke(repo, "--json", "--no-objects").output)
353 assert d["check_objects"] is False
354
355 def test_branch_reflected_in_json(self, tmp_path: pathlib.Path) -> None:
356 repo = _init_repo(tmp_path)
357 _make_commit(repo, idx=0)
358 d = json.loads(_invoke(repo, "--json", "--branch", "main").output)
359 assert d["branch"] == "main"
360
361 def test_branch_none_when_not_specified(self, tmp_path: pathlib.Path) -> None:
362 repo = _init_repo(tmp_path)
363 _make_commit(repo, idx=0)
364 d = json.loads(_invoke(repo, "--json").output)
365 assert d["branch"] is None
366
367 def test_fail_fast_reflected_true(self, tmp_path: pathlib.Path) -> None:
368 repo = _init_repo(tmp_path)
369 _make_commit(repo, idx=0)
370 d = json.loads(_invoke(repo, "--json", "--fail-fast").output)
371 assert d["fail_fast"] is True
372
373 def test_fail_fast_reflected_false(self, tmp_path: pathlib.Path) -> None:
374 repo = _init_repo(tmp_path)
375 _make_commit(repo, idx=0)
376 d = json.loads(_invoke(repo, "--json").output)
377 assert d["fail_fast"] is False
378
379 def test_nothing_checked_false_when_commits_exist(self, tmp_path: pathlib.Path) -> None:
380 repo = _init_repo(tmp_path)
381 _make_commit(repo, idx=0)
382 d = json.loads(_invoke(repo, "--json").output)
383 assert d["nothing_checked"] is False
384
385 def test_nothing_checked_true_empty_repo(self, tmp_path: pathlib.Path) -> None:
386 repo = _init_repo(tmp_path)
387 d = json.loads(_invoke(repo, "--json").output)
388 assert d["nothing_checked"] is True
389
390 def test_duration_ms_is_float(self, tmp_path: pathlib.Path) -> None:
391 repo = _init_repo(tmp_path)
392 _make_commit(repo, idx=0)
393 d = json.loads(_invoke(repo, "--json").output)
394 assert isinstance(d["duration_ms"], float)
395
396 def test_blob_id_unique_per_content(self, tmp_path: pathlib.Path) -> None:
397 """blob_id produces distinct IDs for distinct byte sequences."""
398 ids = {blob_id(b"content-a"), blob_id(b"content-b"), blob_id(b"content-c")}
399 assert len(ids) == 3
400
401 def test_long_id_round_trips(self) -> None:
402 """long_id strips sha256: prefix correctly for comparison."""
403 hex_val = "a" * 64
404 full = long_id(hex_val)
405 assert full == "sha256:" + hex_val
406 assert full == f"sha256:{hex_val}"
407
408 def test_short_id_abbreviates(self) -> None:
409 full = long_id("f" * 64)
410 s = short_id(full)
411 assert s.startswith("sha256:")
412 assert len(s) < len(full)
413
414
415 # ---------------------------------------------------------------------------
416 # IO — OSError handling
417 # ---------------------------------------------------------------------------
418
419
420 class TestIOErrorHandling:
421 def test_exit_code_3_on_io_error_json(self, tmp_path: pathlib.Path) -> None:
422 repo = _init_repo(tmp_path)
423 with mock.patch(
424 "muse.cli.commands.verify.run_verify", side_effect=OSError("io fail")
425 ):
426 r = _invoke(repo, "--json")
427 assert r.exit_code == 3
428 d = json.loads(r.output)
429 assert d["exit_code"] == 3
430
431 def test_exit_code_3_on_io_error_text(self, tmp_path: pathlib.Path) -> None:
432 repo = _init_repo(tmp_path)
433 with mock.patch(
434 "muse.cli.commands.verify.run_verify", side_effect=OSError("io fail")
435 ):
436 r = _invoke(repo)
437 assert r.exit_code == 3
438
439 def test_io_error_json_no_stderr(self, tmp_path: pathlib.Path) -> None:
440 repo = _init_repo(tmp_path)
441 with mock.patch(
442 "muse.cli.commands.verify.run_verify", side_effect=OSError("io fail")
443 ):
444 r = _invoke(repo, "--json")
445 assert r.stderr.strip() == ""
446
447 def test_io_error_text_has_stderr(self, tmp_path: pathlib.Path) -> None:
448 repo = _init_repo(tmp_path)
449 with mock.patch(
450 "muse.cli.commands.verify.run_verify", side_effect=OSError("io fail")
451 ):
452 r = _invoke(repo)
453 assert r.stderr.strip() != ""
454
455
456 # ---------------------------------------------------------------------------
457 # P — Performance
458 # ---------------------------------------------------------------------------
459
460
461 class TestPerformance:
462 def test_duration_ms_positive(self, tmp_path: pathlib.Path) -> None:
463 repo = _init_repo(tmp_path)
464 _make_commit(repo, idx=0)
465 d = json.loads(_invoke(repo, "--json").output)
466 assert d["duration_ms"] > 0
467
468 def test_50_commit_chain_under_5000ms(self, tmp_path: pathlib.Path) -> None:
469 repo = _init_repo(tmp_path)
470 prev: str | None = None
471 for i in range(50):
472 prev = _make_commit(repo, parent_id=prev, idx=i)
473 d = json.loads(_invoke(repo, "--json").output)
474 assert d["duration_ms"] < 5_000, f"Too slow: {d['duration_ms']} ms"
475 assert d["all_ok"] is True
476
477 def test_50_commit_chain_no_objects_faster(self, tmp_path: pathlib.Path) -> None:
478 repo = _init_repo(tmp_path)
479 prev: str | None = None
480 for i in range(50):
481 prev = _make_commit(repo, parent_id=prev, idx=i)
482 full = json.loads(_invoke(repo, "--json").output)["duration_ms"]
483 fast = json.loads(_invoke(repo, "--json", "--no-objects").output)["duration_ms"]
484 # --no-objects should generally be faster; we allow some timing noise
485 # but cap both under 10 s to prevent runaway
486 assert fast < 10_000
487 assert full < 10_000
488
489
490 # ---------------------------------------------------------------------------
491 # Sec — Security
492 # ---------------------------------------------------------------------------
493
494
495 class TestSecurity:
496 def test_no_traceback_on_json_io_error(self, tmp_path: pathlib.Path) -> None:
497 repo = _init_repo(tmp_path)
498 with mock.patch(
499 "muse.cli.commands.verify.run_verify", side_effect=OSError("fail")
500 ):
501 r = _invoke(repo, "--json")
502 assert "Traceback" not in r.output
503 assert "Traceback" not in r.stderr
504
505 def test_no_traceback_on_text_io_error(self, tmp_path: pathlib.Path) -> None:
506 repo = _init_repo(tmp_path)
507 with mock.patch(
508 "muse.cli.commands.verify.run_verify", side_effect=OSError("fail")
509 ):
510 r = _invoke(repo)
511 assert "Traceback" not in r.output
512 assert "Traceback" not in r.stderr
513
514 def test_no_raw_exception_in_stdout(self, tmp_path: pathlib.Path) -> None:
515 repo = _init_repo(tmp_path)
516 with mock.patch(
517 "muse.cli.commands.verify.run_verify", side_effect=OSError("secret path")
518 ):
519 r = _invoke(repo, "--json")
520 # The exception message may appear in the JSON "message" field — that's
521 # intentional. What we check is that no raw Python exception string
522 # (e.g. "OSError:") leaks outside the JSON structure.
523 assert "OSError:" not in r.output
524 assert "OSError:" not in r.stderr
525
526
527 # ---------------------------------------------------------------------------
528 # C — Concurrent readers
529 # ---------------------------------------------------------------------------
530
531
532 class TestConcurrent:
533 def test_10_concurrent_reads_all_valid_json(self, tmp_path: pathlib.Path) -> None:
534 repo = _init_repo(tmp_path)
535 prev: str | None = None
536 for i in range(10):
537 prev = _make_commit(repo, parent_id=prev, idx=i)
538
539 results: list[dict] = []
540 errors: list[Exception] = []
541 lock = threading.Lock()
542
543 def _read() -> None:
544 try:
545 r = _invoke(repo, "--json")
546 d = json.loads(r.output)
547 with lock:
548 results.append(d)
549 except Exception as exc:
550 with lock:
551 errors.append(exc)
552
553 threads = [threading.Thread(target=_read) for _ in range(10)]
554 for t in threads:
555 t.start()
556 for t in threads:
557 t.join()
558
559 assert errors == [], f"Thread errors: {errors}"
560 assert len(results) == 10
561 for d in results:
562 assert d["all_ok"] is True
563 assert d["exit_code"] == 0
564 assert isinstance(d["duration_ms"], float)
File History 2 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 138 days ago