"""TDD — musehub#134: wire_push_unpack_mpack must not persist an empty base when a pushed snapshot's parent is an EXTERNAL delta-only (or missing) snapshot. Root cause, in ``musehub_wire_push.py``: _psnap_rows = (await session.execute( select(MusehubSnapshot.snapshot_id, MusehubSnapshot.manifest_blob) .where(MusehubSnapshot.snapshot_id.in_(_external_parent_sids)) )).all() for _psid, _pblob in _psnap_rows: if _pblob: _parent_snap_manifests[_psid] = dict(_msgpack.unpackb(_pblob, raw=False)) ... _parent_base = _parent_snap_manifests.get(_parent_sid) or {} Only checks ``manifest_blob`` directly. A delta-only external parent (``manifest_blob=NULL``, reconstructible via ``delta_blob`` + ``parent_snapshot_id``) is silently dropped, and the child's delta is applied on top of ``{}`` — a manifest that is missing every inherited file and can never hash back to its own snapshot_id. This test calls ``wire_push_unpack_mpack`` DIRECTLY with a hand-built wire mpack that contains ONLY the new commit (its parent snapshot is genuinely absent from the payload) — this is the only reliable way to force the external-parent path. A CLI-level ``muse push`` integration test (see ``test_push_delta_only_parent_manifest.py``) cannot force it: ``muse push``'s ``have`` negotiation is a literal-branch-tip match with no ancestor-closure expansion (``muse/core/mpack.py::walk_commits`` — ``prune=lambda cid: cid in have_set``), so a real second push naturally resends the whole chain back to the nearest branch tip, making the "external" parent an in-payload sibling instead — which is exactly why that existing test passes without exercising this bug. RED before the fix (D's stored manifest drops f1/f2, inherited from delta-only B). GREEN after (parent reconstructed from its delta chain before applying D's delta). """ from __future__ import annotations import datetime from unittest.mock import AsyncMock, MagicMock, patch import msgpack import pytest from sqlalchemy.ext.asyncio import AsyncSession from muse.core.ids import hash_snapshot from muse.core.mpack import build_wire_mpack from muse.core.types import blob_id from musehub.core.genesis import compute_identity_id from musehub.db.musehub_repo_models import ( MusehubCommit, MusehubCommitGraph, MusehubSnapshot, MusehubSnapshotRef, ) from musehub.services.musehub_repository import create_repo from musehub.services.musehub_wire_push import wire_push_unpack_mpack _OWNER = "gabriel" _IDENTITY_ID = compute_identity_id(b"gabriel") def _cid(seed: str) -> str: return blob_id(f"ext-parent-commit-{seed}".encode()) def _now() -> datetime.datetime: return datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc) def _raw_commit(cid: str, parent: str | None, snap_id: str, *, branch: str = "feat") -> dict: return { "commit_id": cid, "branch": branch, "message": f"commit {cid[:12]}", "author": _OWNER, "committed_at": _now().isoformat(), "parent_commit_id": parent, "parent2_commit_id": None, "snapshot_id": snap_id, "agent_id": "", "model_id": "", "toolchain_id": "", "sem_ver_bump": "none", "breaking_changes": [], "signature": "", "signer_key_id": "", "signer_public_key": "", "prompt_hash": "", } def _mock_backend(mpack_bytes: bytes) -> MagicMock: backend = MagicMock() backend.get_mpack = AsyncMock(return_value=mpack_bytes) backend.put = AsyncMock(return_value=None) backend.put_mpack = AsyncMock(return_value=None) backend.quarantine_mpack = AsyncMock(return_value=None) backend.presign_get = AsyncMock(return_value="") backend.presign_mpack_get = AsyncMock(return_value="") return backend @pytest.mark.asyncio async def test_child_of_external_delta_only_parent_reconstructs_full_manifest( db_session: AsyncSession, ) -> None: """RED: D's parent B is delta-only and NOT included in D's push payload. Seeded directly (simulating an earlier, separate push that already landed A as root/full-manifest and B as a middle/delta-only snapshot — exactly what a real ``muse push`` of A->B->C produces): A: root, full manifest {f1.txt} B: delta-only, manifest_blob=NULL, delta_blob={+f2.txt}, parent=A Then push ONLY commit D (parent=B, delta={+f4.txt}) — B is genuinely external: absent from this push's own commits/snapshots list entirely. """ repo = await create_repo( db_session, name="ext-parent-repro", owner=_OWNER, owner_user_id=_IDENTITY_ID, visibility="public", initialize=False, ) await db_session.commit() oid1 = blob_id(b"content-f1") oid2 = blob_id(b"content-f2") oid4 = blob_id(b"content-f4") manifest_a = {"f1.txt": oid1} manifest_b = {"f1.txt": oid1, "f2.txt": oid2} snap_a = hash_snapshot(manifest_a) snap_b = hash_snapshot(manifest_b) # Seed A: root, full manifest (exactly what a real push stores for a batch root). db_session.add(MusehubSnapshot( snapshot_id=snap_a, directories=[], manifest_blob=msgpack.packb(manifest_a, use_bin_type=True), entry_count=len(manifest_a), created_at=_now(), parent_snapshot_id=None, delta_blob=None, )) # Seed B: delta-only middle snapshot — manifest_blob=NULL, exactly what a real # push stores for a non-root/non-head snapshot. db_session.add(MusehubSnapshot( snapshot_id=snap_b, directories=[], manifest_blob=None, entry_count=len(manifest_b), created_at=_now(), parent_snapshot_id=snap_a, delta_blob=msgpack.packb({"add": {"f2.txt": oid2}}, use_bin_type=True), )) db_session.add(MusehubSnapshotRef(repo_id=repo.repo_id, snapshot_id=snap_a)) db_session.add(MusehubSnapshotRef(repo_id=repo.repo_id, snapshot_id=snap_b)) # Seed A/B's musehub_commits rows (authoritative DAG) + A's commit_graph row # (anchor, generation 0) — exactly what an earlier real push of A->B already # landed on the server before this test's push of D. a_commit = _cid("a") b_commit = _cid("b") db_session.add(MusehubCommit( commit_id=a_commit, branch="feat", message="A", author=_OWNER, timestamp=_now(), parent_ids=[], snapshot_id=snap_a, )) db_session.add(MusehubCommit( commit_id=b_commit, branch="feat", message="B", author=_OWNER, timestamp=_now(), parent_ids=[a_commit], snapshot_id=snap_b, )) db_session.add(MusehubCommitGraph( commit_id=a_commit, parent_ids=[], generation=0, snapshot_id=snap_a, created_at=_now(), )) await db_session.commit() d_commit = _cid("d") manifest_d = {"f1.txt": oid1, "f2.txt": oid2, "f4.txt": oid4} snap_d = hash_snapshot(manifest_d) # The push payload for D ONLY — B is genuinely absent, exactly as a real # incremental push would send once `have` correctly recognizes B is already # on the remote (see module docstring for why muse push's CLI negotiation # can't reliably force this in an integration test). mpack_bytes = build_wire_mpack({ "commits": [_raw_commit(d_commit, b_commit, snap_d)], "snapshots": [{ "snapshot_id": snap_d, "parent_snapshot_id": snap_b, "delta_upsert": {"f4.txt": oid4}, "delta_remove": [], "directories": [], }], "blobs": [{"object_id": oid4, "content": b"content-f4"}], "tags": [], }) mpack_key = blob_id(mpack_bytes) backend = _mock_backend(mpack_bytes) with patch("musehub.services.musehub_wire.get_backend", return_value=backend), \ patch("musehub.services.musehub_wire_push.get_backend", return_value=backend), \ patch("musehub.storage.backends.get_backend", return_value=backend): await wire_push_unpack_mpack( db_session, repo.repo_id, mpack_key, pusher_id=_OWNER, branch="feat", head_commit_id=d_commit, commits_count=1, blobs_count=1, force=True, ) await db_session.flush() stored = await db_session.get(MusehubSnapshot, snap_d) assert stored is not None, "D's snapshot row was not written at all" assert stored.manifest_blob is not None, "D is the head of its own push batch — must get a full manifest_blob" stored_manifest = dict(msgpack.unpackb(stored.manifest_blob, raw=False)) must_have = {"f1.txt", "f2.txt", "f4.txt"} assert must_have <= set(stored_manifest), ( f"D's stored manifest dropped files inherited from the external delta-only " f"parent B (base resolved to empty instead of being reconstructed).\n" f" stored = {sorted(stored_manifest)}\n" f" must_have = {sorted(must_have)}\n" f" missing = {sorted(must_have - set(stored_manifest))}" ) assert hash_snapshot(stored_manifest, list(stored.directories or [])) == snap_d, ( f"stored manifest does not reproduce snapshot_id {snap_d[:18]} — " f"the exact corruption reproduced live on staging.musehub.ai/gabriel/musehub" ) @pytest.mark.asyncio async def test_push_rejected_when_external_parent_is_a_total_phantom( db_session: AsyncSession, ) -> None: """RED: D's parent snapshot has NO row at all — not delta-only, genuinely absent (the exact state found on staging: a snapshot_id referenced as a parent with no backing musehub_snapshots row and no musehub_commits row either). Reconstruction cannot recover any content. The push must be REJECTED, not silently persisted with a manifest that will never hash-verify again. """ repo = await create_repo( db_session, name="phantom-parent-repro", owner=_OWNER, owner_user_id=_IDENTITY_ID, visibility="public", initialize=False, ) await db_session.commit() oid4 = blob_id(b"content-f4-phantom") phantom_snap_id = hash_snapshot({"never.txt": blob_id(b"never-existed")}) manifest_d = {"never.txt": blob_id(b"never-existed"), "f4.txt": oid4} snap_d = hash_snapshot(manifest_d) d_commit = _cid("phantom-d") mpack_bytes = build_wire_mpack({ "commits": [_raw_commit(d_commit, _cid("phantom-parent-commit"), snap_d)], "snapshots": [{ "snapshot_id": snap_d, "parent_snapshot_id": phantom_snap_id, "delta_upsert": {"f4.txt": oid4}, "delta_remove": [], "directories": [], }], "blobs": [{"object_id": oid4, "content": b"content-f4-phantom"}], "tags": [], }) mpack_key = blob_id(mpack_bytes) backend = _mock_backend(mpack_bytes) with patch("musehub.services.musehub_wire.get_backend", return_value=backend), \ patch("musehub.services.musehub_wire_push.get_backend", return_value=backend), \ patch("musehub.storage.backends.get_backend", return_value=backend): with pytest.raises(ValueError, match="cannot be reconstructed"): await wire_push_unpack_mpack( db_session, repo.repo_id, mpack_key, pusher_id=_OWNER, branch="feat", head_commit_id=d_commit, commits_count=1, blobs_count=1, force=True, ) stored = await db_session.get(MusehubSnapshot, snap_d) assert stored is None, "a snapshot that can never be verified must never be persisted"