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