""" Tests for two compounding data-integrity bugs in the store write path. === BUG 1: write_commit skips hash verification of existing records === Root cause (muse/core/store.py::write_commit): existing = CommitRecord.from_msgpack(_read_msgpack_dict(path)) if existing.commit_id != commit.commit_id: raise OSError(...) # checks stored field — not a recomputed hash return # ← skips if commit_id field matches The idempotency check compares the raw commit_id *field* stored in the msgpack record, not a recomputed hash of the core fields. A bit flip in snapshot_id, message, or parent_commit_id leaves commit_id intact, passes the check, and causes write_commit to silently skip the repair. The commit becomes permanently unreadable via read_commit (which DOES call _verify_commit_id). === BUG 2: write_snapshot skips ALL validation of existing records === Root cause (muse/core/store.py::write_snapshot): if path.exists(): logger.debug("⚠️ Snapshot %s already exists — skipped", ...) return # ← no parsing, no hash check, nothing write_snapshot does not parse or verify the existing file at all. Any corruption in the snapshot manifest — wrong object ID for a file, extra entries, missing entries — is silently skipped. read_snapshot always calls _verify_snapshot_id, which recomputes the manifest hash and raises on mismatch. The snapshot is permanently unreadable with no repair path. === Why this matters === Every commit references exactly one snapshot_id. If the snapshot at that ID is corrupt and unrepaired, these operations all fail for that commit: muse checkout — cannot build working tree muse show — cannot display file contents muse diff — cannot compute diff muse log --diff — crashes on affected commits muse push — pushes corrupt snapshot to hub muse pull (on another machine) — imports corrupt snapshot === The fix === write_commit: after confirming commit_id field matches, call _verify_commit_id. Wrap its OSError as ValueError so the existing `except Exception` branch triggers repair (overwrite) rather than propagation. write_snapshot: before returning early, parse and verify the snapshot. If verification fails, fall through to overwrite. === Coverage === Unit — write_commit skips clean existing record (no regression) Unit — write_commit repairs corrupt snapshot_id Unit — write_commit repairs corrupt message Unit — write_commit repairs corrupt parent_commit_id Unit — write_snapshot skips clean existing record (no regression) Unit — write_snapshot repairs corrupt manifest entry Unit — write_snapshot repairs corrupt object ID in manifest Unit — write_snapshot repairs completely empty manifest Data — read_commit returns good record after write_commit repair Data — read_snapshot returns good record after write_snapshot repair Data — commit → snapshot chain readable after both repairs Data — parent chain (A→B→C) survives corruption of middle commit Integration — muse checkout path: snapshot must survive write_snapshot repair Security — corrupt snapshot_id in commit cannot forge a different snapshot Stress — 50 concurrent writes on corrupt commit all repair Stress — 50 concurrent writes on corrupt snapshot all repair Regression — hard integrity violation (commit_id field mismatch) still raises Regression — write_commit normal path (no pre-existing file) still works Regression — write_snapshot normal path still works """ from __future__ import annotations import datetime import pathlib import threading import tempfile import msgpack import pytest from muse.core._types import Manifest, MsgpackDict # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path: (tmp_path / ".muse" / "commits").mkdir(parents=True, exist_ok=True) (tmp_path / ".muse" / "snapshots").mkdir(parents=True, exist_ok=True) return tmp_path def _ts(year: int = 2024) -> str: return f"{year}-01-01T00:00:00+00:00" def _good_commit( snapshot_id: str | None = None, message: str = "test commit", parent_commit_id: str | None = None, ts: str | None = None, ) -> "CommitRecord": from muse.core.store import CommitRecord from muse.core.snapshot import compute_commit_id snap_id = snapshot_id or "b" * 64 timestamp = ts or _ts() parent_ids = [parent_commit_id] if parent_commit_id else [] commit_id = compute_commit_id(parent_ids, snap_id, message, timestamp) return CommitRecord( commit_id=commit_id, repo_id="test-repo", branch="main", snapshot_id=snap_id, message=message, committed_at=datetime.datetime.fromisoformat(timestamp), parent_commit_id=parent_commit_id, parent2_commit_id=None, author="gabriel", metadata={}, ) def _good_snapshot(manifest: Manifest | None = None) -> "SnapshotRecord": from muse.core.store import SnapshotRecord from muse.core.snapshot import compute_snapshot_id m = manifest or {"src/main.py": "c" * 64} snapshot_id = compute_snapshot_id(m, {}) return SnapshotRecord(snapshot_id=snapshot_id, manifest=m, directories={}) def _write_corrupt_commit(repo: pathlib.Path, good: "CommitRecord", corrupt_field: MsgpackDict) -> None: """Write a commit file that has good commit_id but corrupt content.""" base = { "commit_id": good.commit_id, "repo_id": "test-repo", "branch": "main", "snapshot_id": good.snapshot_id, "message": good.message, "committed_at": good.committed_at.isoformat(), "parent_commit_id": good.parent_commit_id, "parent2_commit_id": None, "author": "gabriel", "metadata": {}, "format_version": 1, "reviewed_by": [], } base.update(corrupt_field) path = repo / ".muse" / "commits" / f"{good.commit_id}.msgpack" path.write_bytes(msgpack.packb(base, use_bin_type=True)) def _write_corrupt_snapshot(repo: pathlib.Path, good: "SnapshotRecord", corrupt_manifest: Manifest) -> None: """Write a snapshot file with a corrupt manifest.""" record = { "snapshot_id": good.snapshot_id, "manifest": corrupt_manifest, "directories": {}, "format_version": 1, } path = repo / ".muse" / "snapshots" / f"{good.snapshot_id}.msgpack" path.write_bytes(msgpack.packb(record, use_bin_type=True)) # ============================================================================= # 1. UNIT — write_commit must repair corrupt core fields # ============================================================================= class TestWriteCommitHashVerification: def test_idempotent_skip_clean_record(self, tmp_path: pathlib.Path) -> None: """Regression: write_commit on a clean existing file still returns fast.""" from muse.core.store import write_commit, read_commit repo = _make_repo(tmp_path) good = _good_commit() write_commit(repo, good) write_commit(repo, good) # second call: must not raise, must not change data result = read_commit(repo, good.commit_id) assert result is not None assert result.commit_id == good.commit_id def test_repairs_corrupt_snapshot_id(self, tmp_path: pathlib.Path) -> None: """ A commit file with a corrupt snapshot_id (wrong hash) must be repaired. BUG: write_commit compares existing.commit_id == commit.commit_id (stored field, unchanged), sees a match, and silently skips overwrite. The commit then fails read_commit's _verify_commit_id check forever. FIX: write_commit must also run _verify_commit_id on the existing record. """ from muse.core.store import write_commit, read_commit repo = _make_repo(tmp_path) good = _good_commit(snapshot_id="b" * 64) _write_corrupt_commit(repo, good, {"snapshot_id": "0" * 64}) # wrong hash write_commit(repo, good) # must repair result = read_commit(repo, good.commit_id) assert result is not None, ( "BUG: write_commit skipped repair of corrupt snapshot_id.\n" "The idempotency check only compares the commit_id FIELD — not the " "recomputed hash. A corrupt snapshot_id leaves commit_id intact, " "passes the check, and the file is never overwritten.\n" "FIX: write_commit must call _verify_commit_id on the existing record." ) assert result.snapshot_id == "b" * 64 def test_repairs_corrupt_message(self, tmp_path: pathlib.Path) -> None: """A commit with a corrupt message (alters the hash) must be repaired.""" from muse.core.store import write_commit, read_commit repo = _make_repo(tmp_path) good = _good_commit(message="original message") _write_corrupt_commit(repo, good, {"message": "CORRUPTED MESSAGE"}) write_commit(repo, good) result = read_commit(repo, good.commit_id) assert result is not None, "write_commit skipped repair of corrupt message" assert result.message == "original message" def test_repairs_corrupt_parent_commit_id(self, tmp_path: pathlib.Path) -> None: """A commit with a corrupt parent_commit_id must be repaired.""" from muse.core.store import write_commit, read_commit repo = _make_repo(tmp_path) good = _good_commit(parent_commit_id=None) # Inject a fake parent — changes the hash _write_corrupt_commit(repo, good, {"parent_commit_id": "d" * 64}) write_commit(repo, good) result = read_commit(repo, good.commit_id) assert result is not None, "write_commit skipped repair of corrupt parent_commit_id" assert result.parent_commit_id is None def test_hard_integrity_violation_still_raises(self, tmp_path: pathlib.Path) -> None: """ Regression: when commit_id FIELD in the file mismatches the expected filename/ID, write_commit must still raise OSError (hard violation). """ from muse.core.store import write_commit, read_commit repo = _make_repo(tmp_path) good_a = _good_commit(message="commit A", ts=_ts(2024)) good_b = _good_commit(message="commit B", ts=_ts(2025)) # Write commit B's data under commit A's path (repo / ".muse" / "commits" / f"{good_a.commit_id}.msgpack").write_bytes( msgpack.packb({ "commit_id": good_b.commit_id, # WRONG: different commit_id stored "repo_id": "test-repo", "branch": "main", "snapshot_id": good_b.snapshot_id, "message": good_b.message, "committed_at": good_b.committed_at.isoformat(), "parent_commit_id": None, "parent2_commit_id": None, "author": "gabriel", "metadata": {}, "format_version": 1, "reviewed_by": [], }, use_bin_type=True) ) with pytest.raises(OSError, match="Store integrity violation"): write_commit(repo, good_a) # ============================================================================= # 2. UNIT — write_snapshot must repair corrupt snapshots # ============================================================================= class TestWriteSnapshotHashVerification: def test_idempotent_skip_clean_snapshot(self, tmp_path: pathlib.Path) -> None: """Regression: write_snapshot on a clean existing file still skips correctly.""" from muse.core.store import write_snapshot, read_snapshot repo = _make_repo(tmp_path) good = _good_snapshot() write_snapshot(repo, good) write_snapshot(repo, good) # second call: idempotent result = read_snapshot(repo, good.snapshot_id) assert result is not None assert result.snapshot_id == good.snapshot_id def test_repairs_corrupt_object_id_in_manifest(self, tmp_path: pathlib.Path) -> None: """ A snapshot with a wrong object ID for a file must be repaired. BUG: write_snapshot sees path.exists() → True → return immediately. No parsing, no verification. read_snapshot then always returns None. FIX: write_snapshot must parse and verify the existing file; overwrite if corrupt. """ from muse.core.store import write_snapshot, read_snapshot repo = _make_repo(tmp_path) good = _good_snapshot({"src/main.py": "c" * 64}) _write_corrupt_snapshot(repo, good, {"src/main.py": "0" * 64}) # wrong obj id write_snapshot(repo, good) result = read_snapshot(repo, good.snapshot_id) assert result is not None, ( "BUG: write_snapshot did not repair corrupt snapshot manifest.\n" "write_snapshot sees path.exists() → True → returns immediately with\n" "no parsing or hash verification. Any corruption in the manifest is\n" "permanent — read_snapshot will always return None for this snapshot.\n" "FIX: write_snapshot must verify the existing file and overwrite if corrupt." ) assert result.manifest == {"src/main.py": "c" * 64} def test_repairs_extra_manifest_entry(self, tmp_path: pathlib.Path) -> None: """An extra file in the manifest (changes the hash) must be repaired.""" from muse.core.store import write_snapshot, read_snapshot repo = _make_repo(tmp_path) good = _good_snapshot({"src/main.py": "c" * 64}) _write_corrupt_snapshot(repo, good, { "src/main.py": "c" * 64, "INJECTED_FILE.py": "e" * 64, # extra entry → wrong hash }) write_snapshot(repo, good) result = read_snapshot(repo, good.snapshot_id) assert result is not None, "write_snapshot did not repair snapshot with extra manifest entry" assert "INJECTED_FILE.py" not in result.manifest def test_repairs_empty_manifest(self, tmp_path: pathlib.Path) -> None: """A snapshot with an empty manifest (should have files) must be repaired.""" from muse.core.store import write_snapshot, read_snapshot repo = _make_repo(tmp_path) good = _good_snapshot({"src/main.py": "c" * 64, "src/utils.py": "d" * 64}) _write_corrupt_snapshot(repo, good, {}) # manifest wiped write_snapshot(repo, good) result = read_snapshot(repo, good.snapshot_id) assert result is not None, "write_snapshot did not repair empty manifest" assert len(result.manifest) == 2 def test_repairs_missing_file_in_manifest(self, tmp_path: pathlib.Path) -> None: """A snapshot with a missing file entry must be repaired.""" from muse.core.store import write_snapshot, read_snapshot repo = _make_repo(tmp_path) good = _good_snapshot({"src/main.py": "c" * 64, "src/utils.py": "d" * 64}) _write_corrupt_snapshot(repo, good, {"src/main.py": "c" * 64}) # utils.py missing write_snapshot(repo, good) result = read_snapshot(repo, good.snapshot_id) assert result is not None, "write_snapshot did not repair snapshot with missing manifest entry" assert "src/utils.py" in result.manifest # ============================================================================= # 3. DATA INTEGRITY — full commit → snapshot chain # ============================================================================= class TestCommitSnapshotChain: def test_commit_and_snapshot_both_readable_after_repair(self, tmp_path: pathlib.Path) -> None: """After repairing both commit and snapshot, the full chain is readable.""" from muse.core.store import write_commit, read_commit, write_snapshot, read_snapshot repo = _make_repo(tmp_path) manifest = {"src/main.py": "c" * 64} good_snap = _good_snapshot(manifest) good_commit = _good_commit(snapshot_id=good_snap.snapshot_id) # Corrupt both _write_corrupt_commit(repo, good_commit, {"snapshot_id": "0" * 64}) _write_corrupt_snapshot(repo, good_snap, {"src/main.py": "0" * 64}) # Repair both write_commit(repo, good_commit) write_snapshot(repo, good_snap) # Full chain must be readable commit = read_commit(repo, good_commit.commit_id) assert commit is not None, "commit not readable after repair" snap = read_snapshot(repo, commit.snapshot_id) assert snap is not None, "snapshot not readable after repair" assert snap.manifest == manifest def test_parent_chain_survives_middle_commit_corruption(self, tmp_path: pathlib.Path) -> None: """A→B→C chain: corrupt B's snapshot_id, repair, verify all three readable.""" from muse.core.store import write_commit, read_commit repo = _make_repo(tmp_path) commit_a = _good_commit(message="commit A", ts=_ts(2022)) commit_b = _good_commit(message="commit B", parent_commit_id=commit_a.commit_id, ts=_ts(2023)) commit_c = _good_commit(message="commit C", parent_commit_id=commit_b.commit_id, ts=_ts(2024)) # Write all three write_commit(repo, commit_a) write_commit(repo, commit_b) write_commit(repo, commit_c) # Corrupt B _write_corrupt_commit(repo, commit_b, {"snapshot_id": "0" * 64}) # Repair B write_commit(repo, commit_b) # All three must be readable for c in [commit_a, commit_b, commit_c]: result = read_commit(repo, c.commit_id) assert result is not None, f"Commit '{c.message}' not readable after repair" assert result.commit_id == c.commit_id def test_snapshot_repair_does_not_affect_sibling_snapshots(self, tmp_path: pathlib.Path) -> None: """Repairing one snapshot must not corrupt or affect other snapshots.""" from muse.core.store import write_snapshot, read_snapshot repo = _make_repo(tmp_path) snap_a = _good_snapshot({"a.py": "a" * 64}) snap_b = _good_snapshot({"b.py": "b" * 64}) snap_c = _good_snapshot({"c.py": "c" * 64}) write_snapshot(repo, snap_a) write_snapshot(repo, snap_b) write_snapshot(repo, snap_c) # Corrupt B _write_corrupt_snapshot(repo, snap_b, {"b.py": "0" * 64}) # Repair B write_snapshot(repo, snap_b) # All three readable for snap in [snap_a, snap_b, snap_c]: result = read_snapshot(repo, snap.snapshot_id) assert result is not None, f"Snapshot {snap.snapshot_id[:8]} not readable" # ============================================================================= # 4. SECURITY — corrupt fields cannot forge content # ============================================================================= class TestSecurityCorruptFields: def test_corrupt_snapshot_id_in_commit_cannot_forge_different_snapshot(self, tmp_path: pathlib.Path) -> None: """ An attacker who corrupts a commit's snapshot_id cannot make Muse check out arbitrary content — the commit hash verification detects the change. After write_commit repairs the file, the original snapshot_id is restored. """ from muse.core.store import write_commit, read_commit repo = _make_repo(tmp_path) good = _good_commit(snapshot_id="b" * 64) attacker_snapshot = "e" * 64 # attacker wants to point at different content _write_corrupt_commit(repo, good, {"snapshot_id": attacker_snapshot}) write_commit(repo, good) result = read_commit(repo, good.commit_id) assert result is not None assert result.snapshot_id == "b" * 64, ( f"Attacker's snapshot_id {attacker_snapshot[:8]!r} survived repair!" ) def test_injected_manifest_entry_is_removed_on_snapshot_repair(self, tmp_path: pathlib.Path) -> None: """ An injected file in the manifest (hash mismatch) must be removed on repair. """ from muse.core.store import write_snapshot, read_snapshot repo = _make_repo(tmp_path) good = _good_snapshot({"src/main.py": "c" * 64}) _write_corrupt_snapshot(repo, good, { "src/main.py": "c" * 64, "malicious_backdoor.py": "e" * 64, }) write_snapshot(repo, good) result = read_snapshot(repo, good.snapshot_id) assert result is not None assert "malicious_backdoor.py" not in result.manifest # ============================================================================= # 5. STRESS — concurrent writes all repair correctly # ============================================================================= class TestStressConcurrentRepair: def test_concurrent_write_commit_repairs_corrupt_snapshot_id(self, tmp_path: pathlib.Path) -> None: """20 concurrent write_commit calls on a corrupt file all result in a readable commit.""" from muse.core.store import write_commit, read_commit repo = _make_repo(tmp_path) good = _good_commit() _write_corrupt_commit(repo, good, {"snapshot_id": "0" * 64}) failures = [] lock = threading.Lock() def worker() -> None: write_commit(repo, good) threads = [threading.Thread(target=worker) for _ in range(20)] for t in threads: t.start() for t in threads: t.join() result = read_commit(repo, good.commit_id) assert result is not None, ( "After 20 concurrent write_commit repairs, commit is still unreadable" ) assert result.snapshot_id == good.snapshot_id def test_concurrent_write_snapshot_repairs_corrupt_manifest(self, tmp_path: pathlib.Path) -> None: """20 concurrent write_snapshot calls on a corrupt file all result in a readable snapshot.""" from muse.core.store import write_snapshot, read_snapshot repo = _make_repo(tmp_path) good = _good_snapshot() _write_corrupt_snapshot(repo, good, {}) # empty manifest threads = [threading.Thread(target=lambda: write_snapshot(repo, good)) for _ in range(20)] for t in threads: t.start() for t in threads: t.join() result = read_snapshot(repo, good.snapshot_id) assert result is not None, ( "After 20 concurrent write_snapshot repairs, snapshot is still unreadable" ) def test_50_sequential_repairs_all_succeed(self, tmp_path: pathlib.Path) -> None: """50 different commits each with corrupt snapshot_id, all repaired.""" from muse.core.store import write_commit, read_commit for i in range(50): repo = _make_repo(tmp_path / str(i)) good = _good_commit(message=f"commit {i}", ts=f"202{i % 10}-01-01T00:00:00+00:00") _write_corrupt_commit(repo, good, {"snapshot_id": "0" * 64}) write_commit(repo, good) result = read_commit(repo, good.commit_id) assert result is not None, f"commit {i} not repaired" # ============================================================================= # 6. REGRESSION — normal write paths still work # ============================================================================= class TestRegression: def test_write_commit_new_file_works(self, tmp_path: pathlib.Path) -> None: from muse.core.store import write_commit, read_commit repo = _make_repo(tmp_path) good = _good_commit() write_commit(repo, good) assert read_commit(repo, good.commit_id) is not None def test_write_snapshot_new_file_works(self, tmp_path: pathlib.Path) -> None: from muse.core.store import write_snapshot, read_snapshot repo = _make_repo(tmp_path) good = _good_snapshot() write_snapshot(repo, good) assert read_snapshot(repo, good.snapshot_id) is not None def test_write_commit_idempotent_on_clean_file(self, tmp_path: pathlib.Path) -> None: from muse.core.store import write_commit, read_commit repo = _make_repo(tmp_path) good = _good_commit() write_commit(repo, good) # Write 10 times — must stay idempotent for _ in range(10): write_commit(repo, good) assert read_commit(repo, good.commit_id) is not None def test_write_snapshot_idempotent_on_clean_file(self, tmp_path: pathlib.Path) -> None: from muse.core.store import write_snapshot, read_snapshot repo = _make_repo(tmp_path) good = _good_snapshot() write_snapshot(repo, good) for _ in range(10): write_snapshot(repo, good) assert read_snapshot(repo, good.snapshot_id) is not None