gabriel / muse public
test_cmd_verify_hardening.py python
867 lines 28.9 KB
Raw
sha256:2eaa5d95f9d9383498e76947410a26e5a3ba23d182f339910c424cf88fad412b fix: try fetch/presign before fetch/mpack to avoid Cloudfla… Sonnet 4.6 patch 7 days ago
1 """Hardening tests for ``muse verify`` — security, performance, agent UX.
2
3 Covers:
4 Unit (core):
5 - _branch_refs symlink guard (symlinks silently skipped)
6 - _branch_refs size cap (oversized ref file treated as invalid)
7 - _branch_refs branch filter (only named branch returned)
8 - _MAX_COMMITS >= guard (walk stops at budget, not budget+1)
9 - _make_result helper produces correct all_ok
10 - missing-key reported as kind="key_missing" not kind="signature"
11 - fail_fast stops after first failure
12
13 Security:
14 - Symlink inside .muse/refs/heads/ is silently skipped
15 - Oversized ref file content capped — no memory explosion
16 - Invalid ref (bad hex) reported as kind="ref" not passed to BFS
17 - kind column in text output passes through sanitize_display
18
19 Error routing:
20 - I/O error during run_verify goes to stderr
21 - Failures exit with code 1
22
23 JSON schema:
24 - All _VerifyJson fields present: repo_id, branch, fail_fast, check_objects
25 - failures[].kind is one of the documented literals
26 - all_ok=True when failures=[]
27 - --branch reflected in JSON output
28 - --no-objects reflected as check_objects=false in JSON
29
30 New flags:
31 - --branch limits walk to one branch
32 - --fail-fast stops after first failure in text and json mode
33 - --json flag replaces --format json (old flag rejected)
34 - --no-objects flag sets check_objects=False in JSON
35
36 Integration:
37 - Two-branch repo: --branch verifies one, other not touched
38 - Healthy chain + corrupt branch: fail-fast returns exactly one failure
39 - Missing snapshot reported correctly
40 - Multiple failures all listed in JSON
41
42 E2E:
43 - --help shows --json, --branch, --fail-fast flags
44 - Help mentions key_missing in description
45
46 Stress:
47 - 500-commit chain passes with check_objects=True
48 - 500-commit chain with --fail-fast on first corrupt object
49 - Concurrent reads of the same repo (10 threads)
50 """
51
52 from __future__ import annotations
53
54 import datetime
55 import json
56 import pathlib
57 import threading
58
59 import pytest
60 from tests.cli_test_helper import CliRunner, InvokeResult
61
62 from muse.core.object_store import object_path, write_object
63 from muse.core.ids import hash_commit, hash_snapshot
64 from muse.core.commits import (
65 CommitRecord,
66 write_commit,
67 )
68 from muse.core.snapshots import (
69 SnapshotRecord,
70 write_snapshot,
71 )
72
73 from muse.core.types import Manifest, NULL_LONG_ID, blob_id, long_id, fake_id
74 from muse.core.verify import (
75 VerifyFailure,
76 VerifyResult,
77 _MAX_COMMITS,
78 _branch_refs,
79 _make_result,
80 run_verify,
81 )
82 from muse.core.paths import heads_dir, muse_dir, ref_path
83
84 runner = CliRunner()
85 cli = None # argparse migration — CliRunner ignores this arg
86
87 _REPO_ID = "verify-hardening-test"
88
89
90 # ---------------------------------------------------------------------------
91 # TypedDicts for parsing JSON output
92 # ---------------------------------------------------------------------------
93
94
95 from typing import TypedDict
96
97
98 class _FailureOut(TypedDict):
99 kind: str
100 id: str
101 error: str
102
103
104 class _VerifyOut(TypedDict):
105 repo_id: str
106 refs_checked: int
107 commits_checked: int
108 snapshots_checked: int
109 objects_checked: int
110 signatures_checked: int
111 all_ok: bool
112 check_objects: bool
113 branch: str | None
114 fail_fast: bool
115 failures: list[_FailureOut]
116
117
118 # ---------------------------------------------------------------------------
119 # Helpers
120 # ---------------------------------------------------------------------------
121
122
123
124
125 def _init_repo(path: pathlib.Path) -> pathlib.Path:
126 muse = muse_dir(path)
127 for d in ("commits", "snapshots", "objects", "refs/heads", "keys"):
128 (muse / d).mkdir(parents=True, exist_ok=True)
129 (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
130 (muse / "repo.json").write_text(
131 json.dumps({"repo_id": _REPO_ID, "domain": "midi"}), encoding="utf-8"
132 )
133 return path
134
135
136 def _env(repo: pathlib.Path) -> Manifest:
137 return {"MUSE_REPO_ROOT": str(repo)}
138
139
140 def _make_commit(
141 root: pathlib.Path,
142 parent_id: str | None = None,
143 content: bytes = b"data",
144 branch: str = "main",
145 idx: int = 0,
146 ) -> str:
147 raw = content + str(idx).encode()
148 obj_id = blob_id(raw)
149 write_object(root, obj_id, raw)
150 manifest = {f"file_{idx}.txt": obj_id}
151 snap_id = hash_snapshot(manifest)
152 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
153 committed_at = (
154 datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
155 + datetime.timedelta(hours=idx)
156 )
157 parent_ids = [parent_id] if parent_id else []
158 commit_id = hash_commit(
159 parent_ids=parent_ids,
160 snapshot_id=snap_id,
161 message=f"commit {idx}",
162 committed_at_iso=committed_at.isoformat(),
163 )
164 write_commit(
165 root,
166 CommitRecord(
167 commit_id=commit_id,
168 branch=branch,
169 snapshot_id=snap_id,
170 message=f"commit {idx}",
171 committed_at=committed_at,
172 parent_commit_id=parent_id,
173 ),
174 )
175 # Branch names with '/' require subdirectory creation.
176 branch_ref = ref_path(root, branch)
177 branch_ref.parent.mkdir(parents=True, exist_ok=True)
178 branch_ref.write_text(commit_id, encoding="utf-8")
179 return commit_id
180
181
182 _invoke_lock = threading.Lock()
183
184
185 def _invoke(args: list[str], env: Manifest) -> InvokeResult:
186 with _invoke_lock:
187 return runner.invoke(cli, args, env=env)
188
189
190 def _parse_json(result: InvokeResult) -> _VerifyOut:
191 raw: _VerifyOut = json.loads(result.output)
192 return raw
193
194
195 # ---------------------------------------------------------------------------
196 # Unit: _branch_refs
197 # ---------------------------------------------------------------------------
198
199
200 def test_branch_refs_skips_symlink(tmp_path: pathlib.Path) -> None:
201 _init_repo(tmp_path)
202 heads = heads_dir(tmp_path)
203 real_file = heads / "main"
204 real_file.write_text("a" * 64, encoding="utf-8")
205 link = heads / "malicious"
206 link.symlink_to(real_file)
207 refs = _branch_refs(tmp_path)
208 branch_names = [br for br, _ in refs]
209 assert "malicious" not in branch_names
210
211
212 def test_branch_refs_size_cap(tmp_path: pathlib.Path) -> None:
213 """Oversized ref file is capped; the truncated content fails hex validation
214 in run_verify and is reported as kind='ref' — not read entirely into memory."""
215 _init_repo(tmp_path)
216 heads = heads_dir(tmp_path)
217 # Write 10 MB into a ref file — _branch_refs reads at most 72 bytes.
218 (heads / "main").write_bytes(b"x" * (10 * 1024 * 1024))
219 refs = _branch_refs(tmp_path)
220 # The truncated content (72 'x' chars) is returned but is not a valid ID.
221 # _branch_refs does not validate format — run_verify does.
222 assert len(refs) == 1
223 branch_name, commit_id = refs[0]
224 assert branch_name == "main"
225 # Neither bare hex (64 chars) nor sha256-prefixed (71 chars) — it's garbage.
226 assert commit_id not in (long_id("x" * 64),) and not commit_id.startswith("sha256:")
227 # run_verify must report this as a ref failure.
228 result = run_verify(tmp_path)
229 assert result["all_ok"] is False
230 kinds = [f["kind"] for f in result["failures"]]
231 assert "ref" in kinds
232
233
234 def test_branch_refs_branch_filter_returns_one(tmp_path: pathlib.Path) -> None:
235 _init_repo(tmp_path)
236 _make_commit(tmp_path, content=b"main", branch="main", idx=0)
237 _make_commit(tmp_path, content=b"dev", branch="dev", idx=1)
238 refs = _branch_refs(tmp_path, branch="main")
239 assert len(refs) == 1
240 assert refs[0][0] == "main"
241
242
243 def test_branch_refs_branch_filter_missing_branch(tmp_path: pathlib.Path) -> None:
244 _init_repo(tmp_path)
245 refs = _branch_refs(tmp_path, branch="nonexistent")
246 assert refs == []
247
248
249 def test_branch_refs_branch_filter_skips_symlink(tmp_path: pathlib.Path) -> None:
250 _init_repo(tmp_path)
251 heads = heads_dir(tmp_path)
252 real = heads / "main"
253 real.write_text("a" * 64, encoding="utf-8")
254 link = heads / "malicious"
255 link.symlink_to(real)
256 refs = _branch_refs(tmp_path, branch="malicious")
257 assert refs == []
258
259
260 # ---------------------------------------------------------------------------
261 # Unit: _make_result helper
262 # ---------------------------------------------------------------------------
263
264
265 def test_make_result_all_ok_true_when_no_failures() -> None:
266 result = _make_result(1, 1, 1, 1, 0, [])
267 assert result["all_ok"] is True
268 assert result["failures"] == []
269
270
271 def test_make_result_all_ok_false_when_failures() -> None:
272 failure = VerifyFailure(kind="commit", id="abc", error="missing")
273 result = _make_result(1, 0, 0, 0, 0, [failure])
274 assert result["all_ok"] is False
275 assert len(result["failures"]) == 1
276
277
278 # ---------------------------------------------------------------------------
279 # Unit: _MAX_COMMITS guard — >= not >
280 # ---------------------------------------------------------------------------
281
282
283 def test_max_commits_guard_stops_at_budget(tmp_path: pathlib.Path) -> None:
284 """Walk should stop at _MAX_COMMITS, not _MAX_COMMITS+1."""
285 _init_repo(tmp_path)
286 # Create 3 chained commits and monkey-patch _MAX_COMMITS to 2.
287 import muse.core.verify as verify_mod
288
289 orig = verify_mod._MAX_COMMITS
290 try:
291 verify_mod._MAX_COMMITS = 2
292 prev: str | None = None
293 for i in range(5):
294 prev = _make_commit(tmp_path, parent_id=prev, idx=i)
295 result = run_verify(tmp_path)
296 # Walk stopped early — commits_checked <= 2.
297 assert result["commits_checked"] <= 2
298 finally:
299 verify_mod._MAX_COMMITS = orig
300
301
302 # ---------------------------------------------------------------------------
303 # Unit: missing key → kind="key_missing", not kind="signature"
304 # ---------------------------------------------------------------------------
305
306
307 def test_missing_key_reported_as_key_missing(tmp_path: pathlib.Path) -> None:
308 """A signed commit whose key file is absent → kind='key_missing'."""
309 _init_repo(tmp_path)
310 # Write a commit with a non-empty signature and agent_id but no key file.
311 content = b"signed commit"
312 obj_id = blob_id(content)
313 write_object(tmp_path, obj_id, content)
314 manifest = {"signed.txt": obj_id}
315 snap_id = hash_snapshot(manifest)
316 write_snapshot(tmp_path, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
317 committed_at = datetime.datetime(2026, 3, 1, tzinfo=datetime.timezone.utc)
318 commit_id = hash_commit(
319 parent_ids=[],
320 snapshot_id=snap_id,
321 message="signed",
322 committed_at_iso=committed_at.isoformat(),
323 )
324 write_commit(
325 tmp_path,
326 CommitRecord(
327 commit_id=commit_id,
328 branch="main",
329 snapshot_id=snap_id,
330 message="signed",
331 committed_at=committed_at,
332 signature="ed25519:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
333 agent_id="agent-42",
334 ),
335 )
336 (heads_dir(tmp_path) / "main").write_text(
337 commit_id, encoding="utf-8"
338 )
339 result = run_verify(tmp_path)
340 kinds = [f["kind"] for f in result["failures"]]
341 assert "key_missing" in kinds
342 assert "signature" not in kinds
343
344
345 # ---------------------------------------------------------------------------
346 # Unit: fail_fast stops after first failure
347 # ---------------------------------------------------------------------------
348
349
350 def test_fail_fast_stops_after_first_failure(tmp_path: pathlib.Path) -> None:
351 _init_repo(tmp_path)
352 # Point main at a nonexistent commit — first failure.
353 fake = "a" * 64
354 (heads_dir(tmp_path) / "main").write_text(
355 fake, encoding="utf-8"
356 )
357 # Point dev at another nonexistent commit — potential second failure.
358 fake2 = "b" * 64
359 (heads_dir(tmp_path) / "dev").write_text(
360 fake2, encoding="utf-8"
361 )
362 result = run_verify(tmp_path, fail_fast=True)
363 # fail_fast: should stop after first failure, not accumulate both.
364 assert not result["all_ok"]
365 assert len(result["failures"]) == 1
366
367
368 def test_fail_fast_still_ok_when_healthy(tmp_path: pathlib.Path) -> None:
369 _init_repo(tmp_path)
370 _make_commit(tmp_path, content=b"all good", idx=0)
371 result = run_verify(tmp_path, fail_fast=True)
372 assert result["all_ok"] is True
373 assert result["failures"] == []
374
375
376 # ---------------------------------------------------------------------------
377 # Security: ANSI in branch name sanitized in text output
378 # ---------------------------------------------------------------------------
379
380
381 def test_ansi_in_branch_name_sanitized_in_text(tmp_path: pathlib.Path) -> None:
382 _init_repo(tmp_path)
383 # Write a bad ref (invalid ID) for a "branch" with ANSI escape in name.
384 heads = heads_dir(tmp_path)
385 malicious_name = "\x1b[31mmalicious\x1b[0m"
386 (heads / malicious_name).write_text("not-valid-hex-id", encoding="utf-8")
387 result = _invoke(["verify"], env=_env(tmp_path))
388 assert "\x1b[" not in result.output
389
390
391 def test_ansi_in_error_sanitized_in_text(tmp_path: pathlib.Path) -> None:
392 _init_repo(tmp_path)
393 # Point main at bad ref — the error text is sanitized.
394 (heads_dir(tmp_path) / "main").write_text(
395 "not-a-valid-commit-id", encoding="utf-8"
396 )
397 result = _invoke(["verify"], env=_env(tmp_path))
398 assert "\x1b[" not in result.output
399
400
401 # ---------------------------------------------------------------------------
402 # Error routing
403 # ---------------------------------------------------------------------------
404
405
406 def test_failure_exits_with_nonzero(tmp_path: pathlib.Path) -> None:
407 _init_repo(tmp_path)
408 (heads_dir(tmp_path) / "main").write_text(
409 "b" * 64, encoding="utf-8"
410 )
411 result = _invoke(["verify"], env=_env(tmp_path))
412 assert result.exit_code != 0
413
414
415 def test_quiet_mode_no_stdout(tmp_path: pathlib.Path) -> None:
416 _init_repo(tmp_path)
417 _make_commit(tmp_path, content=b"quiet", idx=0)
418 result = _invoke(["verify", "--quiet"], env=_env(tmp_path))
419 assert result.exit_code == 0
420 assert result.output.strip() == ""
421
422
423 def test_quiet_mode_fails_silently(tmp_path: pathlib.Path) -> None:
424 _init_repo(tmp_path)
425 (heads_dir(tmp_path) / "main").write_text(
426 "c" * 64, encoding="utf-8"
427 )
428 result = _invoke(["verify", "--quiet"], env=_env(tmp_path))
429 assert result.exit_code != 0
430 assert result.output.strip() == ""
431
432
433 # ---------------------------------------------------------------------------
434 # JSON schema
435 # ---------------------------------------------------------------------------
436
437
438 def test_json_all_fields_present(tmp_path: pathlib.Path) -> None:
439 _init_repo(tmp_path)
440 _make_commit(tmp_path, content=b"schema", idx=0)
441 result = _invoke(["verify", "--json"], env=_env(tmp_path))
442 assert result.exit_code == 0
443 data = _parse_json(result)
444 assert data["repo_id"] == _REPO_ID
445 assert data["all_ok"] is True
446 assert data["failures"] == []
447 assert isinstance(data["refs_checked"], int)
448 assert isinstance(data["commits_checked"], int)
449 assert isinstance(data["snapshots_checked"], int)
450 assert isinstance(data["objects_checked"], int)
451 assert isinstance(data["signatures_checked"], int)
452 assert isinstance(data["check_objects"], bool)
453 assert data["branch"] is None
454 assert data["fail_fast"] is False
455
456
457 def test_json_no_objects_reflected(tmp_path: pathlib.Path) -> None:
458 _init_repo(tmp_path)
459 _make_commit(tmp_path, content=b"no-obj", idx=0)
460 result = _invoke(["verify", "--json", "--no-objects"], env=_env(tmp_path))
461 assert result.exit_code == 0
462 data = _parse_json(result)
463 assert data["check_objects"] is False
464
465
466 def test_json_branch_reflected(tmp_path: pathlib.Path) -> None:
467 _init_repo(tmp_path)
468 _make_commit(tmp_path, content=b"branch-json", branch="feat/x", idx=0)
469 result = _invoke(["verify", "--json", "--branch", "feat/x"], env=_env(tmp_path))
470 assert result.exit_code == 0
471 data = _parse_json(result)
472 assert data["branch"] == "feat/x"
473
474
475 def test_json_fail_fast_reflected(tmp_path: pathlib.Path) -> None:
476 _init_repo(tmp_path)
477 _make_commit(tmp_path, content=b"fail-fast-json", idx=0)
478 result = _invoke(["verify", "--json", "--fail-fast"], env=_env(tmp_path))
479 assert result.exit_code == 0
480 data = _parse_json(result)
481 assert data["fail_fast"] is True
482
483
484 def test_json_failures_have_kind_id_error(tmp_path: pathlib.Path) -> None:
485 _init_repo(tmp_path)
486 (heads_dir(tmp_path) / "main").write_text(
487 "d" * 64, encoding="utf-8"
488 )
489 result = _invoke(["verify", "--json"], env=_env(tmp_path))
490 assert result.exit_code != 0
491 data = _parse_json(result)
492 assert data["all_ok"] is False
493 for failure in data["failures"]:
494 assert "kind" in failure
495 assert "id" in failure
496 assert "error" in failure
497
498
499 def test_json_failure_kind_is_valid_literal(tmp_path: pathlib.Path) -> None:
500 _init_repo(tmp_path)
501 (heads_dir(tmp_path) / "main").write_text(
502 "e" * 64, encoding="utf-8"
503 )
504 result = _invoke(["verify", "--json"], env=_env(tmp_path))
505 data = _parse_json(result)
506 valid_kinds = {"ref", "commit", "snapshot", "object", "signature", "key_missing"}
507 for failure in data["failures"]:
508 assert failure["kind"] in valid_kinds
509
510
511 def test_json_missing_snapshot_kind(tmp_path: pathlib.Path) -> None:
512 """A commit pointing at a nonexistent snapshot shows kind='snapshot'."""
513 _init_repo(tmp_path)
514 snap_id = long_id("f" * 64)
515 committed_at = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
516 commit_id = hash_commit(
517 parent_ids=[],
518 snapshot_id=snap_id,
519 message="no snap",
520 committed_at_iso=committed_at.isoformat(),
521 )
522 write_commit(
523 tmp_path,
524 CommitRecord(
525 commit_id=commit_id,
526 branch="main",
527 snapshot_id=snap_id,
528 message="no snap",
529 committed_at=committed_at,
530 ),
531 )
532 (heads_dir(tmp_path) / "main").write_text(
533 commit_id, encoding="utf-8"
534 )
535 result = _invoke(["verify", "--json"], env=_env(tmp_path))
536 data = _parse_json(result)
537 kinds = [f["kind"] for f in data["failures"]]
538 assert "snapshot" in kinds
539
540
541 def test_json_missing_object_kind(tmp_path: pathlib.Path) -> None:
542 """A manifest referencing a nonexistent object shows kind='object'."""
543 _init_repo(tmp_path)
544 obj_id = NULL_LONG_ID
545 manifest = {"ghost.txt": obj_id}
546 snap_id = hash_snapshot(manifest)
547 write_snapshot(tmp_path, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
548 committed_at = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
549 commit_id = hash_commit(
550 parent_ids=[],
551 snapshot_id=snap_id,
552 message="ghost",
553 committed_at_iso=committed_at.isoformat(),
554 )
555 write_commit(
556 tmp_path,
557 CommitRecord(
558 commit_id=commit_id,
559 branch="main",
560 snapshot_id=snap_id,
561 message="ghost",
562 committed_at=committed_at,
563 ),
564 )
565 (heads_dir(tmp_path) / "main").write_text(
566 commit_id, encoding="utf-8"
567 )
568 result = _invoke(["verify", "--json"], env=_env(tmp_path))
569 data = _parse_json(result)
570 kinds = [f["kind"] for f in data["failures"]]
571 assert "object" in kinds
572
573
574 # ---------------------------------------------------------------------------
575 # New flags: --branch
576 # ---------------------------------------------------------------------------
577
578
579 def test_branch_flag_limits_to_named_branch(tmp_path: pathlib.Path) -> None:
580 _init_repo(tmp_path)
581 _make_commit(tmp_path, content=b"main ok", branch="main", idx=0)
582 # dev points at a nonexistent commit — if not limited, would fail.
583 (heads_dir(tmp_path) / "dev").write_text(
584 "1" * 64, encoding="utf-8"
585 )
586 result = _invoke(["verify", "--branch", "main"], env=_env(tmp_path))
587 assert result.exit_code == 0
588
589
590 def test_branch_flag_catches_failure_in_named_branch(tmp_path: pathlib.Path) -> None:
591 _init_repo(tmp_path)
592 (heads_dir(tmp_path) / "main").write_text(
593 "2" * 64, encoding="utf-8"
594 )
595 result = _invoke(["verify", "--branch", "main"], env=_env(tmp_path))
596 assert result.exit_code != 0
597
598
599 def test_branch_flag_missing_branch_is_clean(tmp_path: pathlib.Path) -> None:
600 _init_repo(tmp_path)
601 result = _invoke(["verify", "--branch", "nonexistent"], env=_env(tmp_path))
602 assert result.exit_code == 0
603
604
605 def test_branch_flag_json_branch_field(tmp_path: pathlib.Path) -> None:
606 _init_repo(tmp_path)
607 _make_commit(tmp_path, content=b"b json", branch="main", idx=0)
608 result = _invoke(["verify", "--json", "--branch", "main"], env=_env(tmp_path))
609 data = _parse_json(result)
610 assert data["branch"] == "main"
611 assert data["all_ok"] is True
612
613
614 def test_branch_flag_shown_in_text_output(tmp_path: pathlib.Path) -> None:
615 _init_repo(tmp_path)
616 _make_commit(tmp_path, content=b"text branch", branch="feat/my", idx=0)
617 result = _invoke(["verify", "--branch", "feat/my"], env=_env(tmp_path))
618 assert result.exit_code == 0
619 assert "feat/my" in result.output
620
621
622 # ---------------------------------------------------------------------------
623 # New flags: --fail-fast
624 # ---------------------------------------------------------------------------
625
626
627 def test_fail_fast_cli_stops_early(tmp_path: pathlib.Path) -> None:
628 _init_repo(tmp_path)
629 (heads_dir(tmp_path) / "main").write_text(
630 "3" * 64, encoding="utf-8"
631 )
632 (heads_dir(tmp_path) / "dev").write_text(
633 "4" * 64, encoding="utf-8"
634 )
635 result = _invoke(["verify", "--json", "--fail-fast"], env=_env(tmp_path))
636 assert result.exit_code != 0
637 data = _parse_json(result)
638 assert len(data["failures"]) == 1
639 assert data["fail_fast"] is True
640
641
642 def test_fail_fast_no_effect_on_healthy_repo(tmp_path: pathlib.Path) -> None:
643 _init_repo(tmp_path)
644 _make_commit(tmp_path, content=b"healthy ff", idx=0)
645 result = _invoke(["verify", "--fail-fast"], env=_env(tmp_path))
646 assert result.exit_code == 0
647
648
649 # ---------------------------------------------------------------------------
650 # New flags: --json replaces --format json
651 # ---------------------------------------------------------------------------
652
653
654 def test_json_flag_works(tmp_path: pathlib.Path) -> None:
655 _init_repo(tmp_path)
656 _make_commit(tmp_path, content=b"json flag", idx=0)
657 result = _invoke(["verify", "--json"], env=_env(tmp_path))
658 assert result.exit_code == 0
659 data = _parse_json(result)
660 assert "all_ok" in data
661
662
663 def test_format_flag_rejected(tmp_path: pathlib.Path) -> None:
664 _init_repo(tmp_path)
665 _make_commit(tmp_path, content=b"fmt flag", idx=0)
666 result = _invoke(["verify", "--format", "json"], env=_env(tmp_path))
667 # --format is no longer a valid flag — argparse will reject it.
668 assert result.exit_code != 0
669
670
671 # ---------------------------------------------------------------------------
672 # Integration
673 # ---------------------------------------------------------------------------
674
675
676 def test_two_branch_repo_healthy(tmp_path: pathlib.Path) -> None:
677 _init_repo(tmp_path)
678 _make_commit(tmp_path, content=b"main", branch="main", idx=0)
679 _make_commit(tmp_path, content=b"dev", branch="dev", idx=1)
680 result = run_verify(tmp_path)
681 assert result["all_ok"] is True
682 assert result["refs_checked"] == 2
683
684
685 def test_two_branch_one_broken_full_check(tmp_path: pathlib.Path) -> None:
686 _init_repo(tmp_path)
687 _make_commit(tmp_path, content=b"main ok", branch="main", idx=0)
688 (heads_dir(tmp_path) / "dev").write_text(
689 fake_id("nonexistent-dev-commit"), encoding="utf-8"
690 )
691 result = run_verify(tmp_path)
692 assert result["all_ok"] is False
693 kinds = {f["kind"] for f in result["failures"]}
694 assert "commit" in kinds
695
696
697 def test_two_branch_one_broken_with_branch_filter(tmp_path: pathlib.Path) -> None:
698 _init_repo(tmp_path)
699 _make_commit(tmp_path, content=b"main ok", branch="main", idx=0)
700 (heads_dir(tmp_path) / "dev").write_text(
701 fake_id("nonexistent-dev-6"), encoding="utf-8"
702 )
703 # Limiting to main only — should pass.
704 result = run_verify(tmp_path, branch="main")
705 assert result["all_ok"] is True
706
707
708 def test_corrupt_object_and_fail_fast(tmp_path: pathlib.Path) -> None:
709 """Corrupt the object, run with fail_fast — exactly one failure returned."""
710 import os
711
712 _init_repo(tmp_path)
713 content = b"will be corrupted"
714 obj_id = blob_id(content)
715 write_object(tmp_path, obj_id, content)
716 manifest = {"c.txt": obj_id}
717 snap_id = hash_snapshot(manifest)
718 write_snapshot(tmp_path, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
719 committed_at = datetime.datetime(2026, 3, 5, tzinfo=datetime.timezone.utc)
720 commit_id = hash_commit(
721 parent_ids=[],
722 snapshot_id=snap_id,
723 message="corrupt",
724 committed_at_iso=committed_at.isoformat(),
725 )
726 write_commit(
727 tmp_path,
728 CommitRecord(
729 commit_id=commit_id,
730 branch="main",
731 snapshot_id=snap_id,
732 message="corrupt",
733 committed_at=committed_at,
734 ),
735 )
736 (heads_dir(tmp_path) / "main").write_text(
737 commit_id, encoding="utf-8"
738 )
739 obj_file = object_path(tmp_path, obj_id)
740 os.chmod(obj_file, 0o644)
741 obj_file.write_bytes(b"bad data!")
742 result = run_verify(tmp_path, check_objects=True, fail_fast=True)
743 assert result["all_ok"] is False
744 assert len(result["failures"]) == 1
745 assert result["failures"][0]["kind"] == "object"
746
747
748 def test_multiple_failures_all_listed(tmp_path: pathlib.Path) -> None:
749 _init_repo(tmp_path)
750 for i in range(5):
751 ref_path(tmp_path, f"br{i}").write_text(
752 chr(ord("a") + i) * 64, encoding="utf-8"
753 )
754 result = run_verify(tmp_path)
755 assert result["all_ok"] is False
756 assert len(result["failures"]) >= 5
757
758
759 # ---------------------------------------------------------------------------
760 # E2E: help output
761 # ---------------------------------------------------------------------------
762
763
764 def test_help_shows_json_flag() -> None:
765 result = runner.invoke(cli, ["verify", "--help"])
766 assert result.exit_code == 0
767 assert "--json" in result.output
768
769
770 def test_help_shows_branch_flag() -> None:
771 result = runner.invoke(cli, ["verify", "--help"])
772 assert result.exit_code == 0
773 assert "--branch" in result.output or "-b" in result.output
774
775
776 def test_help_shows_fail_fast_flag() -> None:
777 result = runner.invoke(cli, ["verify", "--help"])
778 assert result.exit_code == 0
779 assert "--fail-fast" in result.output
780
781
782 def test_help_mentions_key_missing() -> None:
783 result = runner.invoke(cli, ["verify", "--help"])
784 assert result.exit_code == 0
785 assert "key_missing" in result.output or "key" in result.output
786
787
788 # ---------------------------------------------------------------------------
789 # Stress
790 # ---------------------------------------------------------------------------
791
792
793 def test_stress_500_commit_chain(tmp_path: pathlib.Path) -> None:
794 _init_repo(tmp_path)
795 prev: str | None = None
796 for i in range(500):
797 prev = _make_commit(tmp_path, parent_id=prev, content=b"chain", idx=i)
798 result = run_verify(tmp_path, check_objects=True)
799 assert result["all_ok"] is True
800 assert result["commits_checked"] == 500
801
802
803 def test_stress_500_commit_no_objects(tmp_path: pathlib.Path) -> None:
804 _init_repo(tmp_path)
805 prev: str | None = None
806 for i in range(500):
807 prev = _make_commit(tmp_path, parent_id=prev, content=b"fast", idx=i)
808 result = run_verify(tmp_path, check_objects=False)
809 assert result["all_ok"] is True
810 assert result["commits_checked"] == 500
811
812
813 def test_stress_concurrent_reads(tmp_path: pathlib.Path) -> None:
814 _init_repo(tmp_path)
815 prev: str | None = None
816 for i in range(20):
817 prev = _make_commit(tmp_path, parent_id=prev, content=b"conc", idx=i)
818
819 errors: list[str] = []
820 lock = threading.Lock()
821
822 def _read() -> None:
823 res = _invoke(["verify", "--json"], env=_env(tmp_path))
824 with lock:
825 if res.exit_code != 0:
826 errors.append(f"exit_code={res.exit_code}")
827 else:
828 try:
829 data = _parse_json(res)
830 if not data["all_ok"]:
831 errors.append("all_ok=False")
832 except Exception as exc:
833 errors.append(str(exc))
834
835 threads = [threading.Thread(target=_read) for _ in range(10)]
836 for t in threads:
837 t.start()
838 for t in threads:
839 t.join()
840 assert errors == [], f"Concurrent read failures: {errors}"
841
842
843 # ---------------------------------------------------------------------------
844 # Flag registration
845 # ---------------------------------------------------------------------------
846
847
848 class TestRegisterFlags:
849 def _parse(self, *args: str) -> "argparse.Namespace":
850 import argparse
851 from muse.cli.commands.verify import register
852 p = argparse.ArgumentParser()
853 sub = p.add_subparsers()
854 register(sub)
855 return p.parse_args(["verify", *args])
856
857 def test_default_json_out_is_false(self) -> None:
858 ns = self._parse()
859 assert ns.json_out is False
860
861 def test_json_flag_sets_json_out(self) -> None:
862 ns = self._parse("--json")
863 assert ns.json_out is True
864
865 def test_j_shorthand_sets_json_out(self) -> None:
866 ns = self._parse("-j")
867 assert ns.json_out is True
File History 1 commit
sha256:2eaa5d95f9d9383498e76947410a26e5a3ba23d182f339910c424cf88fad412b fix: try fetch/presign before fetch/mpack to avoid Cloudfla… Sonnet 4.6 patch 7 days ago