gabriel / muse public
test_cmd_verify_commit.py python
1,057 lines 47.1 KB
Raw
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d feat(pack): delta-encode snapshots in MPackBundle wire format Sonnet 4.6 minor ⚠ breaking 121 days ago
1 """Tests for ``muse verify-commit`` — verify Ed25519 signatures on commits.
2
3 Coverage tiers
4 --------------
5 Unit:
6 _resolve_ref — HEAD→tip, HEAD→missing branch, sha256:-prefixed passthrough,
7 branch name→ref file, missing branch ref→None
8 _verify_one — valid sig, unsigned, missing commit, missing pubkey, bit-flip
9 in signature, unknown sig
10 algo, unknown pubkey algo, committed_at tamper, model_id tamper,
11 wrong keypair, decode_pubkey ValueError, signed_at non-empty,
12 error None on success, key_status cache hit
13 _fetch_key_status — active, revoked, unknown status value, network error
14
15 Integration:
16 Text output: OK/BAD/ERR lines, (unsigned) signer, key= part present/absent
17 JSON output: all schema fields, error field stripped, signed_at non-empty,
18 signer matches agent_id
19 --strict: unsigned exits nonzero; without --strict exits 0
20 --check-key-status: unknown without hub; caches per key_id
21 HEAD shorthand, branch name ref
22 Batch: all valid exits 0; one invalid exits nonzero; result order preserved
23 Nonexistent sha256:- ref → USER_ERROR
24 --json flag accepted (shorthand alias)
25
26 Security:
27 ANSI escape in ref → rejected, error to stderr, stdout empty
28 Null byte in ref → rejected
29 Path traversal ref → rejected
30 Bare hex ref → rejected with clear message, error to stderr, stdout empty
31 No traceback on bad ref or bad format
32
33 Data integrity:
34 committed_at tamper → invalid through full CLI flow
35 model_id tamper → invalid through full CLI flow
36 Wrong keypair → invalid (public key doesn't match signing key)
37
38 Stress:
39 100 signed commits all verify correctly (unit)
40 Batch of 50 commits via CLI → all results emitted
41 key_status_cache: N commits with same key → exactly 1 network call
42 """
43
44 from __future__ import annotations
45 from collections.abc import Mapping
46
47 import datetime
48 import json
49 import pathlib
50 from typing import TYPE_CHECKING
51 from unittest.mock import MagicMock, patch
52
53 import pytest
54
55 from muse.core.types import blob_id, decode_sig, encode_sig, split_id
56
57 if TYPE_CHECKING:
58 from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
59 from muse.core.object_store import write_object
60 from muse.core.provenance import (
61 encode_public_key,
62 provenance_payload,
63 sign_commit_record,
64 verify_commit_ed25519,
65 )
66 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
67 from muse.core.store import CommitRecord, SnapshotRecord, commit_path, write_commit, write_snapshot
68 from muse.core.types import Manifest
69 from muse.core.paths import heads_dir, muse_dir, ref_path
70 from tests.cli_test_helper import CliRunner, InvokeResult
71
72 runner = CliRunner()
73
74 _REPO_ID = "verify-commit-test"
75 _counter = 0
76
77
78 # ---------------------------------------------------------------------------
79 # Helpers
80 # ---------------------------------------------------------------------------
81
82
83 def _init_repo(path: pathlib.Path) -> pathlib.Path:
84 muse = muse_dir(path)
85 for d in ("commits", "snapshots", "objects", "refs/heads", "code"):
86 (muse / d).mkdir(parents=True, exist_ok=True)
87 (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
88 (muse / "repo.json").write_text(
89 json.dumps({"repo_id": _REPO_ID, "domain": "code"}), encoding="utf-8"
90 )
91 return path
92
93
94 def _env(repo: pathlib.Path) -> Mapping[str, str]:
95 return {"MUSE_REPO_ROOT": str(repo)}
96
97
98 def _make_key() -> "Ed25519PrivateKey":
99 """Generate a fresh Ed25519 private key."""
100 from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
101 return Ed25519PrivateKey.generate()
102
103
104 def _commit_files(
105 root: pathlib.Path,
106 files: Mapping[str, bytes],
107 branch: str = "main",
108 message: str | None = None,
109 sign: bool = False,
110 private_key: "Ed25519PrivateKey | None" = None,
111 agent_id: str = "test-agent",
112 model_id: str = "",
113 ) -> tuple[str, CommitRecord]:
114 """Create a commit; optionally sign it. Returns (commit_id, CommitRecord)."""
115 global _counter
116 _counter += 1
117 manifest: Manifest = {}
118 for rel_path, content in files.items():
119 obj_id = blob_id(content)
120 write_object(root, obj_id, content)
121 manifest[rel_path] = obj_id
122 abs_path = root / rel_path
123 abs_path.parent.mkdir(parents=True, exist_ok=True)
124 abs_path.write_bytes(content)
125 snap_id = compute_snapshot_id(manifest)
126 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
127 committed_at = datetime.datetime.now(datetime.timezone.utc)
128 branch_ref = ref_path(root, branch)
129 parent_id = branch_ref.read_text(encoding="utf-8").strip() if branch_ref.exists() else None
130 parents = [parent_id] if parent_id else []
131 msg = message or f"commit {_counter}"
132
133 # Resolve public key before compute_commit_id so signer_public_key is
134 # bound into the v2 hash (matching what _verify_commit_id expects).
135 sig = ""
136 pub_b64 = ""
137 key_id = ""
138 if sign and private_key is not None:
139 from muse.core.provenance import encode_public_key
140 _, pub_b64 = encode_public_key(private_key)
141
142 commit_id = compute_commit_id(
143 parent_ids=parents,
144 snapshot_id=snap_id,
145 message=msg,
146 committed_at_iso=committed_at.isoformat(),
147 signer_public_key=pub_b64,
148 )
149
150 if sign and private_key is not None:
151 result = sign_commit_record(
152 commit_id,
153 agent_id,
154 private_key,
155 model_id=model_id,
156 committed_at=committed_at.isoformat(),
157 )
158 if result:
159 sig, pub_b64, key_id = result
160
161 record = CommitRecord(
162 repo_id=_REPO_ID,
163 commit_id=commit_id,
164 branch=branch,
165 snapshot_id=snap_id,
166 message=msg,
167 committed_at=committed_at,
168 parent_commit_id=parent_id,
169 agent_id=agent_id if sign else "",
170 model_id=model_id if sign else "",
171 signature=sig,
172 signer_public_key=pub_b64,
173 signer_key_id=key_id,
174 )
175 write_commit(root, record)
176 branch_ref.write_text(commit_id, encoding="utf-8")
177 return commit_id, record
178
179
180 def _invoke(repo: pathlib.Path, *args: str) -> InvokeResult:
181 from muse.cli.app import main as cli
182 return runner.invoke(cli, ["verify-commit", *args], env=_env(repo))
183
184
185 def _force_write_commit(root: pathlib.Path, record: CommitRecord) -> None:
186 """Overwrite a commit file unconditionally (bypasses write_commit idempotency)."""
187 import msgpack
188 commit_file = commit_path(root, record.commit_id)
189 commit_file.write_bytes(msgpack.packb(record.to_dict(), use_bin_type=True))
190
191
192 # ---------------------------------------------------------------------------
193 # Unit — _resolve_ref
194 # ---------------------------------------------------------------------------
195
196
197 class TestResolveRef:
198 def test_head_resolves_to_branch_tip(self, tmp_path: pathlib.Path) -> None:
199 from muse.cli.commands.verify_commit import _resolve_ref
200 root = _init_repo(tmp_path)
201 key = _make_key()
202 commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key)
203 resolved = _resolve_ref(root, "HEAD")
204 assert resolved == commit_id
205
206 def test_head_returns_none_when_branch_has_no_commits(self, tmp_path: pathlib.Path) -> None:
207 from muse.cli.commands.verify_commit import _resolve_ref
208 root = _init_repo(tmp_path)
209 # HEAD points to main but main ref file doesn't exist yet
210 resolved = _resolve_ref(root, "HEAD")
211 assert resolved is None
212
213 def test_sha256_prefixed_id_passthrough(self, tmp_path: pathlib.Path) -> None:
214 """sha256:-prefixed IDs are returned as-is (no ref-file lookup)."""
215 from muse.cli.commands.verify_commit import _resolve_ref
216 root = _init_repo(tmp_path)
217 key = _make_key()
218 commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key)
219 resolved = _resolve_ref(root, commit_id)
220 assert resolved == commit_id
221
222 def test_bare_hex_normalised_to_sha256_prefix(self, tmp_path: pathlib.Path) -> None:
223 """A 64-char bare hex is normalised to sha256: prefix."""
224 from muse.cli.commands.verify_commit import _resolve_ref
225 root = _init_repo(tmp_path)
226 key = _make_key()
227 commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key)
228 bare = split_id(commit_id)[1]
229 resolved = _resolve_ref(root, bare)
230 assert resolved == commit_id
231
232 def test_branch_name_resolves_via_ref_file(self, tmp_path: pathlib.Path) -> None:
233 from muse.cli.commands.verify_commit import _resolve_ref
234 root = _init_repo(tmp_path)
235 key = _make_key()
236 commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key, branch="dev")
237 resolved = _resolve_ref(root, "dev")
238 assert resolved == commit_id
239
240 def test_missing_branch_ref_returns_none(self, tmp_path: pathlib.Path) -> None:
241 from muse.cli.commands.verify_commit import _resolve_ref
242 root = _init_repo(tmp_path)
243 resolved = _resolve_ref(root, "nonexistent-branch")
244 assert resolved is None
245
246
247 # ---------------------------------------------------------------------------
248 # Unit — _verify_one
249 # ---------------------------------------------------------------------------
250
251
252 class TestVerifyOne:
253 def test_valid_signature(self, tmp_path: pathlib.Path) -> None:
254 from muse.cli.commands.verify_commit import _verify_one
255 root = _init_repo(tmp_path)
256 key = _make_key()
257 commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key)
258 result = _verify_one(root, commit_id)
259 assert result["valid"] is True
260 assert result["commit_id"] == commit_id
261 assert len(result["key_id"]) > 0
262
263 def test_unsigned_commit_valid_false_no_error(self, tmp_path: pathlib.Path) -> None:
264 from muse.cli.commands.verify_commit import _verify_one
265 root = _init_repo(tmp_path)
266 commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=False)
267 result = _verify_one(root, commit_id)
268 assert result["valid"] is False
269 assert result["error"] is None
270 assert result["signer"] == ""
271
272 def test_missing_commit_returns_error(self, tmp_path: pathlib.Path) -> None:
273 from muse.cli.commands.verify_commit import _verify_one
274 root = _init_repo(tmp_path)
275 result = _verify_one(root, blob_id(b"nonexistent commit"))
276 assert result["valid"] is False
277 assert "not found" in (result["error"] or "")
278
279 def test_missing_public_key_valid_false(self, tmp_path: pathlib.Path) -> None:
280 from muse.cli.commands.verify_commit import _verify_one
281 root = _init_repo(tmp_path)
282 key = _make_key()
283 commit_id, record = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key)
284 tampered = CommitRecord(
285 commit_id=record.commit_id, repo_id=record.repo_id, branch=record.branch,
286 snapshot_id=record.snapshot_id, message=record.message,
287 committed_at=record.committed_at, parent_commit_id=record.parent_commit_id,
288 agent_id=record.agent_id, signature=record.signature,
289 signer_public_key="", # stripped
290 signer_key_id=record.signer_key_id,
291 )
292 _force_write_commit(root, tampered)
293 result = _verify_one(root, commit_id)
294 assert result["valid"] is False
295
296 def test_bit_flip_in_signature(self, tmp_path: pathlib.Path) -> None:
297 """A single bit flip in the stored signature must invalidate it."""
298 from muse.cli.commands.verify_commit import _verify_one
299 root = _init_repo(tmp_path)
300 key = _make_key()
301 commit_id, record = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key)
302 algo, sig_bytes = decode_sig(record.signature)
303 flipped = bytes([sig_bytes[0] ^ 0xFF]) + sig_bytes[1:]
304 bad_sig = encode_sig(algo, flipped)
305 tampered = CommitRecord(
306 commit_id=record.commit_id, repo_id=record.repo_id, branch=record.branch,
307 snapshot_id=record.snapshot_id, message=record.message,
308 committed_at=record.committed_at, parent_commit_id=record.parent_commit_id,
309 agent_id=record.agent_id, signature=bad_sig,
310 signer_public_key=record.signer_public_key, signer_key_id=record.signer_key_id,
311 )
312 _force_write_commit(root, tampered)
313 result = _verify_one(root, commit_id)
314 assert result["valid"] is False
315
316 def test_unknown_signature_algorithm(self, tmp_path: pathlib.Path) -> None:
317 """A commit whose signature carries an unknown algorithm prefix returns valid=False."""
318 from muse.cli.commands.verify_commit import _verify_one
319 root = _init_repo(tmp_path)
320 key = _make_key()
321 commit_id, record = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key)
322 _, raw_sig_bytes = decode_sig(record.signature)
323 unknown_sig = encode_sig("mldsa65", raw_sig_bytes)
324 tampered = CommitRecord(
325 commit_id=record.commit_id, repo_id=record.repo_id, branch=record.branch,
326 snapshot_id=record.snapshot_id, message=record.message,
327 committed_at=record.committed_at, parent_commit_id=record.parent_commit_id,
328 agent_id=record.agent_id, signature=unknown_sig,
329 signer_public_key=record.signer_public_key, signer_key_id=record.signer_key_id,
330 )
331 _force_write_commit(root, tampered)
332 result = _verify_one(root, commit_id)
333 assert result["valid"] is False
334 assert "mldsa65" in (result.get("error") or "")
335
336 def test_unknown_public_key_algorithm(self, tmp_path: pathlib.Path) -> None:
337 """A commit with an unknown pubkey algorithm prefix returns valid=False with error."""
338 from muse.cli.commands.verify_commit import _verify_one
339 from muse.core.types import encode_pubkey, encode_sig
340 from muse.core.snapshot import compute_snapshot_id
341 from muse.core.store import write_snapshot, SnapshotRecord
342 root = _init_repo(tmp_path)
343
344 # Build commit with mldsa65 key from scratch so commit_id is consistent.
345 fake_mldsa_key = encode_pubkey("mldsa65", b"\xab" * 32)
346 fake_sig = encode_sig("ed25519", b"\x00" * 64)
347 snap_id = compute_snapshot_id({"a.py": blob_id(b"x = 1\n")})
348 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest={"a.py": blob_id(b"x = 1\n")}))
349 import datetime
350 committed_at = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
351 commit_id = compute_commit_id( parent_ids=[], snapshot_id=snap_id,
352 message="mldsa-test", committed_at_iso=committed_at.isoformat(),
353 signer_public_key=fake_mldsa_key,
354 )
355 record = CommitRecord(
356 commit_id=commit_id, repo_id=_REPO_ID, branch="main",
357 snapshot_id=snap_id, message="mldsa-test", committed_at=committed_at,
358 agent_id="test-agent", signature=fake_sig,
359 signer_public_key=fake_mldsa_key,
360 )
361 commit_path(root, commit_id).parent.mkdir(parents=True, exist_ok=True)
362 _force_write_commit(root, record)
363 (heads_dir(root) / "main").write_text(commit_id, encoding="utf-8")
364
365 result = _verify_one(root, commit_id)
366 assert result["valid"] is False
367 assert "mldsa65" in (result.get("error") or "")
368
369 def test_committed_at_tamper_invalidates_signature(self, tmp_path: pathlib.Path) -> None:
370 """Mutating committed_at in the stored record must invalidate the signature."""
371 from muse.cli.commands.verify_commit import _verify_one
372 root = _init_repo(tmp_path)
373 key = _make_key()
374 commit_id, record = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key)
375 # Shift committed_at by one day.
376 original_ts = record.committed_at
377 tampered_ts = original_ts + datetime.timedelta(days=1)
378 tampered = CommitRecord(
379 commit_id=record.commit_id, repo_id=record.repo_id, branch=record.branch,
380 snapshot_id=record.snapshot_id, message=record.message,
381 committed_at=tampered_ts, # mutated
382 parent_commit_id=record.parent_commit_id,
383 agent_id=record.agent_id, signature=record.signature,
384 signer_public_key=record.signer_public_key, signer_key_id=record.signer_key_id,
385 )
386 _force_write_commit(root, tampered)
387 result = _verify_one(root, commit_id)
388 assert result["valid"] is False
389
390 def test_agent_id_tamper_invalidates_signature(self, tmp_path: pathlib.Path) -> None:
391 """Changing agent_id in the stored record must invalidate the signature."""
392 from muse.cli.commands.verify_commit import _verify_one
393 root = _init_repo(tmp_path)
394 key = _make_key()
395 commit_id, record = _commit_files(
396 root, {"a.py": b"x = 1\n"}, sign=True, private_key=key, agent_id="agent-A"
397 )
398 tampered = CommitRecord(
399 commit_id=record.commit_id, repo_id=record.repo_id, branch=record.branch,
400 snapshot_id=record.snapshot_id, message=record.message,
401 committed_at=record.committed_at, parent_commit_id=record.parent_commit_id,
402 agent_id="agent-B", # tampered
403 signature=record.signature,
404 signer_public_key=record.signer_public_key, signer_key_id=record.signer_key_id,
405 )
406 _force_write_commit(root, tampered)
407 result = _verify_one(root, commit_id)
408 assert result["valid"] is False
409
410 def test_model_id_tamper_invalidates_signature(self, tmp_path: pathlib.Path) -> None:
411 """Changing model_id in the stored record must invalidate the signature."""
412 from muse.cli.commands.verify_commit import _verify_one
413 root = _init_repo(tmp_path)
414 key = _make_key()
415 commit_id, record = _commit_files(
416 root, {"a.py": b"x = 1\n"}, sign=True, private_key=key, model_id="claude-sonnet-4-6"
417 )
418 tampered = CommitRecord(
419 commit_id=record.commit_id, repo_id=record.repo_id, branch=record.branch,
420 snapshot_id=record.snapshot_id, message=record.message,
421 committed_at=record.committed_at, parent_commit_id=record.parent_commit_id,
422 agent_id=record.agent_id, model_id="claude-opus-4-6", # tampered
423 signature=record.signature,
424 signer_public_key=record.signer_public_key, signer_key_id=record.signer_key_id,
425 )
426 _force_write_commit(root, tampered)
427 result = _verify_one(root, commit_id)
428 assert result["valid"] is False
429
430 def test_wrong_keypair_invalid(self, tmp_path: pathlib.Path) -> None:
431 """Public key from a different keypair must not verify the signature."""
432 from muse.cli.commands.verify_commit import _verify_one
433 root = _init_repo(tmp_path)
434 signing_key = _make_key()
435 wrong_key = _make_key()
436 commit_id, record = _commit_files(
437 root, {"a.py": b"x = 1\n"}, sign=True, private_key=signing_key
438 )
439 # Swap in the public key from wrong_key.
440 _, wrong_pub_b64 = encode_public_key(wrong_key)
441 tampered = CommitRecord(
442 commit_id=record.commit_id, repo_id=record.repo_id, branch=record.branch,
443 snapshot_id=record.snapshot_id, message=record.message,
444 committed_at=record.committed_at, parent_commit_id=record.parent_commit_id,
445 agent_id=record.agent_id, signature=record.signature,
446 signer_public_key=wrong_pub_b64, signer_key_id=record.signer_key_id,
447 )
448 _force_write_commit(root, tampered)
449 result = _verify_one(root, commit_id)
450 assert result["valid"] is False
451
452 def test_signed_at_populated_for_signed_commit(self, tmp_path: pathlib.Path) -> None:
453 """signed_at is a non-empty ISO string for a signed commit."""
454 from muse.cli.commands.verify_commit import _verify_one
455 root = _init_repo(tmp_path)
456 key = _make_key()
457 commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key)
458 result = _verify_one(root, commit_id)
459 assert result["signed_at"]
460 assert "T" in result["signed_at"] # ISO 8601 format
461
462 def test_error_none_for_valid_commit(self, tmp_path: pathlib.Path) -> None:
463 """error field is None when the signature is valid."""
464 from muse.cli.commands.verify_commit import _verify_one
465 root = _init_repo(tmp_path)
466 key = _make_key()
467 commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key)
468 result = _verify_one(root, commit_id)
469 assert result["error"] is None
470
471 def test_key_status_unknown_without_hub(self, tmp_path: pathlib.Path) -> None:
472 from muse.cli.commands.verify_commit import _verify_one
473 root = _init_repo(tmp_path)
474 key = _make_key()
475 commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key)
476 result = _verify_one(root, commit_id, check_key_status=True, hub_url=None)
477 assert result["key_status"] == "unknown"
478
479 def test_key_status_cache_hit_skips_network(self, tmp_path: pathlib.Path) -> None:
480 """Cache hit must prevent a second _fetch_key_status call."""
481 from muse.cli.commands.verify_commit import _verify_one
482 root = _init_repo(tmp_path)
483 key = _make_key()
484 commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key)
485 cache: dict[str, str] = {}
486 call_count = 0
487
488 def mock_fetch(hub_url: str, key_id: str) -> str:
489 nonlocal call_count
490 call_count += 1
491 return "active"
492
493 with patch("muse.cli.commands.verify_commit._fetch_key_status", side_effect=mock_fetch):
494 _verify_one(root, commit_id, check_key_status=True, hub_url="http://fake", key_status_cache=cache)
495 _verify_one(root, commit_id, check_key_status=True, hub_url="http://fake", key_status_cache=cache)
496
497 assert call_count == 1
498
499 def test_json_schema_all_keys_present(self, tmp_path: pathlib.Path) -> None:
500 from muse.cli.commands.verify_commit import _verify_one
501 root = _init_repo(tmp_path)
502 key = _make_key()
503 commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key)
504 result = _verify_one(root, commit_id)
505 assert {"commit_id", "valid", "signer", "key_id", "signed_at", "key_status", "error"} == set(result)
506
507
508 # ---------------------------------------------------------------------------
509 # Unit — _fetch_key_status
510 # ---------------------------------------------------------------------------
511
512
513 class TestFetchKeyStatus:
514 def test_returns_active(self) -> None:
515 from muse.cli.commands.verify_commit import _fetch_key_status
516 mock_resp = MagicMock()
517 mock_resp.read.return_value = json.dumps({"status": "active"}).encode()
518 mock_resp.__enter__ = lambda s: s
519 mock_resp.__exit__ = MagicMock(return_value=False)
520 with patch("urllib.request.urlopen", return_value=mock_resp):
521 assert _fetch_key_status("http://hub", "key123") == "active"
522
523 def test_returns_revoked(self) -> None:
524 from muse.cli.commands.verify_commit import _fetch_key_status
525 mock_resp = MagicMock()
526 mock_resp.read.return_value = json.dumps({"status": "revoked"}).encode()
527 mock_resp.__enter__ = lambda s: s
528 mock_resp.__exit__ = MagicMock(return_value=False)
529 with patch("urllib.request.urlopen", return_value=mock_resp):
530 assert _fetch_key_status("http://hub", "key123") == "revoked"
531
532 def test_returns_unknown_for_unrecognised_status(self) -> None:
533 from muse.cli.commands.verify_commit import _fetch_key_status
534 mock_resp = MagicMock()
535 mock_resp.read.return_value = json.dumps({"status": "pending"}).encode()
536 mock_resp.__enter__ = lambda s: s
537 mock_resp.__exit__ = MagicMock(return_value=False)
538 with patch("urllib.request.urlopen", return_value=mock_resp):
539 assert _fetch_key_status("http://hub", "key123") == "unknown"
540
541 def test_returns_unknown_on_network_error(self) -> None:
542 from muse.cli.commands.verify_commit import _fetch_key_status
543 with patch("urllib.request.urlopen", side_effect=OSError("connection refused")):
544 assert _fetch_key_status("http://hub", "key123") == "unknown"
545
546 def test_returns_unknown_on_timeout(self) -> None:
547 from muse.cli.commands.verify_commit import _fetch_key_status
548 import socket
549 with patch("urllib.request.urlopen", side_effect=socket.timeout("timed out")):
550 assert _fetch_key_status("http://hub", "key123") == "unknown"
551
552 def test_returns_unknown_on_invalid_json(self) -> None:
553 from muse.cli.commands.verify_commit import _fetch_key_status
554 mock_resp = MagicMock()
555 mock_resp.read.return_value = b"not json"
556 mock_resp.__enter__ = lambda s: s
557 mock_resp.__exit__ = MagicMock(return_value=False)
558 with patch("urllib.request.urlopen", return_value=mock_resp):
559 assert _fetch_key_status("http://hub", "key123") == "unknown"
560
561
562 # ---------------------------------------------------------------------------
563 # Integration — text output
564 # ---------------------------------------------------------------------------
565
566
567 class TestTextOutput:
568 def test_ok_line_format(self, tmp_path: pathlib.Path) -> None:
569 """Text output: 'OK <short_id> signer=<agent_id> key=<key_id>'"""
570 root = _init_repo(tmp_path)
571 key = _make_key()
572 commit_id, _ = _commit_files(
573 root, {"a.py": b"x = 1\n"}, sign=True, private_key=key, agent_id="claude-code"
574 )
575 result = _invoke(root, commit_id)
576 assert result.exit_code == 0
577 assert "OK" in result.output
578 assert "claude-code" in result.output
579 assert "key=" in result.output
580
581 def test_bad_line_for_invalid_signature(self, tmp_path: pathlib.Path) -> None:
582 root = _init_repo(tmp_path)
583 key = _make_key()
584 commit_id, record = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key)
585 algo, sig_bytes = decode_sig(record.signature)
586 flipped = bytes([sig_bytes[0] ^ 0xFF]) + sig_bytes[1:]
587 tampered = CommitRecord(
588 commit_id=record.commit_id, repo_id=record.repo_id, branch=record.branch,
589 snapshot_id=record.snapshot_id, message=record.message,
590 committed_at=record.committed_at, parent_commit_id=record.parent_commit_id,
591 agent_id=record.agent_id, signature=encode_sig(algo, flipped),
592 signer_public_key=record.signer_public_key, signer_key_id=record.signer_key_id,
593 )
594 _force_write_commit(root, tampered)
595 result = _invoke(root, commit_id)
596 assert result.exit_code != 0
597 assert "BAD" in result.output
598
599 def test_err_line_for_missing_commit(self, tmp_path: pathlib.Path) -> None:
600 """Text output: 'ERR <short_id> (commit not found)'"""
601 root = _init_repo(tmp_path)
602 key = _make_key()
603 commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key)
604 # Write a ref that points to a commit that doesn't exist.
605 ghost_id = blob_id(b"ghost commit that does not exist")
606 result = _invoke(root, ghost_id)
607 assert result.exit_code != 0
608 assert "ERR" in result.output
609
610 def test_unsigned_shows_unsigned_signer(self, tmp_path: pathlib.Path) -> None:
611 """Unsigned commits show '(unsigned)' as the signer in text output."""
612 root = _init_repo(tmp_path)
613 commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=False)
614 result = _invoke(root, commit_id)
615 assert result.exit_code == 0
616 assert "(unsigned)" in result.output
617
618 def test_key_absent_from_text_output_for_unsigned(self, tmp_path: pathlib.Path) -> None:
619 """key= part must not appear in text output for unsigned commits."""
620 root = _init_repo(tmp_path)
621 commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=False)
622 result = _invoke(root, commit_id)
623 assert "key=" not in result.output
624
625 def test_short_commit_id_in_text_output(self, tmp_path: pathlib.Path) -> None:
626 """Text output uses short_id, not the full 71-char commit ID."""
627 root = _init_repo(tmp_path)
628 key = _make_key()
629 commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key)
630 result = _invoke(root, commit_id)
631 assert commit_id not in result.output # full ID not present
632 assert commit_id[len("sha256:"):len("sha256:") + 12] in result.output # short hex present
633
634
635 # ---------------------------------------------------------------------------
636 # Integration — JSON output
637 # ---------------------------------------------------------------------------
638
639
640 class TestJsonOutput:
641 def test_valid_commit_all_fields(self, tmp_path: pathlib.Path) -> None:
642 root = _init_repo(tmp_path)
643 key = _make_key()
644 commit_id, _ = _commit_files(
645 root, {"a.py": b"x = 1\n"}, sign=True, private_key=key, agent_id="claude-code"
646 )
647 result = _invoke(root, commit_id, "--json")
648 assert result.exit_code == 0
649 data = json.loads(result.stdout)
650 assert data["commit_id"] == commit_id
651 assert data["valid"] is True
652 assert data["signer"] == "claude-code"
653 assert data["key_id"]
654 assert data["key_status"] == "unknown"
655
656 def test_signed_at_non_empty_for_signed_commit(self, tmp_path: pathlib.Path) -> None:
657 root = _init_repo(tmp_path)
658 key = _make_key()
659 commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key)
660 result = _invoke(root, commit_id, "--json")
661 data = json.loads(result.stdout)
662 assert data["signed_at"]
663 assert "T" in data["signed_at"]
664
665 def test_error_field_stripped_from_json_output(self, tmp_path: pathlib.Path) -> None:
666 """The internal 'error' field must not appear in emitted JSON."""
667 root = _init_repo(tmp_path)
668 key = _make_key()
669 commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key)
670 result = _invoke(root, commit_id, "--json")
671 data = json.loads(result.stdout)
672 assert "error" not in data
673
674 def test_duration_ms_and_exit_code_in_json(self, tmp_path: pathlib.Path) -> None:
675 """duration_ms and exit_code are present in every JSON result line."""
676 root = _init_repo(tmp_path)
677 key = _make_key()
678 commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key)
679 result = _invoke(root, commit_id, "--json")
680 data = json.loads(result.stdout)
681 assert "duration_ms" in data
682 assert isinstance(data["duration_ms"], float)
683 assert data["duration_ms"] >= 0
684 assert "exit_code" in data
685 assert data["exit_code"] == 0
686
687 def test_exit_code_nonzero_in_json_on_failure(self, tmp_path: pathlib.Path) -> None:
688 """exit_code reflects the actual exit status — non-zero when verification fails."""
689 root = _init_repo(tmp_path)
690 result = _invoke(root, blob_id(b"nonexistent commit"), "--json")
691 data = json.loads(result.stdout)
692 assert data["exit_code"] != 0
693 assert data["duration_ms"] >= 0
694
695 def test_unsigned_commit_json(self, tmp_path: pathlib.Path) -> None:
696 root = _init_repo(tmp_path)
697 commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=False)
698 result = _invoke(root, commit_id, "--json")
699 assert result.exit_code == 0
700 data = json.loads(result.stdout)
701 assert data["valid"] is False
702 assert data["signer"] == ""
703 assert "error" not in data
704
705 def test_batch_each_line_is_valid_json(self, tmp_path: pathlib.Path) -> None:
706 """Batch output: one valid JSON object per line."""
707 root = _init_repo(tmp_path)
708 key = _make_key()
709 ids = [_commit_files(root, {f"f{i}.py": f"x={i}".encode()}, sign=True, private_key=key)[0]
710 for i in range(3)]
711 result = _invoke(root, *ids, "--json")
712 assert result.exit_code == 0
713 lines = [l for l in result.stdout.strip().splitlines() if l]
714 assert len(lines) == 3
715 for line in lines:
716 obj = json.loads(line)
717 assert obj["valid"] is True
718
719 def test_batch_results_in_submission_order(self, tmp_path: pathlib.Path) -> None:
720 """Batch results must arrive in the same order as the input commit IDs."""
721 root = _init_repo(tmp_path)
722 key = _make_key()
723 ids = [_commit_files(root, {f"f{i}.py": f"x={i}".encode()}, sign=True, private_key=key)[0]
724 for i in range(5)]
725 result = _invoke(root, *ids, "--json")
726 assert result.exit_code == 0
727 lines = [l for l in result.stdout.strip().splitlines() if l]
728 returned_ids = [json.loads(l)["commit_id"] for l in lines]
729 assert returned_ids == ids
730
731 def test_json_flag_alias(self, tmp_path: pathlib.Path) -> None:
732 """--json is accepted and produces JSON output."""
733 root = _init_repo(tmp_path)
734 key = _make_key()
735 commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key)
736 result = _invoke(root, "--json", commit_id)
737 assert result.exit_code == 0
738 assert "valid" in json.loads(result.stdout)
739
740
741 # ---------------------------------------------------------------------------
742 # Integration — HEAD and branch name refs
743 # ---------------------------------------------------------------------------
744
745
746 class TestRefResolution:
747 def test_head_shorthand(self, tmp_path: pathlib.Path) -> None:
748 root = _init_repo(tmp_path)
749 key = _make_key()
750 _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key)
751 result = _invoke(root, "HEAD", "--json")
752 assert result.exit_code == 0
753 assert json.loads(result.stdout)["valid"] is True
754
755 def test_branch_name_ref(self, tmp_path: pathlib.Path) -> None:
756 root = _init_repo(tmp_path)
757 key = _make_key()
758 commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key, branch="dev")
759 result = _invoke(root, "dev", "--json")
760 assert result.exit_code == 0
761 data = json.loads(result.stdout)
762 assert data["commit_id"] == commit_id
763 assert data["valid"] is True
764
765 def test_nonexistent_sha256_ref_exits_user_error(self, tmp_path: pathlib.Path) -> None:
766 """A sha256:-prefixed ID that doesn't exist in the store exits USER_ERROR."""
767 root = _init_repo(tmp_path)
768 key = _make_key()
769 _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key)
770 ghost = blob_id(b"ghost commit")
771 result = _invoke(root, ghost, "--json")
772 assert result.exit_code != 0
773 data = json.loads(result.stdout)
774 assert data["valid"] is False
775
776 def test_nonexistent_branch_exits_user_error(self, tmp_path: pathlib.Path) -> None:
777 root = _init_repo(tmp_path)
778 result = _invoke(root, "no-such-branch")
779 assert result.exit_code != 0
780 assert result.stdout_bytes == b"" # error went to stderr
781
782
783 # ---------------------------------------------------------------------------
784 # Integration — --strict
785 # ---------------------------------------------------------------------------
786
787
788 class TestStrictMode:
789 def test_unsigned_no_strict_exits_0(self, tmp_path: pathlib.Path) -> None:
790 root = _init_repo(tmp_path)
791 commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=False)
792 result = _invoke(root, commit_id, "--json")
793 assert result.exit_code == 0
794 assert json.loads(result.stdout)["valid"] is False
795
796 def test_unsigned_strict_exits_nonzero(self, tmp_path: pathlib.Path) -> None:
797 root = _init_repo(tmp_path)
798 commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=False)
799 result = _invoke(root, commit_id, "--strict")
800 assert result.exit_code != 0
801
802 def test_signed_strict_exits_0(self, tmp_path: pathlib.Path) -> None:
803 root = _init_repo(tmp_path)
804 key = _make_key()
805 commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key)
806 result = _invoke(root, commit_id, "--strict")
807 assert result.exit_code == 0
808
809 def test_batch_one_unsigned_strict_exits_nonzero(self, tmp_path: pathlib.Path) -> None:
810 root = _init_repo(tmp_path)
811 key = _make_key()
812 cid1, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key)
813 cid2, _ = _commit_files(root, {"a.py": b"x = 2\n"}, sign=False)
814 result = _invoke(root, cid1, cid2, "--strict")
815 assert result.exit_code != 0
816
817
818 # ---------------------------------------------------------------------------
819 # Integration — --check-key-status
820 # ---------------------------------------------------------------------------
821
822
823 class TestCheckKeyStatus:
824 def test_unknown_without_hub(self, tmp_path: pathlib.Path) -> None:
825 root = _init_repo(tmp_path)
826 key = _make_key()
827 commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key)
828 result = _invoke(root, commit_id, "--check-key-status", "--json")
829 assert result.exit_code == 0
830 assert json.loads(result.stdout)["key_status"] == "unknown"
831
832
833
834 # ---------------------------------------------------------------------------
835 # Security
836 # ---------------------------------------------------------------------------
837
838
839 class TestSecurity:
840 def test_ansi_in_ref_rejected(self, tmp_path: pathlib.Path) -> None:
841 root = _init_repo(tmp_path)
842 result = _invoke(root, "\x1b[31mbad\x1b[0m")
843 assert result.exit_code != 0
844 assert result.stdout_bytes == b"" # error went to stderr
845
846 def test_null_byte_in_ref_rejected(self, tmp_path: pathlib.Path) -> None:
847 root = _init_repo(tmp_path)
848 result = _invoke(root, "sha256:abc\x00def")
849 assert result.exit_code != 0
850
851 def test_path_traversal_in_ref_rejected(self, tmp_path: pathlib.Path) -> None:
852 root = _init_repo(tmp_path)
853 result = _invoke(root, "../../etc/passwd")
854 assert result.exit_code != 0
855 assert result.stdout_bytes == b""
856
857 def test_bare_hex_ref_rejected_with_message(self, tmp_path: pathlib.Path) -> None:
858 """Bare hex without sha256: prefix → rejected; message mentions sha256:."""
859 root = _init_repo(tmp_path)
860 bare = "a" * 64
861 result = _invoke(root, bare)
862 assert result.exit_code != 0
863 assert result.stdout_bytes == b""
864 assert "sha256:" in result.stderr
865
866 def test_no_traceback_on_bad_ref(self, tmp_path: pathlib.Path) -> None:
867 root = _init_repo(tmp_path)
868 result = _invoke(root, "not-a-real-ref")
869 assert "Traceback" not in result.output
870 assert "Traceback" not in result.stderr
871
872
873 # ---------------------------------------------------------------------------
874 # Data integrity — full CLI flow
875 # ---------------------------------------------------------------------------
876
877
878 class TestDataIntegrity:
879 def test_committed_at_tamper_fails_cli(self, tmp_path: pathlib.Path) -> None:
880 """committed_at mutation detected through the full CLI verify-commit flow."""
881 root = _init_repo(tmp_path)
882 key = _make_key()
883 commit_id, record = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key)
884 tampered_ts = record.committed_at + datetime.timedelta(seconds=1)
885 tampered = CommitRecord(
886 commit_id=record.commit_id, repo_id=record.repo_id, branch=record.branch,
887 snapshot_id=record.snapshot_id, message=record.message,
888 committed_at=tampered_ts,
889 parent_commit_id=record.parent_commit_id,
890 agent_id=record.agent_id, signature=record.signature,
891 signer_public_key=record.signer_public_key, signer_key_id=record.signer_key_id,
892 )
893 _force_write_commit(root, tampered)
894 result = _invoke(root, commit_id, "--json")
895 assert result.exit_code != 0
896 assert json.loads(result.stdout)["valid"] is False
897
898 def test_model_id_tamper_fails_cli(self, tmp_path: pathlib.Path) -> None:
899 root = _init_repo(tmp_path)
900 key = _make_key()
901 commit_id, record = _commit_files(
902 root, {"a.py": b"x = 1\n"}, sign=True, private_key=key, model_id="claude-sonnet-4-6"
903 )
904 tampered = CommitRecord(
905 commit_id=record.commit_id, repo_id=record.repo_id, branch=record.branch,
906 snapshot_id=record.snapshot_id, message=record.message,
907 committed_at=record.committed_at, parent_commit_id=record.parent_commit_id,
908 agent_id=record.agent_id, model_id="gpt-5", # tampered
909 signature=record.signature,
910 signer_public_key=record.signer_public_key, signer_key_id=record.signer_key_id,
911 )
912 _force_write_commit(root, tampered)
913 result = _invoke(root, commit_id, "--json")
914 assert result.exit_code != 0
915 assert json.loads(result.stdout)["valid"] is False
916
917 def test_wrong_keypair_fails_cli(self, tmp_path: pathlib.Path) -> None:
918 """Public key from a different keypair → invalid through full CLI flow."""
919 root = _init_repo(tmp_path)
920 signing_key = _make_key()
921 wrong_key = _make_key()
922 commit_id, record = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=signing_key)
923 _, wrong_pub_b64 = encode_public_key(wrong_key)
924 tampered = CommitRecord(
925 commit_id=record.commit_id, repo_id=record.repo_id, branch=record.branch,
926 snapshot_id=record.snapshot_id, message=record.message,
927 committed_at=record.committed_at, parent_commit_id=record.parent_commit_id,
928 agent_id=record.agent_id, signature=record.signature,
929 signer_public_key=wrong_pub_b64, signer_key_id=record.signer_key_id,
930 )
931 _force_write_commit(root, tampered)
932 result = _invoke(root, commit_id, "--json")
933 assert result.exit_code != 0
934 assert json.loads(result.stdout)["valid"] is False
935
936 def test_ed25519_prefix_survives_store_roundtrip(self, tmp_path: pathlib.Path) -> None:
937 """Signature and public key must keep 'ed25519:' prefix through write→read."""
938 from muse.core.store import read_commit
939 root = _init_repo(tmp_path)
940 key = _make_key()
941 commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key)
942 reloaded = read_commit(root, commit_id)
943 assert reloaded is not None
944 assert reloaded.signature.startswith("ed25519:")
945 assert reloaded.signer_public_key.startswith("ed25519:")
946
947 def test_force_writecommit_path_matches_read_commit(self, tmp_path: pathlib.Path) -> None:
948 """_force_write_commit must write to the path that read_commit reads from."""
949 from muse.core.store import read_commit
950 root = _init_repo(tmp_path)
951 key = _make_key()
952 commit_id, record = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key)
953 sentinel = CommitRecord(
954 commit_id=record.commit_id, repo_id=record.repo_id, branch=record.branch,
955 snapshot_id=record.snapshot_id, message=record.message,
956 committed_at=record.committed_at, parent_commit_id=record.parent_commit_id,
957 agent_id="sentinel-agent", signature=record.signature,
958 signer_public_key=record.signer_public_key, signer_key_id=record.signer_key_id,
959 )
960 _force_write_commit(root, sentinel)
961 reloaded = read_commit(root, commit_id)
962 assert reloaded is not None
963 assert reloaded.agent_id == "sentinel-agent"
964
965
966 # ---------------------------------------------------------------------------
967 # Stress
968 # ---------------------------------------------------------------------------
969
970
971 class TestStress:
972 def test_100_signed_commits_all_valid(self, tmp_path: pathlib.Path) -> None:
973 """100 signed commits all verify correctly (unit path)."""
974 from muse.cli.commands.verify_commit import _verify_one
975 root = _init_repo(tmp_path)
976 key = _make_key()
977 for i in range(100):
978 commit_id, _ = _commit_files(
979 root, {f"f{i}.py": f"v = {i}\n".encode()}, sign=True, private_key=key
980 )
981 result = _verify_one(root, commit_id)
982 assert result["valid"] is True, f"commit {i} failed"
983
984 def test_batch_50_commits_all_results_emitted(self, tmp_path: pathlib.Path) -> None:
985 """Batch of 50 commits → CLI emits exactly 50 JSON lines."""
986 root = _init_repo(tmp_path)
987 key = _make_key()
988 ids = [
989 _commit_files(root, {f"f{i}.py": f"x={i}".encode()}, sign=True, private_key=key)[0]
990 for i in range(50)
991 ]
992 result = _invoke(root, *ids, "--json")
993 assert result.exit_code == 0
994 lines = [l for l in result.stdout.strip().splitlines() if l]
995 assert len(lines) == 50
996 assert all(json.loads(l)["valid"] for l in lines)
997
998 def test_10_different_keys_all_valid(self, tmp_path: pathlib.Path) -> None:
999 """Each commit signed with a different key verifies independently."""
1000 from muse.cli.commands.verify_commit import _verify_one
1001 root = _init_repo(tmp_path)
1002 for i in range(10):
1003 key = _make_key()
1004 commit_id, _ = _commit_files(
1005 root, {f"k{i}.py": f"v={i}".encode()}, sign=True, private_key=key
1006 )
1007 result = _verify_one(root, commit_id)
1008 assert result["valid"] is True, f"key {i} failed"
1009
1010 def test_key_status_cache_n_commits_same_key_one_call(self, tmp_path: pathlib.Path) -> None:
1011 """N commits sharing a key_id → exactly 1 network call via cache."""
1012 from muse.cli.commands.verify_commit import _verify_one
1013 root = _init_repo(tmp_path)
1014 key = _make_key()
1015 call_count = 0
1016
1017 def mock_fetch(hub_url: str, key_id: str) -> str:
1018 nonlocal call_count
1019 call_count += 1
1020 return "active"
1021
1022 cache: dict[str, str] = {}
1023 with patch("muse.cli.commands.verify_commit._fetch_key_status", side_effect=mock_fetch):
1024 for i in range(10):
1025 commit_id, _ = _commit_files(
1026 root, {f"f{i}.py": f"x={i}".encode()}, sign=True, private_key=key
1027 )
1028 _verify_one(root, commit_id, check_key_status=True, hub_url="http://fake", key_status_cache=cache)
1029
1030 assert call_count == 1, f"expected 1 network call, got {call_count}"
1031
1032
1033 # ---------------------------------------------------------------------------
1034 # Flag registration
1035 # ---------------------------------------------------------------------------
1036
1037
1038 class TestRegisterFlags:
1039 def _parse(self, *args: str) -> "argparse.Namespace":
1040 import argparse
1041 from muse.cli.commands.verify_commit import register
1042 p = argparse.ArgumentParser()
1043 sub = p.add_subparsers()
1044 register(sub)
1045 return p.parse_args(["verify-commit", *args])
1046
1047 def test_default_json_out_is_false(self) -> None:
1048 ns = self._parse("HEAD")
1049 assert ns.json_out is False
1050
1051 def test_json_flag_sets_json_out(self) -> None:
1052 ns = self._parse("HEAD", "--json")
1053 assert ns.json_out is True
1054
1055 def test_j_shorthand_sets_json_out(self) -> None:
1056 ns = self._parse("HEAD", "-j")
1057 assert ns.json_out is True
File History 1 commit
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d feat(pack): delta-encode snapshots in MPackBundle wire format Sonnet 4.6 minor 121 days ago