"""Tests for ``muse patch-id`` — content-based commit identity. Coverage tiers: - Unit: _compute_patch_id helper (same diff → same id, different → different, whitespace normalization with --stable, initial commit) - Integration: HEAD returns patch-id; specific commit; same diff = same patch-id across cherry-picked commits; --json schema; text format; nonexistent ref exits nonzero; empty repo exits nonzero - End-to-end: full CLI via CliRunner - Security: ANSI in ref rejected; path traversal in ref argument - Stress: 10-commit range returns 10 distinct patch-ids """ from __future__ import annotations import datetime import hashlib import json import pathlib import pytest from tests.cli_test_helper import CliRunner from muse.core.object_store import write_object from muse.core.snapshot import compute_commit_id, compute_snapshot_id from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot from muse.core._types import Manifest runner = CliRunner() _REPO_ID = "patch-id-test" _counter = 0 # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _sha(data: bytes) -> str: return hashlib.sha256(data).hexdigest() def _init_repo(path: pathlib.Path) -> pathlib.Path: muse = path / ".muse" for d in ("commits", "snapshots", "objects", "refs/heads", "code"): (muse / d).mkdir(parents=True, exist_ok=True) (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8") (muse / "repo.json").write_text( json.dumps({"repo_id": _REPO_ID, "domain": "code"}), encoding="utf-8" ) return path def _env(repo: pathlib.Path) -> dict[str, str]: return {"MUSE_REPO_ROOT": str(repo)} def _commit_files( root: pathlib.Path, files: dict[str, bytes], branch: str = "main", ) -> str: global _counter _counter += 1 manifest: Manifest = {} for rel_path, content in files.items(): obj_id = _sha(content) write_object(root, obj_id, content) manifest[rel_path] = obj_id abs_path = root / rel_path abs_path.parent.mkdir(parents=True, exist_ok=True) abs_path.write_bytes(content) snap_id = compute_snapshot_id(manifest) write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest)) committed_at = datetime.datetime.now(datetime.timezone.utc) ref_path = root / ".muse" / "refs" / "heads" / branch parent_id = ref_path.read_text(encoding="utf-8").strip() if ref_path.exists() else None parents = [parent_id] if parent_id else [] commit_id = compute_commit_id( parents, snap_id, f"commit {_counter}", committed_at.isoformat() ) write_commit( root, CommitRecord( commit_id=commit_id, repo_id=_REPO_ID, branch=branch, snapshot_id=snap_id, message=f"commit {_counter}", committed_at=committed_at, parent_commit_id=parent_id, ), ) ref_path.write_text(commit_id, encoding="utf-8") return commit_id def _invoke(repo: pathlib.Path, *args: str): from muse.cli.app import main as cli return runner.invoke(cli, ["patch-id", *args], env=_env(repo)) # --------------------------------------------------------------------------- # Unit — _compute_patch_id # --------------------------------------------------------------------------- def test_compute_patch_id_same_diff_same_id(tmp_path: pathlib.Path) -> None: from muse.cli.commands.patch_id import _compute_patch_id root = _init_repo(tmp_path) base = {"a.py": _sha(b"# a\n")} target = {"a.py": _sha(b"# b\n")} id1 = _compute_patch_id(root, base, target, stable=False) id2 = _compute_patch_id(root, base, target, stable=False) assert id1 == id2 assert len(id1) == 64 def test_compute_patch_id_different_diff_different_id(tmp_path: pathlib.Path) -> None: from muse.cli.commands.patch_id import _compute_patch_id root = _init_repo(tmp_path) write_object(root, _sha(b"# a\n"), b"# a\n") write_object(root, _sha(b"# b\n"), b"# b\n") write_object(root, _sha(b"# c\n"), b"# c\n") base = {"a.py": _sha(b"# a\n")} target1 = {"a.py": _sha(b"# b\n")} target2 = {"a.py": _sha(b"# c\n")} id1 = _compute_patch_id(root, base, target1, stable=False) id2 = _compute_patch_id(root, base, target2, stable=False) assert id1 != id2 def test_compute_patch_id_empty_diff_is_deterministic(tmp_path: pathlib.Path) -> None: """A commit that changes nothing (no-op) should produce a deterministic id.""" from muse.cli.commands.patch_id import _compute_patch_id root = _init_repo(tmp_path) write_object(root, _sha(b"# a\n"), b"# a\n") manifest = {"a.py": _sha(b"# a\n")} id1 = _compute_patch_id(root, manifest, manifest, stable=False) id2 = _compute_patch_id(root, manifest, manifest, stable=False) assert id1 == id2 def test_compute_patch_id_stable_normalizes_whitespace(tmp_path: pathlib.Path) -> None: """--stable strips trailing whitespace so 'hello ' and 'hello' produce same id.""" from muse.cli.commands.patch_id import _compute_patch_id root = _init_repo(tmp_path) content_a = b"x = 1\n" content_b = b"x = 2\n" content_b_ws = b"x = 2 \n" # trailing whitespace write_object(root, _sha(content_a), content_a) write_object(root, _sha(content_b), content_b) write_object(root, _sha(content_b_ws), content_b_ws) base = {"f.py": _sha(content_a)} target_clean = {"f.py": _sha(content_b)} target_ws = {"f.py": _sha(content_b_ws)} id_clean = _compute_patch_id(root, base, target_clean, stable=True) id_ws = _compute_patch_id(root, base, target_ws, stable=True) assert id_clean == id_ws, "--stable must treat trailing-whitespace differences as identical" def test_compute_patch_id_without_stable_is_sensitive_to_whitespace(tmp_path: pathlib.Path) -> None: from muse.cli.commands.patch_id import _compute_patch_id root = _init_repo(tmp_path) content_a = b"x = 1\n" content_b = b"x = 2\n" content_b_ws = b"x = 2 \n" write_object(root, _sha(content_a), content_a) write_object(root, _sha(content_b), content_b) write_object(root, _sha(content_b_ws), content_b_ws) base = {"f.py": _sha(content_a)} target_clean = {"f.py": _sha(content_b)} target_ws = {"f.py": _sha(content_b_ws)} id_clean = _compute_patch_id(root, base, target_clean, stable=False) id_ws = _compute_patch_id(root, base, target_ws, stable=False) assert id_clean != id_ws, "Without --stable, whitespace differences must produce different ids" # --------------------------------------------------------------------------- # Integration — JSON output # --------------------------------------------------------------------------- def test_patch_id_json_schema_keys(tmp_path: pathlib.Path) -> None: root = _init_repo(tmp_path) _commit_files(root, {"a.py": b"# a\n"}) _commit_files(root, {"a.py": b"# b\n"}) result = _invoke(root, "HEAD", "--json") assert result.exit_code == 0 data = json.loads(result.stdout) assert "commit_id" in data assert "patch_id" in data assert "subject" in data assert len(data["patch_id"]) == 64 def test_patch_id_json_subject_is_commit_message(tmp_path: pathlib.Path) -> None: root = _init_repo(tmp_path) _commit_files(root, {"a.py": b"# a\n"}) _commit_files(root, {"a.py": b"# b\n"}) result = _invoke(root, "HEAD", "--json") data = json.loads(result.stdout) assert isinstance(data["subject"], str) def test_patch_id_json_commit_id_matches_head(tmp_path: pathlib.Path) -> None: root = _init_repo(tmp_path) commit_id = _commit_files(root, {"a.py": b"# a\n"}) _commit_files(root, {"a.py": b"# b\n"}) head_commit_id = (root / ".muse" / "refs" / "heads" / "main").read_text().strip() result = _invoke(root, "HEAD", "--json") data = json.loads(result.stdout) assert data["commit_id"] == head_commit_id # --------------------------------------------------------------------------- # Integration — text output # --------------------------------------------------------------------------- def test_patch_id_text_output_has_two_parts(tmp_path: pathlib.Path) -> None: """Text output: ' '""" root = _init_repo(tmp_path) _commit_files(root, {"a.py": b"# a\n"}) _commit_files(root, {"a.py": b"# b\n"}) result = _invoke(root, "HEAD") assert result.exit_code == 0 line = result.stdout.strip() parts = line.split() assert len(parts) == 2 assert len(parts[0]) == 64 # patch_id assert len(parts[1]) == 64 # commit_id # --------------------------------------------------------------------------- # Integration — same diff → same patch-id (cherry-pick detection) # --------------------------------------------------------------------------- def test_patch_id_same_diff_same_id_across_commits(tmp_path: pathlib.Path) -> None: """Two commits that make the same change get the same patch-id.""" from muse.cli.commands.patch_id import _compute_patch_id root = _init_repo(tmp_path) # Set up a base state write_object(root, _sha(b"x = 1\n"), b"x = 1\n") write_object(root, _sha(b"x = 2\n"), b"x = 2\n") base_manifest = {"a.py": _sha(b"x = 1\n")} target_manifest = {"a.py": _sha(b"x = 2\n")} # Compute patch-id for the same logical diff twice id1 = _compute_patch_id(root, base_manifest, target_manifest, stable=False) id2 = _compute_patch_id(root, base_manifest, target_manifest, stable=False) assert id1 == id2 # --------------------------------------------------------------------------- # Integration — specific commit ref # --------------------------------------------------------------------------- def test_patch_id_specific_commit_id(tmp_path: pathlib.Path) -> None: root = _init_repo(tmp_path) commit1 = _commit_files(root, {"a.py": b"# a\n"}) commit2 = _commit_files(root, {"a.py": b"# b\n"}) result = _invoke(root, commit2, "--json") assert result.exit_code == 0 data = json.loads(result.stdout) assert data["commit_id"] == commit2 def test_patch_id_branch_name_ref(tmp_path: pathlib.Path) -> None: root = _init_repo(tmp_path) _commit_files(root, {"a.py": b"# a\n"}) result = _invoke(root, "main", "--json") assert result.exit_code == 0 data = json.loads(result.stdout) assert "patch_id" in data # --------------------------------------------------------------------------- # Integration — error cases # --------------------------------------------------------------------------- def test_patch_id_nonexistent_ref_exits_nonzero(tmp_path: pathlib.Path) -> None: root = _init_repo(tmp_path) _commit_files(root, {"a.py": b"# a\n"}) result = _invoke(root, "no-such-branch") assert result.exit_code != 0 def test_patch_id_empty_repo_exits_nonzero(tmp_path: pathlib.Path) -> None: root = _init_repo(tmp_path) result = _invoke(root, "HEAD") assert result.exit_code != 0 # --------------------------------------------------------------------------- # Security # --------------------------------------------------------------------------- def test_patch_id_ansi_in_ref_rejected(tmp_path: pathlib.Path) -> None: root = _init_repo(tmp_path) _commit_files(root, {"a.py": b"# a\n"}) result = _invoke(root, "\x1b[31mbad\x1b[0m") assert result.exit_code != 0 # --------------------------------------------------------------------------- # Stress — 10-commit range # --------------------------------------------------------------------------- def test_patch_id_different_commits_have_different_ids(tmp_path: pathlib.Path) -> None: """10 commits that each make distinct changes must produce 10 distinct patch-ids.""" from muse.cli.commands.patch_id import _compute_patch_id root = _init_repo(tmp_path) # Build 10 manifest transitions patch_ids = set() prev_manifest: dict[str, str] = {} for i in range(10): content = f"value = {i}\n".encode() obj_id = _sha(content) write_object(root, obj_id, content) new_manifest = {"file.py": obj_id} pid = _compute_patch_id(root, prev_manifest, new_manifest, stable=False) patch_ids.add(pid) prev_manifest = new_manifest assert len(patch_ids) == 10, "Each distinct diff must produce a unique patch-id"