"""Tests for ``muse commit --attest`` — Phase 7.3. Rule #0 tier coverage: * unit — flag parsing, skip-if-no-provider, metadata storage shape. * integration — end-to-end: commit --attest with a registered stub provider produces a record under commit.metadata['attestation']. * security — provider raises AttestationRequiredError → commit aborted with ExitCode.ATTESTATION_REQUIRED (not silent skip); required=False non-required errors → commit succeeds without attestation; sensitive metadata never leaks into stderr. """ from __future__ import annotations import argparse import json import os import pathlib import subprocess from typing import Any import pytest from muse.cli.commands import commit as commit_cmd from muse.core.attestation import ( AnchorReceipt, AttestationRecord, AttestationRequiredError, MuseAttestationProvider, VerifyResult, register_attestation_provider, unregister_attestation_provider, ) from muse.core.errors import ExitCode # --------------------------------------------------------------------------- # Stub providers for fault injection # --------------------------------------------------------------------------- class _OkStub: """Always-succeeds provider — verifies happy-path behaviour.""" domain = "knowtation" def __init__(self) -> None: self.calls: list[tuple[str, dict[str, object]]] = [] def compute_attestation( self, snapshot_id: str, commit_meta: dict[str, object] ) -> AttestationRecord: self.calls.append((snapshot_id, dict(commit_meta))) return { "id": "a" * 64, "action": "write", "path": str(commit_meta.get("path", "")), "timestamp": str(commit_meta.get("timestamp", "")), "content_hash": snapshot_id, "sig": "stub-sig", "algorithm": "stub", "provider_id": "knowtation", } def anchor(self, record: AttestationRecord) -> AnchorReceipt: return { "tx_id": "stub", "anchor_url": "https://stub.example/x", "anchored_at": "", "provider_id": "knowtation", } def verify(self, record: AttestationRecord, receipt: AnchorReceipt) -> VerifyResult: return {"valid": True, "reason": "", "depth": 0, "provider_id": "knowtation"} class _RequiredStub(_OkStub): """Provider that always raises AttestationRequiredError.""" def compute_attestation( self, snapshot_id: str, commit_meta: dict[str, object] ) -> AttestationRecord: raise AttestationRequiredError("simulated air.required=true rejection") class _SoftFailStub(_OkStub): """Provider that raises a non-required AttestationError — should be soft-skipped.""" def compute_attestation( self, snapshot_id: str, commit_meta: dict[str, object] ) -> AttestationRecord: from muse.core.attestation import AttestationError raise AttestationError("transient endpoint failure") @pytest.fixture(autouse=True) def _registry_cleanup() -> None: """Ensure each test starts and ends with no provider registered.""" unregister_attestation_provider("knowtation") yield unregister_attestation_provider("knowtation") # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _init_repo(tmp_path: pathlib.Path) -> pathlib.Path: """Initialise a knowtation-domain Muse repo at *tmp_path* and chdir into it.""" os.chdir(tmp_path) from muse.cli.commands.init import run as init_run args = argparse.Namespace( bare=False, template=None, default_branch="main", force=False, domain="knowtation", as_json=False, ) init_run(args) (tmp_path / "notes").mkdir() (tmp_path / "notes" / "seed.md").write_text("# seed\n", encoding="utf-8") return tmp_path def _commit( tmp_path: pathlib.Path, *, message: str = "first", attest: bool = False, sign: bool = False, agent_id: str = "", ) -> dict[str, Any]: """Run muse commit and return the parsed JSON result.""" args = argparse.Namespace( message=message, allow_empty=False, dry_run=False, section=None, track=None, emotion=None, author=None, agent_id=agent_id or None, model_id=None, toolchain_id=None, event_type=None, meta=None, sign=sign, attest=attest, attest_config=None, fmt="json", ) captured: list[str] = [] import builtins real_print = builtins.print try: builtins.print = lambda *a, **kw: captured.append(" ".join(str(x) for x in a)) # type: ignore[assignment] commit_cmd.run(args) finally: builtins.print = real_print payload_lines = [c for c in captured if c.strip().startswith("{")] assert payload_lines, f"commit produced no JSON output; got: {captured!r}" return json.loads(payload_lines[-1]) # =========================================================================== # UNIT TIER # =========================================================================== class TestFlagParsing: """argparse should accept the new --attest and --attest-config flags.""" def test_attest_flag_in_parser(self) -> None: """Help text mentions --attest.""" parser = argparse.ArgumentParser() sp = parser.add_subparsers(dest="cmd") commit_cmd.register(sp) help_text = parser.format_help() # The subparser help text is populated lazily; ensure the option # is at least present in the parser's actions. commit_parser = sp.choices["commit"] opts = {a.option_strings[0] for a in commit_parser._actions if a.option_strings} assert "--attest" in opts assert "--attest-config" in opts # =========================================================================== # INTEGRATION TIER # =========================================================================== class TestEndToEnd: """Real init + commit flow with a registered provider.""" def test_attest_skipped_when_no_provider(self, tmp_path: pathlib.Path) -> None: """No provider → commit succeeds, no attestation key in metadata.""" from muse.core.store import read_commit _init_repo(tmp_path) result = _commit(tmp_path, attest=True) assert result["dry_run"] is False commit_record = read_commit(tmp_path, result["commit_id"]) assert commit_record is not None assert "attestation" not in (commit_record.metadata or {}) def test_attest_attaches_record_when_provider_present( self, tmp_path: pathlib.Path ) -> None: """Registered provider → metadata['attestation'] is canonical JSON.""" from muse.core.store import read_commit provider = _OkStub() register_attestation_provider("knowtation", provider) _init_repo(tmp_path) result = _commit(tmp_path, attest=True) commit_record = read_commit(tmp_path, result["commit_id"]) assert commit_record is not None attestation_blob = commit_record.metadata["attestation"] parsed = json.loads(attestation_blob) assert parsed["provider_id"] == "knowtation" assert parsed["sig"] == "stub-sig" assert provider.calls snap_id, meta = provider.calls[0] assert meta["action"] == "write" # Picks the first changed path in canonical sort order — covers both # `.museattributes` (created by init) and `notes/seed.md`. assert meta["path"] in (".museattributes", "notes/seed.md") def test_no_attest_flag_no_provider_call(self, tmp_path: pathlib.Path) -> None: """Without --attest the provider is not called even when registered.""" provider = _OkStub() register_attestation_provider("knowtation", provider) _init_repo(tmp_path) _commit(tmp_path, attest=False) assert provider.calls == [] # =========================================================================== # SECURITY TIER (mandatory) # =========================================================================== class TestSecurity: """Hardening contracts.""" def test_required_error_aborts_commit(self, tmp_path: pathlib.Path) -> None: """AttestationRequiredError → exit ATTESTATION_REQUIRED, no commit written.""" from muse.core.store import get_head_commit_id, read_current_branch register_attestation_provider("knowtation", _RequiredStub()) _init_repo(tmp_path) # Snapshot HEAD before — must not advance. branch = read_current_branch(tmp_path) head_before = get_head_commit_id(tmp_path, branch) with pytest.raises(SystemExit) as exc: _commit(tmp_path, attest=True) assert exc.value.code == ExitCode.ATTESTATION_REQUIRED head_after = get_head_commit_id(tmp_path, branch) assert head_after == head_before # HEAD unchanged: commit aborted. def test_soft_failure_does_not_abort(self, tmp_path: pathlib.Path) -> None: """Non-required AttestationError → commit succeeds, no record attached.""" from muse.core.store import read_commit register_attestation_provider("knowtation", _SoftFailStub()) _init_repo(tmp_path) result = _commit(tmp_path, attest=True) commit_record = read_commit(tmp_path, result["commit_id"]) assert commit_record is not None assert "attestation" not in (commit_record.metadata or {}) def test_attestation_record_canonical_json(self, tmp_path: pathlib.Path) -> None: """The stored attestation blob is sort_keys=True compact JSON — no NaN.""" from muse.core.store import read_commit register_attestation_provider("knowtation", _OkStub()) _init_repo(tmp_path) result = _commit(tmp_path, attest=True) commit_record = read_commit(tmp_path, result["commit_id"]) assert commit_record is not None blob = commit_record.metadata["attestation"] # Re-encoding sort_keys must equal the original — round-trip stable. roundtrip = json.dumps(json.loads(blob), sort_keys=True, separators=(",", ":")) assert blob == roundtrip