"""TDD: bare hex IDs are rejected at every CLI boundary. The sha256: prefix is a type tag, not decoration. It tells the system which algorithm produced the hash. Accepting bare hex at CLI boundaries forecloses future algorithm agility — if we ever add blake3: IDs, bare hex becomes fatally ambiguous. Architecture note ----------------- Enforcement belongs at the CLI outer shell — the hard boundary where untrusted user input enters the system. Internal functions like resolve_commit_ref() operate on already-validated input; they are not the primary enforcement point. Defense-in-depth at the core is a bonus, not the design. Rule (always, without exception) --------------------------------- - sha256:<64 lowercase hex> — full ID, accepted everywhere. - sha256: — prefix resolution, accepted. - — REJECTED at the CLI boundary with a clear error. The only place bare hex appears is on disk (filenames) — stripped on write, restored on read. Users never see it; agents never pass it. Covered boundaries ------------------ - muse snapshot read - muse snapshot export - muse snapshot-diff - muse verify-commit """ from __future__ import annotations import datetime import json import pathlib from muse.core.types import Manifest, blob_id, long_id, short_id from muse.core.object_store import write_object from muse.core.ids import hash_commit, hash_snapshot from muse.core.commits import ( CommitRecord, write_commit, ) from muse.core.snapshots import ( SnapshotRecord, write_snapshot, ) from muse.core.paths import muse_dir, ref_path from tests.cli_test_helper import CliRunner cli = None runner = CliRunner() # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- _BARE_HEX_FULL = "a" * 64 # 64 hex chars, no prefix _BARE_HEX_SHORT = "abc123def456" # short hex prefix, no prefix _INVALID_LOOK = "deadbeef" # 8 hex chars, no prefix def _init_repo(path: pathlib.Path) -> pathlib.Path: dot_muse = muse_dir(path) for d in ("commits", "snapshots", "objects", "refs/heads"): (dot_muse / d).mkdir(parents=True, exist_ok=True) (dot_muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8") (dot_muse / "repo.json").write_text( json.dumps({"repo_id": "bare-hex-test", "domain": "code"}), encoding="utf-8" ) return path def _env(repo: pathlib.Path) -> Manifest: return {"MUSE_REPO_ROOT": str(repo)} def _obj(repo: pathlib.Path, content: bytes) -> str: oid = blob_id(content) write_object(repo, oid, content) return oid def _snap(repo: pathlib.Path, manifest: Manifest) -> str: sid = hash_snapshot(manifest) write_snapshot( repo, SnapshotRecord( snapshot_id=sid, manifest=manifest, created_at=datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc), ), ) return sid def _commit(repo: pathlib.Path, sid: str, branch: str = "main") -> str: committed_at = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc) cid = hash_commit( parent_ids=[], snapshot_id=sid, message="test", committed_at_iso=committed_at.isoformat(), author="tester", ) write_commit( repo, CommitRecord( commit_id=cid, branch=branch, snapshot_id=sid, message="test", committed_at=committed_at, author="tester", parent_commit_id=None, ), ) ref = ref_path(repo, branch) ref.write_text(cid, encoding="utf-8") return cid def _create_snapshot_and_commit(repo: pathlib.Path) -> tuple[str, str]: """Return (snapshot_id, commit_id) for a one-file repo snapshot.""" oid = _obj(repo, b"hello world") sid = _snap(repo, {"file.txt": oid}) cid = _commit(repo, sid) return sid, cid # --------------------------------------------------------------------------- # muse snapshot read — bare hex must be rejected # --------------------------------------------------------------------------- class TestSnapshotReadBareHexRejected: """snapshot read must reject bare hex, full or short.""" def test_full_bare_hex_rejected(self, tmp_path: pathlib.Path) -> None: repo = _init_repo(tmp_path) result = runner.invoke(cli, ["snapshot", "read", _BARE_HEX_FULL], env=_env(repo)) assert result.exit_code != 0, "bare full 64-char hex must be rejected" def test_short_bare_hex_rejected(self, tmp_path: pathlib.Path) -> None: repo = _init_repo(tmp_path) result = runner.invoke(cli, ["snapshot", "read", _BARE_HEX_SHORT], env=_env(repo)) assert result.exit_code != 0, "bare short hex must be rejected" def test_8_char_bare_hex_rejected(self, tmp_path: pathlib.Path) -> None: repo = _init_repo(tmp_path) result = runner.invoke(cli, ["snapshot", "read", _INVALID_LOOK], env=_env(repo)) assert result.exit_code != 0, "any bare hex must be rejected" def test_error_message_mentions_sha256_prefix(self, tmp_path: pathlib.Path) -> None: repo = _init_repo(tmp_path) result = runner.invoke(cli, ["snapshot", "read", _BARE_HEX_SHORT], env=_env(repo)) assert result.exit_code != 0 assert "sha256:" in result.output.lower() or "sha256:" in (result.stderr or "").lower(), ( "error message must tell the user to use sha256: prefix" ) def test_prefixed_full_id_accepted(self, tmp_path: pathlib.Path) -> None: repo = _init_repo(tmp_path) sid, _ = _create_snapshot_and_commit(repo) result = runner.invoke(cli, ["snapshot", "read", sid], env=_env(repo)) assert result.exit_code == 0, f"sha256: prefixed full ID must be accepted; got: {result.stderr}" def test_prefixed_short_id_accepted(self, tmp_path: pathlib.Path) -> None: repo = _init_repo(tmp_path) sid, _ = _create_snapshot_and_commit(repo) # Short prefix: sha256: + first 12 hex chars short_prefixed = short_id(sid) result = runner.invoke(cli, ["snapshot", "read", short_prefixed], env=_env(repo)) assert result.exit_code == 0, ( f"sha256:-prefixed short ID must be accepted; got: {result.stderr}" ) # --------------------------------------------------------------------------- # muse snapshot export — bare hex must be rejected # --------------------------------------------------------------------------- class TestSnapshotExportBareHexRejected: """snapshot export must reject bare hex.""" def test_full_bare_hex_rejected(self, tmp_path: pathlib.Path) -> None: repo = _init_repo(tmp_path) out = tmp_path / "out.tar.gz" result = runner.invoke( cli, ["snapshot", "export", _BARE_HEX_FULL, "--output", str(out)], env=_env(repo), ) assert result.exit_code != 0, "bare hex must be rejected by snapshot export" def test_short_bare_hex_rejected(self, tmp_path: pathlib.Path) -> None: repo = _init_repo(tmp_path) out = tmp_path / "out.tar.gz" result = runner.invoke( cli, ["snapshot", "export", _BARE_HEX_SHORT, "--output", str(out)], env=_env(repo), ) assert result.exit_code != 0, "short bare hex must be rejected by snapshot export" def test_prefixed_id_accepted(self, tmp_path: pathlib.Path) -> None: repo = _init_repo(tmp_path) sid, _ = _create_snapshot_and_commit(repo) out = tmp_path / "out.tar.gz" result = runner.invoke( cli, ["snapshot", "export", sid, "--output", str(out)], env=_env(repo), ) assert result.exit_code == 0, f"sha256: prefixed ID must be accepted; got: {result.stderr}" def test_prefixed_short_id_accepted(self, tmp_path: pathlib.Path) -> None: repo = _init_repo(tmp_path) sid, _ = _create_snapshot_and_commit(repo) short_prefixed = short_id(sid) out = tmp_path / "out.tar.gz" result = runner.invoke( cli, ["snapshot", "export", short_prefixed, "--output", str(out)], env=_env(repo), ) assert result.exit_code == 0, ( f"sha256:-prefixed short ID must be accepted; got: {result.stderr}" ) # --------------------------------------------------------------------------- # muse snapshot-diff — bare hex must be rejected for both refs # --------------------------------------------------------------------------- class TestSnapshotDiffBareHexRejected: """snapshot-diff must reject bare hex in ref_a or ref_b position.""" def test_ref_a_bare_hex_full_rejected(self, tmp_path: pathlib.Path) -> None: repo = _init_repo(tmp_path) sid, _ = _create_snapshot_and_commit(repo) result = runner.invoke( cli, ["snapshot-diff", _BARE_HEX_FULL, sid], env=_env(repo) ) assert result.exit_code != 0, "bare hex in ref_a position must be rejected" def test_ref_b_bare_hex_full_rejected(self, tmp_path: pathlib.Path) -> None: repo = _init_repo(tmp_path) sid, _ = _create_snapshot_and_commit(repo) result = runner.invoke( cli, ["snapshot-diff", sid, _BARE_HEX_FULL], env=_env(repo) ) assert result.exit_code != 0, "bare hex in ref_b position must be rejected" def test_ref_a_bare_short_hex_rejected(self, tmp_path: pathlib.Path) -> None: repo = _init_repo(tmp_path) sid, _ = _create_snapshot_and_commit(repo) result = runner.invoke( cli, ["snapshot-diff", _BARE_HEX_SHORT, sid], env=_env(repo) ) assert result.exit_code != 0, "short bare hex in ref_a must be rejected" def test_ref_b_bare_short_hex_rejected(self, tmp_path: pathlib.Path) -> None: repo = _init_repo(tmp_path) sid, _ = _create_snapshot_and_commit(repo) result = runner.invoke( cli, ["snapshot-diff", sid, _BARE_HEX_SHORT], env=_env(repo) ) assert result.exit_code != 0, "short bare hex in ref_b must be rejected" def test_both_prefixed_full_ids_accepted(self, tmp_path: pathlib.Path) -> None: repo = _init_repo(tmp_path) oid_a = _obj(repo, b"version_a") oid_b = _obj(repo, b"version_b") sid_a = _snap(repo, {"f.txt": oid_a}) sid_b = _snap(repo, {"f.txt": oid_b}) result = runner.invoke(cli, ["snapshot-diff", sid_a, sid_b], env=_env(repo)) assert result.exit_code == 0, f"prefixed full IDs must be accepted; got: {result.stderr}" def test_ref_a_prefixed_short_accepted(self, tmp_path: pathlib.Path) -> None: repo = _init_repo(tmp_path) oid_a = _obj(repo, b"version_a") oid_b = _obj(repo, b"version_b") sid_a = _snap(repo, {"f.txt": oid_a}) sid_b = _snap(repo, {"f.txt": oid_b}) short_a = short_id(sid_a) result = runner.invoke(cli, ["snapshot-diff", short_a, sid_b], env=_env(repo)) assert result.exit_code == 0, ( f"sha256:-prefixed short ID in ref_a must be accepted; got: {result.stderr}" ) def test_ref_b_prefixed_short_accepted(self, tmp_path: pathlib.Path) -> None: repo = _init_repo(tmp_path) oid_a = _obj(repo, b"version_a") oid_b = _obj(repo, b"version_b") sid_a = _snap(repo, {"f.txt": oid_a}) sid_b = _snap(repo, {"f.txt": oid_b}) short_b = short_id(sid_b) result = runner.invoke(cli, ["snapshot-diff", sid_a, short_b], env=_env(repo)) assert result.exit_code == 0, ( f"sha256:-prefixed short ID in ref_b must be accepted; got: {result.stderr}" ) def test_both_prefixed_short_accepted(self, tmp_path: pathlib.Path) -> None: repo = _init_repo(tmp_path) oid_a = _obj(repo, b"version_a") oid_b = _obj(repo, b"version_b") sid_a = _snap(repo, {"f.txt": oid_a}) sid_b = _snap(repo, {"f.txt": oid_b}) short_a = short_id(sid_a) short_b = short_id(sid_b) result = runner.invoke(cli, ["snapshot-diff", short_a, short_b], env=_env(repo)) assert result.exit_code == 0, ( f"both sha256:-prefixed short IDs must be accepted; got: {result.stderr}" ) def test_branch_name_still_accepted(self, tmp_path: pathlib.Path) -> None: """Non-hex branch names must continue to resolve normally.""" repo = _init_repo(tmp_path) oid_a = _obj(repo, b"v1") oid_b = _obj(repo, b"v2") sid_a = _snap(repo, {"f.txt": oid_a}) sid_b = _snap(repo, {"f.txt": oid_b}) _commit(repo, sid_a, branch="main") _commit(repo, sid_b, branch="dev") result = runner.invoke(cli, ["snapshot-diff", "main", "dev"], env=_env(repo)) assert result.exit_code == 0, f"branch names must still resolve; got: {result.stderr}" def test_head_still_accepted(self, tmp_path: pathlib.Path) -> None: """HEAD must continue to resolve normally.""" repo = _init_repo(tmp_path) oid = _obj(repo, b"v1") sid = _snap(repo, {"f.txt": oid}) _commit(repo, sid) result = runner.invoke(cli, ["snapshot-diff", "HEAD", "HEAD"], env=_env(repo)) assert result.exit_code == 0, f"HEAD must still resolve; got: {result.stderr}" # --------------------------------------------------------------------------- # muse verify-commit — bare hex must be rejected # --------------------------------------------------------------------------- class TestVerifyCommitBareHexRejected: """verify-commit must reject bare 64-char hex commit IDs.""" def test_bare_64hex_rejected(self, tmp_path: pathlib.Path) -> None: repo = _init_repo(tmp_path) result = runner.invoke( cli, ["verify-commit", _BARE_HEX_FULL], env=_env(repo) ) assert result.exit_code != 0, "bare 64-char hex must be rejected by verify-commit" def test_prefixed_id_not_found_is_not_bare_hex_error(self, tmp_path: pathlib.Path) -> None: """A sha256:-prefixed ID that doesn't exist should fail with 'not found', not 'bare hex'.""" repo = _init_repo(tmp_path) prefixed = long_id("b" * 64) result = runner.invoke(cli, ["verify-commit", prefixed], env=_env(repo)) # Exit code != 0 is expected (commit doesn't exist), but the reason # must NOT be a bare-hex rejection — 'sha256:' prefix is correct. output_combined = result.output + (result.stderr or "") # The word "bare" should not appear if the input was correctly prefixed. assert "bare" not in output_combined.lower() or result.exit_code != 0 # --------------------------------------------------------------------------- # muse read — bare hex must be rejected at the CLI boundary # --------------------------------------------------------------------------- class TestReadBareHexRejected: """muse read must reject bare hex commit refs. show uses resolve_commit_ref() — the CLI layer must catch bare hex before that function is ever called. resolve_commit_ref() itself is internal and is not the enforcement point. """ def test_bare_full_hex_rejected(self, tmp_path: pathlib.Path) -> None: repo = _init_repo(tmp_path) result = runner.invoke(cli, ["read", _BARE_HEX_FULL], env=_env(repo)) assert result.exit_code != 0, "bare 64-char hex must be rejected by show" def test_bare_short_hex_rejected(self, tmp_path: pathlib.Path) -> None: repo = _init_repo(tmp_path) result = runner.invoke(cli, ["read", _BARE_HEX_SHORT], env=_env(repo)) assert result.exit_code != 0, "bare short hex must be rejected by show" def test_prefixed_full_id_accepted(self, tmp_path: pathlib.Path) -> None: repo = _init_repo(tmp_path) oid = _obj(repo, b"content") sid = _snap(repo, {"f.txt": oid}) cid = _commit(repo, sid) result = runner.invoke(cli, ["read", cid], env=_env(repo)) assert result.exit_code == 0, f"sha256:-prefixed full commit ID must be accepted; got: {result.stderr}" def test_branch_name_still_accepted(self, tmp_path: pathlib.Path) -> None: repo = _init_repo(tmp_path) oid = _obj(repo, b"content") sid = _snap(repo, {"f.txt": oid}) _commit(repo, sid, branch="main") result = runner.invoke(cli, ["read", "main"], env=_env(repo)) assert result.exit_code == 0, f"branch name must still resolve via show; got: {result.stderr}" def test_head_still_accepted(self, tmp_path: pathlib.Path) -> None: repo = _init_repo(tmp_path) oid = _obj(repo, b"content") sid = _snap(repo, {"f.txt": oid}) _commit(repo, sid, branch="main") result = runner.invoke(cli, ["read", "HEAD"], env=_env(repo)) assert result.exit_code == 0, f"HEAD must still resolve via show; got: {result.stderr}"