test_repo_init_commit_cloneable.py
python
sha256:5dbc6b75b9add84f8812f9f390fbd5b47fcf9e22947b2cf86ae54146c99c9df8
fix: create_repo seeds invalid genesis commit; commit_files…
Sonnet 5
patch
2 days ago
| 1 | """TDD -- musehub#220: create_repo(initialize=True) seeded every new repo |
| 2 | with a structurally invalid genesis commit -- no snapshot_id, no |
| 3 | MusehubSnapshot row, and (found during investigation, same ticket) a |
| 4 | commit_id computed by a hand-rolled hash that doesn't match what a real |
| 5 | client recomputes via hash_commit() on clone/fetch. |
| 6 | |
| 7 | The second bug is in commit_files_to_repo itself -- shared by org |
| 8 | creation, org membership changes, and identity-repo creation at |
| 9 | registration. Every commit ever created through it would fail content-hash |
| 10 | verification ("Commit ... failed content-hash verification") the moment |
| 11 | any real client tried to store it locally (clone, pull). Fixed at the |
| 12 | source so every caller benefits, not just create_repo. |
| 13 | |
| 14 | Test IDs: |
| 15 | RC_01 -- create_repo(initialize=True) commit has a non-empty snapshot_id |
| 16 | RC_02 -- that snapshot_id resolves to a real MusehubSnapshot row (empty manifest) |
| 17 | RC_03 -- the commit_id is exactly what a real client's hash_commit() recomputes |
| 18 | from the stored fields (the actual _verify_commit_id check, reproduced) |
| 19 | RC_04 -- commit_files_to_repo's own commit_id (independent of create_repo) |
| 20 | also passes the same recomputation check -- the shared root-cause fix |
| 21 | RC_05 -- create_repo(initialize=False) still creates zero commits (unchanged) |
| 22 | """ |
| 23 | from __future__ import annotations |
| 24 | |
| 25 | import datetime |
| 26 | |
| 27 | import pytest |
| 28 | from sqlalchemy.ext.asyncio import AsyncSession |
| 29 | |
| 30 | from muse.core.ids import hash_commit |
| 31 | from musehub.core.genesis import compute_identity_id |
| 32 | from musehub.db.musehub_repo_models import MusehubCommit, MusehubCommitRef, MusehubSnapshot |
| 33 | from musehub.services.musehub_repository import create_repo |
| 34 | from musehub.services.musehub_sync import commit_files_to_repo |
| 35 | |
| 36 | pytestmark = pytest.mark.asyncio |
| 37 | |
| 38 | _OWNER = "gabriel" |
| 39 | _IDENTITY_ID = compute_identity_id(b"gabriel") |
| 40 | _COUNTER: list[int] = [0] |
| 41 | |
| 42 | |
| 43 | def _uid(tag: str = "") -> str: |
| 44 | _COUNTER[0] += 1 |
| 45 | return f"rc{tag}{_COUNTER[0]}" |
| 46 | |
| 47 | |
| 48 | def _recompute_commit_id(commit: MusehubCommit) -> str: |
| 49 | """Exactly reproduces muse.core.commits._verify_commit_id's recomputation.""" |
| 50 | return hash_commit( |
| 51 | parent_ids=list(commit.parent_ids or []), |
| 52 | snapshot_id=commit.snapshot_id or "", |
| 53 | message=commit.message, |
| 54 | committed_at_iso=commit.committed_at_raw, |
| 55 | author=commit.author or "", |
| 56 | signer_public_key=getattr(commit, "signer_public_key", "") or "", |
| 57 | ) |
| 58 | |
| 59 | |
| 60 | async def test_rc01_initialize_true_commit_has_nonempty_snapshot_id( |
| 61 | db_session: AsyncSession, |
| 62 | ) -> None: |
| 63 | repo = await create_repo( |
| 64 | db_session, |
| 65 | name=_uid("name"), |
| 66 | owner=_OWNER, |
| 67 | owner_user_id=_IDENTITY_ID, |
| 68 | visibility="private", |
| 69 | initialize=True, |
| 70 | ) |
| 71 | await db_session.commit() |
| 72 | |
| 73 | head_ref = (await db_session.execute( |
| 74 | MusehubCommitRef.__table__.select().where(MusehubCommitRef.repo_id == repo.repo_id) |
| 75 | )).first() |
| 76 | assert head_ref is not None, "initialize=True must create a commit" |
| 77 | commit = await db_session.get(MusehubCommit, head_ref.commit_id) |
| 78 | assert commit is not None |
| 79 | assert commit.snapshot_id, "snapshot_id must not be empty -- musehub#220" |
| 80 | |
| 81 | |
| 82 | async def test_rc02_snapshot_id_resolves_to_real_snapshot_row( |
| 83 | db_session: AsyncSession, |
| 84 | ) -> None: |
| 85 | repo = await create_repo( |
| 86 | db_session, |
| 87 | name=_uid("name"), |
| 88 | owner=_OWNER, |
| 89 | owner_user_id=_IDENTITY_ID, |
| 90 | visibility="private", |
| 91 | initialize=True, |
| 92 | ) |
| 93 | await db_session.commit() |
| 94 | |
| 95 | head_ref = (await db_session.execute( |
| 96 | MusehubCommitRef.__table__.select().where(MusehubCommitRef.repo_id == repo.repo_id) |
| 97 | )).first() |
| 98 | commit = await db_session.get(MusehubCommit, head_ref.commit_id) |
| 99 | snap = await db_session.get(MusehubSnapshot, commit.snapshot_id) |
| 100 | assert snap is not None, ( |
| 101 | "the commit's snapshot_id must resolve to a real MusehubSnapshot row" |
| 102 | ) |
| 103 | assert snap.entry_count == 0 |
| 104 | |
| 105 | |
| 106 | async def test_rc03_initialize_true_commit_id_is_client_verifiable( |
| 107 | db_session: AsyncSession, |
| 108 | ) -> None: |
| 109 | """Reproduces the exact check muse.core.commits._verify_commit_id performs |
| 110 | on every write_commit call (including clone/pull) -- the real bug behind |
| 111 | 'apply_mpack: malformed commit' / 'failed content-hash verification'.""" |
| 112 | repo = await create_repo( |
| 113 | db_session, |
| 114 | name=_uid("name"), |
| 115 | owner=_OWNER, |
| 116 | owner_user_id=_IDENTITY_ID, |
| 117 | visibility="private", |
| 118 | initialize=True, |
| 119 | ) |
| 120 | await db_session.commit() |
| 121 | |
| 122 | head_ref = (await db_session.execute( |
| 123 | MusehubCommitRef.__table__.select().where(MusehubCommitRef.repo_id == repo.repo_id) |
| 124 | )).first() |
| 125 | commit = await db_session.get(MusehubCommit, head_ref.commit_id) |
| 126 | |
| 127 | recomputed = _recompute_commit_id(commit) |
| 128 | assert recomputed == commit.commit_id, ( |
| 129 | f"commit_id is not client-verifiable -- a real `muse clone` would reject " |
| 130 | f"this commit with 'failed content-hash verification'. " |
| 131 | f"stored={commit.commit_id!r} recomputed={recomputed!r}" |
| 132 | ) |
| 133 | |
| 134 | |
| 135 | async def test_rc04_commit_files_to_repo_produces_client_verifiable_commit_id( |
| 136 | db_session: AsyncSession, |
| 137 | ) -> None: |
| 138 | """The shared root-cause fix: commit_files_to_repo (used independently by |
| 139 | org creation, membership changes, and identity-repo creation) must itself |
| 140 | produce a client-verifiable commit_id, not just when called via create_repo.""" |
| 141 | repo = await create_repo( |
| 142 | db_session, |
| 143 | name=_uid("name"), |
| 144 | owner=_OWNER, |
| 145 | owner_user_id=_IDENTITY_ID, |
| 146 | visibility="private", |
| 147 | initialize=False, |
| 148 | ) |
| 149 | await db_session.commit() |
| 150 | |
| 151 | commit_id = await commit_files_to_repo( |
| 152 | db_session, |
| 153 | repo_id=repo.repo_id, |
| 154 | branch="main", |
| 155 | files={"a.txt": b"hello"}, |
| 156 | message="test commit", |
| 157 | author=_IDENTITY_ID, |
| 158 | ) |
| 159 | await db_session.commit() |
| 160 | |
| 161 | commit = await db_session.get(MusehubCommit, commit_id) |
| 162 | assert commit is not None |
| 163 | recomputed = _recompute_commit_id(commit) |
| 164 | assert recomputed == commit.commit_id, ( |
| 165 | f"commit_files_to_repo's commit_id is not client-verifiable. " |
| 166 | f"stored={commit.commit_id!r} recomputed={recomputed!r}" |
| 167 | ) |
| 168 | |
| 169 | |
| 170 | async def test_rc05_initialize_false_creates_no_commit(db_session: AsyncSession) -> None: |
| 171 | repo = await create_repo( |
| 172 | db_session, |
| 173 | name=_uid("name"), |
| 174 | owner=_OWNER, |
| 175 | owner_user_id=_IDENTITY_ID, |
| 176 | visibility="private", |
| 177 | initialize=False, |
| 178 | ) |
| 179 | await db_session.commit() |
| 180 | |
| 181 | head_ref = (await db_session.execute( |
| 182 | MusehubCommitRef.__table__.select().where(MusehubCommitRef.repo_id == repo.repo_id) |
| 183 | )).first() |
| 184 | assert head_ref is None, "initialize=False must not create any commit" |
File History
1 commit
sha256:5dbc6b75b9add84f8812f9f390fbd5b47fcf9e22947b2cf86ae54146c99c9df8
fix: create_repo seeds invalid genesis commit; commit_files…
Sonnet 5
patch
2 days ago