gabriel / musehub public

test_repo_init_commit_cloneable.py file-level

at sha256:5 · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:1 fix: getting-started page's muse --version sample output showed musehub… · gabriel · Oct 23, 2026
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"