test_wire_push_root_snapshot_integrity.py
python
sha256:7281683f5c41e5d88b6d8811fbdafebd3e01a0c9dcd90975cfcb444ba71e8e81
docs: add local source-of-truth for musehub#225, #226, #227…
Sonnet 5
1 day ago
| 1 | """TDD — musehub#93: root-of-push snapshots were never hash-verified before storage. |
| 2 | |
| 3 | Root cause, in ``musehub_wire_push.py``'s snapshot-processing loop:: |
| 4 | |
| 5 | if _parent_sid and hash_snapshot(_base, _snap_dirs or None) != _sid: |
| 6 | raise ValueError(...) |
| 7 | |
| 8 | This integrity check only ran when ``_parent_sid`` was truthy. Every snapshot |
| 9 | that is the *root* of a push batch (``parent_snapshot_id=None`` — the first |
| 10 | commit sent, or a genuinely new repo's first commit) has ``_parent_sid`` falsy |
| 11 | and was therefore never checked against its own declared ID at all. A wire/ |
| 12 | transport bug that dropped or mangled ``directories`` (or, in principle, the |
| 13 | manifest itself) for exactly this kind of entry would sail through unverified |
| 14 | and be persisted permanently -- undetectable until a client later re-hashed it |
| 15 | on clone and rejected the mismatch (the exact `entries=1062 dirs=0` corruption |
| 16 | found live on `gabriel/muse`@staging that prompted this issue). |
| 17 | |
| 18 | RED before the fix: a root snapshot with directories that don't reproduce its |
| 19 | own declared snapshot_id is silently stored with dirs=0. |
| 20 | GREEN after: the push is rejected outright, matching the existing (and |
| 21 | unaffected) behavior for a snapshot with a bad/phantom parent. |
| 22 | """ |
| 23 | from __future__ import annotations |
| 24 | |
| 25 | import datetime |
| 26 | from unittest.mock import AsyncMock, MagicMock, patch |
| 27 | |
| 28 | import msgpack |
| 29 | import pytest |
| 30 | from sqlalchemy.ext.asyncio import AsyncSession |
| 31 | |
| 32 | from muse.core.ids import hash_snapshot |
| 33 | from muse.core.mpack import build_wire_mpack |
| 34 | from muse.core.types import blob_id |
| 35 | from musehub.core.genesis import compute_identity_id |
| 36 | from musehub.db.musehub_repo_models import MusehubSnapshot |
| 37 | from musehub.services.musehub_repository import create_repo |
| 38 | from musehub.services.musehub_wire_push import wire_push_unpack_mpack |
| 39 | |
| 40 | _OWNER = "gabriel" |
| 41 | _IDENTITY_ID = compute_identity_id(b"gabriel") |
| 42 | |
| 43 | |
| 44 | def _cid(seed: str) -> str: |
| 45 | return blob_id(f"root-integrity-commit-{seed}".encode()) |
| 46 | |
| 47 | |
| 48 | def _now() -> datetime.datetime: |
| 49 | return datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc) |
| 50 | |
| 51 | |
| 52 | def _raw_commit(cid: str, snap_id: str) -> dict: |
| 53 | return { |
| 54 | "commit_id": cid, |
| 55 | "branch": "feat", |
| 56 | "message": f"commit {cid[:12]}", |
| 57 | "author": _OWNER, |
| 58 | "committed_at": _now().isoformat(), |
| 59 | "parent_commit_id": None, |
| 60 | "parent2_commit_id": None, |
| 61 | "snapshot_id": snap_id, |
| 62 | "agent_id": "", |
| 63 | "model_id": "", |
| 64 | "toolchain_id": "", |
| 65 | "sem_ver_bump": "none", |
| 66 | "breaking_changes": [], |
| 67 | "signature": "", |
| 68 | "signer_key_id": "", |
| 69 | "signer_public_key": "", |
| 70 | "prompt_hash": "", |
| 71 | } |
| 72 | |
| 73 | |
| 74 | def _mock_backend(mpack_bytes: bytes) -> MagicMock: |
| 75 | backend = MagicMock() |
| 76 | backend.get_mpack = AsyncMock(return_value=mpack_bytes) |
| 77 | backend.put = AsyncMock(return_value=None) |
| 78 | backend.put_mpack = AsyncMock(return_value=None) |
| 79 | backend.quarantine_mpack = AsyncMock(return_value=None) |
| 80 | backend.presign_get = AsyncMock(return_value="") |
| 81 | backend.presign_mpack_get = AsyncMock(return_value="") |
| 82 | return backend |
| 83 | |
| 84 | |
| 85 | @pytest.mark.asyncio |
| 86 | async def test_root_snapshot_with_wrong_directories_is_rejected_not_silently_stored( |
| 87 | db_session: AsyncSession, |
| 88 | ) -> None: |
| 89 | """RED: a root (parent_snapshot_id=None) snapshot's declared directories |
| 90 | don't reproduce its own snapshot_id. Must be rejected outright -- exactly |
| 91 | like the existing phantom-parent case -- not persisted with the wrong |
| 92 | (or dropped) directories.""" |
| 93 | repo = await create_repo( |
| 94 | db_session, |
| 95 | name="root-integrity-repro", |
| 96 | owner=_OWNER, |
| 97 | owner_user_id=_IDENTITY_ID, |
| 98 | visibility="public", |
| 99 | initialize=False, |
| 100 | ) |
| 101 | await db_session.commit() |
| 102 | |
| 103 | oid1 = blob_id(b"content-f1") |
| 104 | manifest = {"tracks/f1.txt": oid1} |
| 105 | real_directories = ["tracks", "docs"] |
| 106 | # The snapshot_id was genuinely computed WITH these directories locally -- |
| 107 | # this is what a correct client push looks like. |
| 108 | snap_id = hash_snapshot(manifest, real_directories) |
| 109 | |
| 110 | a_commit = _cid("a") |
| 111 | mpack_bytes = build_wire_mpack({ |
| 112 | "commits": [_raw_commit(a_commit, snap_id)], |
| 113 | "snapshots": [{ |
| 114 | "snapshot_id": snap_id, |
| 115 | "parent_snapshot_id": None, |
| 116 | "delta_upsert": manifest, |
| 117 | "delta_remove": [], |
| 118 | # Wire/transport bug simulation: directories arrived wrong (here: |
| 119 | # dropped entirely) relative to what snap_id was actually hashed |
| 120 | # with. This is exactly the shape of musehub#93's live corruption. |
| 121 | "directories": [], |
| 122 | }], |
| 123 | "blobs": [{"object_id": oid1, "content": b"content-f1"}], |
| 124 | "tags": [], |
| 125 | }) |
| 126 | mpack_key = blob_id(mpack_bytes) |
| 127 | backend = _mock_backend(mpack_bytes) |
| 128 | |
| 129 | with patch("musehub.services.musehub_wire.get_backend", return_value=backend), \ |
| 130 | patch("musehub.services.musehub_wire_push.get_backend", return_value=backend), \ |
| 131 | patch("musehub.storage.backends.get_backend", return_value=backend): |
| 132 | with pytest.raises(ValueError, match="does not reproduce its own declared ID"): |
| 133 | await wire_push_unpack_mpack( |
| 134 | db_session, |
| 135 | repo.repo_id, |
| 136 | mpack_key, |
| 137 | pusher_id=_OWNER, |
| 138 | branch="feat", |
| 139 | head_commit_id=a_commit, |
| 140 | commits_count=1, |
| 141 | blobs_count=1, |
| 142 | force=True, |
| 143 | ) |
| 144 | |
| 145 | stored = await db_session.get(MusehubSnapshot, snap_id) |
| 146 | assert stored is None, ( |
| 147 | "a root snapshot whose directories don't reproduce its own declared ID " |
| 148 | "must never be persisted -- this is exactly the musehub#93 corruption " |
| 149 | "(dirs silently dropped, entry_count correct) sailing through unverified" |
| 150 | ) |
| 151 | |
| 152 | |
| 153 | @pytest.mark.asyncio |
| 154 | async def test_root_snapshot_with_correct_directories_is_stored_intact( |
| 155 | db_session: AsyncSession, |
| 156 | ) -> None: |
| 157 | """GREEN control: a correctly-hashed root snapshot with real directories |
| 158 | must still push successfully and be stored with those directories intact -- |
| 159 | the new unconditional check must not reject legitimate pushes.""" |
| 160 | repo = await create_repo( |
| 161 | db_session, |
| 162 | name="root-integrity-happy-path", |
| 163 | owner=_OWNER, |
| 164 | owner_user_id=_IDENTITY_ID, |
| 165 | visibility="public", |
| 166 | initialize=False, |
| 167 | ) |
| 168 | await db_session.commit() |
| 169 | |
| 170 | oid1 = blob_id(b"content-f1-happy") |
| 171 | manifest = {"tracks/f1.txt": oid1} |
| 172 | real_directories = ["tracks", "docs", "empty_dir"] |
| 173 | snap_id = hash_snapshot(manifest, real_directories) |
| 174 | |
| 175 | a_commit = _cid("happy-a") |
| 176 | mpack_bytes = build_wire_mpack({ |
| 177 | "commits": [_raw_commit(a_commit, snap_id)], |
| 178 | "snapshots": [{ |
| 179 | "snapshot_id": snap_id, |
| 180 | "parent_snapshot_id": None, |
| 181 | "delta_upsert": manifest, |
| 182 | "delta_remove": [], |
| 183 | "directories": real_directories, |
| 184 | }], |
| 185 | "blobs": [{"object_id": oid1, "content": b"content-f1-happy"}], |
| 186 | "tags": [], |
| 187 | }) |
| 188 | mpack_key = blob_id(mpack_bytes) |
| 189 | backend = _mock_backend(mpack_bytes) |
| 190 | |
| 191 | with patch("musehub.services.musehub_wire.get_backend", return_value=backend), \ |
| 192 | patch("musehub.services.musehub_wire_push.get_backend", return_value=backend), \ |
| 193 | patch("musehub.storage.backends.get_backend", return_value=backend): |
| 194 | await wire_push_unpack_mpack( |
| 195 | db_session, |
| 196 | repo.repo_id, |
| 197 | mpack_key, |
| 198 | pusher_id=_OWNER, |
| 199 | branch="feat", |
| 200 | head_commit_id=a_commit, |
| 201 | commits_count=1, |
| 202 | blobs_count=1, |
| 203 | force=True, |
| 204 | ) |
| 205 | await db_session.flush() |
| 206 | |
| 207 | stored = await db_session.get(MusehubSnapshot, snap_id) |
| 208 | assert stored is not None, "a correctly-hashed root snapshot must be stored" |
| 209 | assert sorted(stored.directories or []) == sorted(real_directories), ( |
| 210 | f"stored directories don't match what was pushed: " |
| 211 | f"stored={stored.directories!r} pushed={real_directories!r}" |
| 212 | ) |
| 213 | assert hash_snapshot(manifest, list(stored.directories or [])) == snap_id |
File History
1 commit
sha256:7281683f5c41e5d88b6d8811fbdafebd3e01a0c9dcd90975cfcb444ba71e8e81
docs: add local source-of-truth for musehub#225, #226, #227…
Sonnet 5
1 day ago