"""Tests for Bug 13: apply_mpack propagates unhandled OSError from write_commit. Root cause: apply_mpack's commit loop catches (KeyError, ValueError, TypeError) but NOT OSError. When write_commit raises OSError, apply_mpack must log CRITICAL and continue rather than crashing the entire pull/push/clone call stack. These tests use mock injection to force the OSError path, which is the correct approach now that commits live in the unified object store (not msgpack files). """ from __future__ import annotations import datetime import logging import pathlib from unittest.mock import patch import pytest from muse.core.types import Manifest, fake_id from muse.core.ids import hash_commit, hash_snapshot from muse.core.mpack import MPack, apply_mpack from muse.core.commits import ( CommitDict, CommitRecord, read_commit, ) from muse.core.paths import muse_dir _TS = datetime.datetime(2024, 6, 15, 10, 0, 0, tzinfo=datetime.timezone.utc) def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path: repo = tmp_path / "repo" repo.mkdir() dot = muse_dir(repo) dot.mkdir() (dot / "objects").mkdir() (dot / "refs" / "heads").mkdir(parents=True) (dot / "HEAD").write_text("ref: refs/heads/main\n") return repo def _manifest_for(message: str) -> Manifest: return {f"{message}.py": fake_id("obj")} def _make_commit_record(message: str, parent: str | None = None) -> CommitRecord: manifest = _manifest_for(message) snap_id = hash_snapshot(manifest) parent_ids = [parent] if parent else [] cid = hash_commit( parent_ids=parent_ids, snapshot_id=snap_id, message=message, committed_at_iso=_TS.isoformat(), author="tester", ) return CommitRecord( commit_id=cid, branch="main", snapshot_id=snap_id, message=message, committed_at=_TS, author="tester", parent_commit_id=parent, parent2_commit_id=None, ) def _mpack_with_commits(commits: list[CommitRecord]) -> MPack: # Include the snapshot for each commit so apply_mpack's missing-snapshot # guard passes and commits reach the write_commit call (where OSError mocks fire). snapshots = [ { "snapshot_id": c.snapshot_id, "parent_snapshot_id": None, "delta_upsert": _manifest_for(c.message), "delta_remove": [], "directories": [], } for c in commits ] return MPack(blobs=[], snapshots=snapshots, commits=[c.to_dict() for c in commits], tags=[]) # ────────────────────────────────────────────────────────────────────────────── # Bug 13: unhandled OSError from write_commit # ────────────────────────────────────────────────────────────────────────────── class TestApplyMPackOsErrorIntegrity: def test_apply_mpack_does_not_crash_on_store_integrity_violation(self, tmp_path: pathlib.Path) -> None: """Bug 13: apply_mpack must not propagate OSError from write_commit.""" repo = _make_repo(tmp_path) c = _make_commit_record("good-commit") mpack = _mpack_with_commits([c]) with patch("muse.core.mpack.write_commit", side_effect=OSError("Store integrity violation")): result = apply_mpack(repo, mpack) # must not raise assert result is not None def test_apply_mpack_logs_critical_on_store_integrity_violation( self, tmp_path: pathlib.Path, caplog: pytest.LogCaptureFixture ) -> None: """apply_mpack must log CRITICAL (not swallow silently) when OSError is raised.""" repo = _make_repo(tmp_path) c = _make_commit_record("critical-log-commit") mpack = _mpack_with_commits([c]) with caplog.at_level(logging.CRITICAL, logger="muse.core.mpack"): with patch("muse.core.mpack.write_commit", side_effect=OSError("Store integrity violation")): apply_mpack(repo, mpack) crits = [r for r in caplog.records if r.levelno >= logging.CRITICAL] assert crits, "apply_mpack must log CRITICAL when write_commit raises OSError" assert any("integrity" in r.message.lower() or "violation" in r.message.lower() for r in crits) def test_apply_mpack_continues_after_integrity_violation(self, tmp_path: pathlib.Path) -> None: """Remaining commits must be processed after an integrity-violation skip.""" repo = _make_repo(tmp_path) bad = _make_commit_record("bad-commit") good = _make_commit_record("good-commit-after") mpack = _mpack_with_commits([bad, good]) call_count = 0 original_write_commit = None def _write_commit_side_effect(repo_root: pathlib.Path, commit: CommitRecord, **kwargs: str) -> None: nonlocal call_count call_count += 1 if commit.commit_id == bad.commit_id: raise OSError("Store integrity violation") from muse.core.commits import write_commit as _wc _wc(repo_root, commit, **kwargs) with patch("muse.core.mpack.write_commit", side_effect=_write_commit_side_effect): apply_mpack(repo, mpack) assert read_commit(repo, good.commit_id) is not None, ( "apply_mpack must continue processing commits after an integrity violation" ) def test_apply_mpack_commits_written_excludes_integrity_violation(self, tmp_path: pathlib.Path) -> None: """commits_written must not include commits that triggered OSError.""" repo = _make_repo(tmp_path) bad = _make_commit_record("integrity-violation") good = _make_commit_record("normal-commit") mpack = _mpack_with_commits([bad, good]) def _write_commit_side_effect(repo_root: pathlib.Path, commit: CommitRecord, **kwargs: str) -> None: if commit.commit_id == bad.commit_id: raise OSError("Store integrity violation") from muse.core.commits import write_commit as _wc _wc(repo_root, commit, **kwargs) with patch("muse.core.mpack.write_commit", side_effect=_write_commit_side_effect): result = apply_mpack(repo, mpack) assert result["commits_written"] == 1, ( f"commits_written should be 1 (only good), got {result['commits_written']}" ) def test_apply_mpack_oserror_commit_not_written(self, tmp_path: pathlib.Path) -> None: """A commit that raised OSError during write must not appear in the store.""" repo = _make_repo(tmp_path) c = _make_commit_record("legit-commit") mpack = _mpack_with_commits([c]) with patch("muse.core.mpack.write_commit", side_effect=OSError("injected")): apply_mpack(repo, mpack) assert read_commit(repo, c.commit_id) is None, ( "A commit whose write_commit raised OSError must not appear in the store" ) def test_apply_mpack_valid_commit_no_integrity_violation(self, tmp_path: pathlib.Path) -> None: """Regression: valid commits must still be written normally.""" repo = _make_repo(tmp_path) c = _make_commit_record("clean-commit") mpack = _mpack_with_commits([c]) result = apply_mpack(repo, mpack) assert result["commits_written"] == 1 assert read_commit(repo, c.commit_id) is not None def test_apply_mpack_wrong_hash_raises_valuerror_not_oserror(self, tmp_path: pathlib.Path) -> None: """Commits with mismatched commit_id hash are skipped (not raised).""" repo = _make_repo(tmp_path) bad_dict = CommitDict( commit_id="sha256:" + "f" * 64, # wrong hash branch="main", snapshot_id="sha256:" + "a" * 64, message="bad", committed_at=_TS.isoformat(), parent_commit_id=None, parent2_commit_id=None, author="tester", ) mpack = MPack(blobs=[], snapshots=[], commits=[bad_dict], tags=[]) result = apply_mpack(repo, mpack) # must not raise assert result["commits_written"] == 0