gabriel / musehub public
test_commit_meta_promotion.py python
437 lines 17.1 KB
Raw
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 124 days ago
1 """TDD — promote all commit_meta fields to first-class columns.
2
3 Each WireCommit field gets its own column. commit_meta is dropped.
4
5 Tests are written RED-first against the current schema and pass once
6 migration 0020 and the wire service are updated.
7 """
8 from __future__ import annotations
9
10 import pytest
11 from datetime import datetime, timezone
12 from sqlalchemy.ext.asyncio import AsyncSession
13 import sqlalchemy as sa
14
15 from muse.core.types import blob_id, long_id, now_utc_iso
16 from musehub.db import musehub_models as db
17 from musehub.types.json_types import JSONObject, JSONValue
18
19
20 def _utc() -> datetime:
21 return datetime.now(tz=timezone.utc)
22
23
24 async def _make_repo(session: AsyncSession, slug: str) -> str:
25 from musehub.core.genesis import compute_identity_id, compute_repo_id
26 owner = "gabriel"
27 owner_user_id = compute_identity_id(owner.encode())
28 created_at = _utc()
29 repo_id = compute_repo_id(owner_user_id, slug, "code", created_at.isoformat())
30 session.add(db.MusehubRepo(
31 repo_id=repo_id,
32 name=slug,
33 owner=owner,
34 slug=slug,
35 visibility="public",
36 owner_user_id=owner_user_id,
37 description="",
38 tags=[],
39 created_at=created_at,
40 ))
41 await session.commit()
42 return repo_id
43
44
45 # ---------------------------------------------------------------------------
46 # ORM column existence
47 # ---------------------------------------------------------------------------
48
49 class TestMusehubCommitColumns:
50 """MusehubCommit must declare each promoted column."""
51
52 def test_has_signature_column(self) -> None:
53 cols = {c.key for c in db.MusehubCommit.__table__.columns}
54 assert "signature" in cols
55
56 def test_has_signer_public_key_column(self) -> None:
57 cols = {c.key for c in db.MusehubCommit.__table__.columns}
58 assert "signer_public_key" in cols
59
60 def test_has_signer_key_id_column(self) -> None:
61 cols = {c.key for c in db.MusehubCommit.__table__.columns}
62 assert "signer_key_id" in cols
63
64 def test_has_sem_ver_bump_column(self) -> None:
65 cols = {c.key for c in db.MusehubCommit.__table__.columns}
66 assert "sem_ver_bump" in cols
67
68 def test_has_breaking_changes_column(self) -> None:
69 cols = {c.key for c in db.MusehubCommit.__table__.columns}
70 assert "breaking_changes" in cols
71
72 def test_has_toolchain_id_column(self) -> None:
73 cols = {c.key for c in db.MusehubCommit.__table__.columns}
74 assert "toolchain_id" in cols
75
76 def test_has_prompt_hash_column(self) -> None:
77 cols = {c.key for c in db.MusehubCommit.__table__.columns}
78 assert "prompt_hash" in cols
79
80 def test_has_reviewed_by_column(self) -> None:
81 cols = {c.key for c in db.MusehubCommit.__table__.columns}
82 assert "reviewed_by" in cols
83
84 def test_has_test_runs_column(self) -> None:
85 cols = {c.key for c in db.MusehubCommit.__table__.columns}
86 assert "test_runs" in cols
87
88 def test_has_no_commit_meta_column(self) -> None:
89 cols = {c.key for c in db.MusehubCommit.__table__.columns}
90 assert "commit_meta" not in cols, "commit_meta must be dropped"
91
92
93 # ---------------------------------------------------------------------------
94 # Wire push populates columns
95 # ---------------------------------------------------------------------------
96
97
98 def _make_wire_commit(snapshot_id: str, **overrides: JSONValue) -> JSONObject:
99 import time
100 cid = blob_id(f"meta-promo-{time.time()}".encode())
101 base = {
102 "commit_id": cid,
103 "parent_ids": [],
104 "parent_commit_id": None,
105 "parent2_commit_id": None,
106 "snapshot_id": snapshot_id,
107 "branch": "task/my-feature",
108 "message": "feat: test commit",
109 "author": "gabriel",
110 "committed_at": now_utc_iso(),
111 "signature": "",
112 "signer_public_key": "",
113 "signer_key_id": "",
114 "agent_id": "claude-code",
115 "model_id": "claude-sonnet-4-6",
116 "toolchain_id": "cursor-agent-v2",
117 "prompt_hash": long_id("a" * 64),
118 "sem_ver_bump": "minor",
119 "breaking_changes": ["removed foo()"],
120 "reviewed_by": ["gabriel"],
121 "test_runs": 3,
122 "metadata": {},
123 "structured_delta": None,
124 "format_version": 7,
125 }
126 base.update(overrides)
127 return base
128
129
130 def _make_snapshot(snapshot_id: str) -> JSONObject:
131 return {"snapshot_id": snapshot_id, "manifest": {}, "committed_at": now_utc_iso()}
132
133
134 def _stub_r2(monkeypatch: pytest.MonkeyPatch) -> None:
135 from unittest.mock import AsyncMock
136 store: dict[str, bytes] = {}
137
138 async def _exists(oid: str) -> bool:
139 return oid in store
140
141 async def _put(oid: str, data: bytes, **kwargs: JSONValue) -> str:
142 store[oid] = data
143 return f"https://r2.fake/{oid}"
144
145 async def _get(oid: str) -> bytes | None:
146 return store.get(oid)
147
148 backend = AsyncMock()
149 backend.exists = _exists
150 backend.put = _put
151 backend.get = _get
152 monkeypatch.setattr("musehub.services.musehub_wire.get_backend", lambda: backend)
153
154
155 def _stub_verify(monkeypatch: pytest.MonkeyPatch) -> None:
156 """Bypass Ed25519 crypto gate so tests can use dummy key/sig values."""
157 monkeypatch.setattr("musehub.services.musehub_wire.verify_commit_ed25519", lambda *_: True)
158
159
160 async def _push_commit(db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch, commit: JSONObject, snap: JSONObject, repo_id: str) -> None:
161 import msgpack
162 from musehub.services.musehub_wire import wire_push_stream
163 from musehub.models.wire import SFRAME_HEADER, SFRAME_COMMIT_PACK, SFRAME_END
164 from muse.core.mpack import MuseWireFrameWriter
165
166 _fw = MuseWireFrameWriter()
167
168 def _wrap(ft: str, payload: JSONObject) -> bytes:
169 body = msgpack.packb(payload, use_bin_type=True)
170 return _fw.wrap(frame_type=ft, payload=body)
171
172 header_frame = _wrap(SFRAME_HEADER, {
173 "t": SFRAME_HEADER, "branch": "dev", "force": False,
174 "have": [], "n_objects": 0, "n_commits": 1,
175 })
176 commit_frame = _wrap(SFRAME_COMMIT_PACK, {"t": SFRAME_COMMIT_PACK, "commits": [commit], "snapshots": [snap]})
177 end_frame = _wrap(SFRAME_END, {"t": SFRAME_END, "n_objects": 0, "n_commits": 1})
178
179 async def body():
180 yield header_frame + commit_frame + end_frame
181
182 frames = []
183 async for chunk in wire_push_stream(db_session, repo_id, body(), "gabriel"):
184 frames.append(chunk)
185
186
187 @pytest.mark.asyncio
188 async def test_push_stores_signature(db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch) -> None:
189 """wire_push_stream writes signature to its own column."""
190 repo_id = await _make_repo(db_session, "meta-promo-sig")
191 snap_id = blob_id(b"snap-sig")
192 commit = _make_wire_commit(snap_id, signature="ed25519:MYSIG", signer_public_key="ed25519:PUBKEY")
193 _stub_r2(monkeypatch)
194 _stub_verify(monkeypatch)
195 await _push_commit(db_session, monkeypatch, commit, _make_snapshot(snap_id), repo_id)
196 row = (await db_session.execute(
197 sa.select(db.MusehubCommit).where(db.MusehubCommit.commit_id == commit["commit_id"])
198 )).scalar_one()
199 assert row.signature == "ed25519:MYSIG"
200
201
202 @pytest.mark.asyncio
203 async def test_push_stores_signer_public_key(db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch) -> None:
204 repo_id = await _make_repo(db_session, "meta-promo-spk")
205 snap_id = blob_id(b"snap-spk")
206 commit = _make_wire_commit(snap_id, signature="ed25519:SIG", signer_public_key="ed25519:PUBKEY")
207 _stub_r2(monkeypatch)
208 _stub_verify(monkeypatch)
209 await _push_commit(db_session, monkeypatch, commit, _make_snapshot(snap_id), repo_id)
210 row = (await db_session.execute(
211 sa.select(db.MusehubCommit).where(db.MusehubCommit.commit_id == commit["commit_id"])
212 )).scalar_one()
213 assert row.signer_public_key == "ed25519:PUBKEY"
214
215
216 @pytest.mark.asyncio
217 async def test_push_stores_signer_key_id(db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch) -> None:
218 repo_id = await _make_repo(db_session, "meta-promo-skid")
219 snap_id = blob_id(b"snap-skid")
220 commit = _make_wire_commit(snap_id, signer_key_id="ed25519:KEYID")
221 _stub_r2(monkeypatch)
222 await _push_commit(db_session, monkeypatch, commit, _make_snapshot(snap_id), repo_id)
223 row = (await db_session.execute(
224 sa.select(db.MusehubCommit).where(db.MusehubCommit.commit_id == commit["commit_id"])
225 )).scalar_one()
226 assert row.signer_key_id == "ed25519:KEYID"
227
228
229 @pytest.mark.asyncio
230 async def test_push_stores_sem_ver_bump(db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch) -> None:
231 repo_id = await _make_repo(db_session, "meta-promo-semver")
232 snap_id = blob_id(b"snap-semver")
233 commit = _make_wire_commit(snap_id, sem_ver_bump="major")
234 _stub_r2(monkeypatch)
235 await _push_commit(db_session, monkeypatch, commit, _make_snapshot(snap_id), repo_id)
236 row = (await db_session.execute(
237 sa.select(db.MusehubCommit).where(db.MusehubCommit.commit_id == commit["commit_id"])
238 )).scalar_one()
239 assert row.sem_ver_bump == "major"
240
241
242 @pytest.mark.asyncio
243 async def test_push_stores_breaking_changes(db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch) -> None:
244 repo_id = await _make_repo(db_session, "meta-promo-breaking")
245 snap_id = blob_id(b"snap-breaking")
246 commit = _make_wire_commit(snap_id, breaking_changes=["removed bar()", "renamed baz()"])
247 _stub_r2(monkeypatch)
248 await _push_commit(db_session, monkeypatch, commit, _make_snapshot(snap_id), repo_id)
249 row = (await db_session.execute(
250 sa.select(db.MusehubCommit).where(db.MusehubCommit.commit_id == commit["commit_id"])
251 )).scalar_one()
252 assert row.breaking_changes == ["removed bar()", "renamed baz()"]
253
254
255 @pytest.mark.asyncio
256 async def test_push_stores_toolchain_id(db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch) -> None:
257 repo_id = await _make_repo(db_session, "meta-promo-toolchain")
258 snap_id = blob_id(b"snap-toolchain")
259 commit = _make_wire_commit(snap_id, toolchain_id="cursor-agent-v2")
260 _stub_r2(monkeypatch)
261 await _push_commit(db_session, monkeypatch, commit, _make_snapshot(snap_id), repo_id)
262 row = (await db_session.execute(
263 sa.select(db.MusehubCommit).where(db.MusehubCommit.commit_id == commit["commit_id"])
264 )).scalar_one()
265 assert row.toolchain_id == "cursor-agent-v2"
266
267
268 @pytest.mark.asyncio
269 async def test_push_stores_prompt_hash(db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch) -> None:
270 repo_id = await _make_repo(db_session, "meta-promo-prompt")
271 snap_id = blob_id(b"snap-prompt")
272 ph = long_id("b" * 64)
273 commit = _make_wire_commit(snap_id, prompt_hash=ph)
274 _stub_r2(monkeypatch)
275 await _push_commit(db_session, monkeypatch, commit, _make_snapshot(snap_id), repo_id)
276 row = (await db_session.execute(
277 sa.select(db.MusehubCommit).where(db.MusehubCommit.commit_id == commit["commit_id"])
278 )).scalar_one()
279 assert row.prompt_hash == ph
280
281
282 @pytest.mark.asyncio
283 async def test_push_stores_reviewed_by(db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch) -> None:
284 repo_id = await _make_repo(db_session, "meta-promo-reviewed")
285 snap_id = blob_id(b"snap-reviewed")
286 commit = _make_wire_commit(snap_id, reviewed_by=["gabriel", "alice"])
287 _stub_r2(monkeypatch)
288 await _push_commit(db_session, monkeypatch, commit, _make_snapshot(snap_id), repo_id)
289 row = (await db_session.execute(
290 sa.select(db.MusehubCommit).where(db.MusehubCommit.commit_id == commit["commit_id"])
291 )).scalar_one()
292 assert row.reviewed_by == ["gabriel", "alice"]
293
294
295 @pytest.mark.asyncio
296 async def test_push_stores_test_runs(db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch) -> None:
297 repo_id = await _make_repo(db_session, "meta-promo-testruns")
298 snap_id = blob_id(b"snap-testruns")
299 commit = _make_wire_commit(snap_id, test_runs=7)
300 _stub_r2(monkeypatch)
301 await _push_commit(db_session, monkeypatch, commit, _make_snapshot(snap_id), repo_id)
302 row = (await db_session.execute(
303 sa.select(db.MusehubCommit).where(db.MusehubCommit.commit_id == commit["commit_id"])
304 )).scalar_one()
305 assert row.test_runs == 7
306
307
308 @pytest.mark.asyncio
309 async def test_push_does_not_store_commit_meta(db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch) -> None:
310 """After promotion, commit_meta column must not exist on pushed rows."""
311 repo_id = await _make_repo(db_session, "meta-promo-no-meta")
312 snap_id = blob_id(b"snap-no-meta")
313 commit = _make_wire_commit(snap_id)
314 _stub_r2(monkeypatch)
315 await _push_commit(db_session, monkeypatch, commit, _make_snapshot(snap_id), repo_id)
316 row = (await db_session.execute(
317 sa.select(db.MusehubCommit).where(db.MusehubCommit.commit_id == commit["commit_id"])
318 )).scalar_one()
319 assert not hasattr(row, "commit_meta"), "commit_meta must be gone"
320
321
322 # ---------------------------------------------------------------------------
323 # Route query — promoted columns appear in symbol timeline context
324 # ---------------------------------------------------------------------------
325
326 @pytest.mark.asyncio
327 async def test_symbol_timeline_query_includes_signature(
328 db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
329 ) -> None:
330 """ui_symbols route selects signature from MusehubCommit — no AttributeError on row.signature."""
331 from musehub.db import musehub_models as db_models
332 from musehub.core.genesis import compute_identity_id, compute_repo_id
333 from sqlalchemy import select
334
335 # Build a minimal repo + commit + history entry so the route has data to query
336 owner = "gabriel"
337 slug = "meta-promo-route-test"
338 owner_user_id = compute_identity_id(owner.encode())
339 created_at = _utc()
340 repo_id = compute_repo_id(owner_user_id, slug, "code", created_at.isoformat())
341 db_session.add(db_models.MusehubRepo(
342 repo_id=repo_id, name=slug, owner=owner, slug=slug,
343 visibility="public", owner_user_id=owner_user_id,
344 description="", tags=[], created_at=created_at,
345 ))
346 await db_session.commit()
347
348 commit_id = blob_id(b"route-test-commit")
349 committed_at = _utc()
350 db_session.add(db_models.MusehubCommit(
351 commit_id=commit_id, repo_id=repo_id, branch="dev",
352 parent_ids=[], message="feat: route test", author="gabriel",
353 timestamp=committed_at, agent_id="claude-code", model_id="claude-sonnet-4-6",
354 commit_branch="task/route-test", signature="ed25519:ROUTESIG",
355 ))
356 db_session.add(db_models.MusehubSymbolHistoryEntry(
357 repo_id=repo_id, address="src/route.py::fn",
358 commit_id=commit_id, committed_at=committed_at,
359 author="gabriel", op="add",
360 content_id=blob_id(b"body-route"),
361 ))
362 await db_session.commit()
363
364 # Execute the same SELECT the route uses — must include signature without AttributeError
365 result = await db_session.execute(
366 select(
367 db_models.MusehubCommit.commit_id,
368 db_models.MusehubCommit.message,
369 db_models.MusehubCommit.author,
370 db_models.MusehubCommit.branch,
371 db_models.MusehubCommit.commit_branch,
372 db_models.MusehubCommit.agent_id,
373 db_models.MusehubCommit.model_id,
374 db_models.MusehubCommit.signature,
375 ).where(
376 db_models.MusehubCommit.repo_id == repo_id,
377 db_models.MusehubCommit.commit_id == commit_id,
378 )
379 )
380 row = result.one()
381 assert row.signature == "ed25519:ROUTESIG"
382 assert row.agent_id == "claude-code"
383 assert row.commit_branch == "task/route-test"
384
385
386 # ---------------------------------------------------------------------------
387 # _to_wire_commit reads from first-class columns, not commit_meta
388 # ---------------------------------------------------------------------------
389
390 @pytest.mark.asyncio
391 async def test_to_wire_commit_reads_first_class_columns(
392 db_session: AsyncSession,
393 ) -> None:
394 """_to_wire_commit must read provenance from ORM columns, not commit_meta."""
395 from musehub.services.musehub_wire import _to_wire_commit
396
397 repo_id = await _make_repo(db_session, "wire-commit-columns-001")
398 commit_id = blob_id(b"wire-commit-cols-test")
399
400 row = db.MusehubCommit(
401 commit_id=commit_id,
402 repo_id=repo_id,
403 branch="dev",
404 parent_ids=[blob_id(b"parent-commit")],
405 message="feat: first-class columns",
406 author="gabriel",
407 timestamp=_utc(),
408 agent_id="claude-code",
409 model_id="claude-sonnet-4-6",
410 toolchain_id="muse-1.0",
411 commit_branch="task/my-feature",
412 signature="ed25519:SIG123",
413 signer_public_key="ed25519:PUB456",
414 signer_key_id="key-id-789",
415 sem_ver_bump="minor",
416 breaking_changes=[],
417 reviewed_by=[],
418 test_runs=3,
419 prompt_hash=blob_id(b"test-prompt"),
420 )
421 db_session.add(row)
422 await db_session.commit()
423 await db_session.refresh(row)
424
425 wc = _to_wire_commit(row)
426
427 assert wc.commit_id == commit_id
428 assert wc.agent_id == "claude-code"
429 assert wc.model_id == "claude-sonnet-4-6"
430 assert wc.toolchain_id == "muse-1.0"
431 assert wc.signature == "ed25519:SIG123"
432 assert wc.signer_public_key == "ed25519:PUB456"
433 assert wc.signer_key_id == "key-id-789"
434 assert wc.sem_ver_bump == "minor"
435 assert wc.test_runs == 3
436 assert wc.prompt_hash == blob_id(b"test-prompt")
437 assert wc.parent_commit_id == blob_id(b"parent-commit")
File History 1 commit
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 124 days ago