"""TDD -- musehub#220: create_repo(initialize=True) seeded every new repo with a structurally invalid genesis commit -- no snapshot_id, no MusehubSnapshot row, and (found during investigation, same ticket) a commit_id computed by a hand-rolled hash that doesn't match what a real client recomputes via hash_commit() on clone/fetch. The second bug is in commit_files_to_repo itself -- shared by org creation, org membership changes, and identity-repo creation at registration. Every commit ever created through it would fail content-hash verification ("Commit ... failed content-hash verification") the moment any real client tried to store it locally (clone, pull). Fixed at the source so every caller benefits, not just create_repo. Test IDs: RC_01 -- create_repo(initialize=True) commit has a non-empty snapshot_id RC_02 -- that snapshot_id resolves to a real MusehubSnapshot row (empty manifest) RC_03 -- the commit_id is exactly what a real client's hash_commit() recomputes from the stored fields (the actual _verify_commit_id check, reproduced) RC_04 -- commit_files_to_repo's own commit_id (independent of create_repo) also passes the same recomputation check -- the shared root-cause fix RC_05 -- create_repo(initialize=False) still creates zero commits (unchanged) """ from __future__ import annotations import datetime import pytest from sqlalchemy.ext.asyncio import AsyncSession from muse.core.ids import hash_commit from musehub.core.genesis import compute_identity_id from musehub.db.musehub_repo_models import MusehubCommit, MusehubCommitRef, MusehubSnapshot from musehub.services.musehub_repository import create_repo from musehub.services.musehub_sync import commit_files_to_repo pytestmark = pytest.mark.asyncio _OWNER = "gabriel" _IDENTITY_ID = compute_identity_id(b"gabriel") _COUNTER: list[int] = [0] def _uid(tag: str = "") -> str: _COUNTER[0] += 1 return f"rc{tag}{_COUNTER[0]}" def _recompute_commit_id(commit: MusehubCommit) -> str: """Exactly reproduces muse.core.commits._verify_commit_id's recomputation.""" return hash_commit( parent_ids=list(commit.parent_ids or []), snapshot_id=commit.snapshot_id or "", message=commit.message, committed_at_iso=commit.committed_at_raw, author=commit.author or "", signer_public_key=getattr(commit, "signer_public_key", "") or "", ) async def test_rc01_initialize_true_commit_has_nonempty_snapshot_id( db_session: AsyncSession, ) -> None: repo = await create_repo( db_session, name=_uid("name"), owner=_OWNER, owner_user_id=_IDENTITY_ID, visibility="private", initialize=True, ) await db_session.commit() head_ref = (await db_session.execute( MusehubCommitRef.__table__.select().where(MusehubCommitRef.repo_id == repo.repo_id) )).first() assert head_ref is not None, "initialize=True must create a commit" commit = await db_session.get(MusehubCommit, head_ref.commit_id) assert commit is not None assert commit.snapshot_id, "snapshot_id must not be empty -- musehub#220" async def test_rc02_snapshot_id_resolves_to_real_snapshot_row( db_session: AsyncSession, ) -> None: repo = await create_repo( db_session, name=_uid("name"), owner=_OWNER, owner_user_id=_IDENTITY_ID, visibility="private", initialize=True, ) await db_session.commit() head_ref = (await db_session.execute( MusehubCommitRef.__table__.select().where(MusehubCommitRef.repo_id == repo.repo_id) )).first() commit = await db_session.get(MusehubCommit, head_ref.commit_id) snap = await db_session.get(MusehubSnapshot, commit.snapshot_id) assert snap is not None, ( "the commit's snapshot_id must resolve to a real MusehubSnapshot row" ) assert snap.entry_count == 0 async def test_rc03_initialize_true_commit_id_is_client_verifiable( db_session: AsyncSession, ) -> None: """Reproduces the exact check muse.core.commits._verify_commit_id performs on every write_commit call (including clone/pull) -- the real bug behind 'apply_mpack: malformed commit' / 'failed content-hash verification'.""" repo = await create_repo( db_session, name=_uid("name"), owner=_OWNER, owner_user_id=_IDENTITY_ID, visibility="private", initialize=True, ) await db_session.commit() head_ref = (await db_session.execute( MusehubCommitRef.__table__.select().where(MusehubCommitRef.repo_id == repo.repo_id) )).first() commit = await db_session.get(MusehubCommit, head_ref.commit_id) recomputed = _recompute_commit_id(commit) assert recomputed == commit.commit_id, ( f"commit_id is not client-verifiable -- a real `muse clone` would reject " f"this commit with 'failed content-hash verification'. " f"stored={commit.commit_id!r} recomputed={recomputed!r}" ) async def test_rc04_commit_files_to_repo_produces_client_verifiable_commit_id( db_session: AsyncSession, ) -> None: """The shared root-cause fix: commit_files_to_repo (used independently by org creation, membership changes, and identity-repo creation) must itself produce a client-verifiable commit_id, not just when called via create_repo.""" repo = await create_repo( db_session, name=_uid("name"), owner=_OWNER, owner_user_id=_IDENTITY_ID, visibility="private", initialize=False, ) await db_session.commit() commit_id = await commit_files_to_repo( db_session, repo_id=repo.repo_id, branch="main", files={"a.txt": b"hello"}, message="test commit", author=_IDENTITY_ID, ) await db_session.commit() commit = await db_session.get(MusehubCommit, commit_id) assert commit is not None recomputed = _recompute_commit_id(commit) assert recomputed == commit.commit_id, ( f"commit_files_to_repo's commit_id is not client-verifiable. " f"stored={commit.commit_id!r} recomputed={recomputed!r}" ) async def test_rc05_initialize_false_creates_no_commit(db_session: AsyncSession) -> None: repo = await create_repo( db_session, name=_uid("name"), owner=_OWNER, owner_user_id=_IDENTITY_ID, visibility="private", initialize=False, ) await db_session.commit() head_ref = (await db_session.execute( MusehubCommitRef.__table__.select().where(MusehubCommitRef.repo_id == repo.repo_id) )).first() assert head_ref is None, "initialize=False must not create any commit"