gabriel / musehub public
test_wire_fetch_mpack.py python
605 lines 24.0 KB
Raw
sha256:a68b91a9b1f1705e66730a0c328710192bc5e921c829892f0f9a11678341e148 fix: wire_fetch_mpack falls back to MusehubCommit.snapshot_… Sonnet 4.6 patch 42 days ago
1 """TDD — MPack-based fetch path: wire_fetch_mpack (issue #47).
2
3 The core principle: content-addressing is a proof, not a label.
4 sha256(mpack_bytes) == mpack_id IS the integrity check for everything inside.
5 No per-object hash loop. No secondary verification.
6
7 New function: wire_fetch_mpack(session, repo_id, want, have, ttl_seconds)
8
9 Returns FetchMPackResult:
10 mpack_id — "sha256:<hex>" — content-addressed ID of the mpack
11 mpack_url — presigned GET URL for the entire mpack (None when empty)
12 commit_count — number of commits in the delta
13 object_count — number of unique new objects
14
15 Tests:
16 FB0 Single commit + single object → mpack_id present, mpack_url set.
17 FB1 sha256(mpack_bytes) == mpack_id — the content-addressing proof.
18 FB2 MPack deserializes to expected commits and objects.
19 FB3 have cuts the delta — commits the client already has are excluded.
20 FB4 Empty want list → empty result (mpack_id=None, mpack_url=None).
21 FB5 All objects in the mpack satisfy sha256(content) == object_id (individual proofs).
22 """
23 from __future__ import annotations
24
25 import hashlib
26
27 import msgpack
28 import pytest
29 from sqlalchemy.dialects.postgresql import insert as pg_insert
30 from sqlalchemy.ext.asyncio import AsyncSession
31 from unittest.mock import AsyncMock
32
33 from datetime import datetime, timezone
34
35 from sqlalchemy import select
36 from muse.core.types import blob_id, fake_id
37 from muse.core.mpack import parse_wire_mpack
38 from musehub.db import musehub_repo_models as db
39 from tests.factories import create_repo
40
41 type ObjectStore = dict[str, bytes]
42
43
44 # ── helpers ───────────────────────────────────────────────────────────────────
45
46 def _now() -> datetime:
47 return datetime.now(tz=timezone.utc)
48
49
50 def _stub_backend(monkeypatch: pytest.MonkeyPatch) -> ObjectStore:
51 store: dict[str, bytes] = {}
52
53 async def _put(oid: str, data: bytes, **_: typing.Any) -> str:
54 store[oid] = data
55 return f"mem://{oid}"
56
57 async def _get(oid: str) -> bytes | None:
58 return store.get(oid)
59
60 async def _exists(oid: str, **_: typing.Any) -> bool:
61 return oid in store
62
63 async def _presign_get(oid: str, ttl: int) -> str:
64 return f"https://minio.test/{oid}?ttl={ttl}"
65
66 async def _get_mpack(mpack_id: str) -> bytes | None:
67 return store.get(mpack_id)
68
69 async def _put_mpack(mpack_id: str, data: bytes) -> str:
70 store[mpack_id] = data
71 return f"mem://mpacks/{mpack_id}"
72
73 async def _presign_mpack_get(mpack_id: str, ttl: int) -> str:
74 return f"https://minio.test/mpacks/{mpack_id}?ttl={ttl}"
75
76 backend = AsyncMock()
77 backend.put = _put
78 backend.get = _get
79 backend.exists = _exists
80 backend.presign_get = _presign_get
81 backend.get_mpack = _get_mpack
82 backend.put_mpack = _put_mpack
83 backend.presign_mpack_get = _presign_mpack_get
84 backend.supports_presign = True
85 monkeypatch.setattr("musehub.services.musehub_wire.get_backend", lambda: backend)
86 monkeypatch.setattr("musehub.services.musehub_wire_fetch.get_backend", lambda: backend)
87 monkeypatch.setattr("musehub.services.musehub_wire_shared.get_backend", lambda: backend)
88 return store
89
90
91 async def _store_object(
92 session: AsyncSession,
93 repo_id: str,
94 oid: str,
95 content: bytes,
96 store: dict[str, bytes],
97 ) -> None:
98 # Store content in mock backend (legacy fallback path)
99 store[oid] = content
100
101 # Build a per-object fake mpack so the MPackIndex path also works
102 fake_mpack_bytes = msgpack.packb(
103 {"blobs": [{"object_id": oid, "content": content}],
104 "commits": [], "snapshots": []},
105 use_bin_type=True,
106 )
107 fake_mpack_id = blob_id(fake_mpack_bytes)
108 store[fake_mpack_id] = fake_mpack_bytes
109
110 await session.execute(
111 pg_insert(db.MusehubObject)
112 .values(
113 object_id=oid,
114 path="file.dat",
115 size_bytes=len(content),
116 storage_uri=f"mem://{oid}",
117 )
118 .on_conflict_do_nothing(index_elements=["object_id"])
119 )
120 await session.execute(
121 pg_insert(db.MusehubObjectRef)
122 .values(repo_id=repo_id, object_id=oid)
123 .on_conflict_do_nothing()
124 )
125 await session.execute(
126 pg_insert(db.MusehubMPackIndex)
127 .values(entity_id=oid, mpack_id=fake_mpack_id, entity_type="object")
128 .on_conflict_do_nothing()
129 )
130 await session.commit()
131
132
133 async def _make_commit(
134 session: AsyncSession,
135 repo_id: str,
136 *,
137 manifest: dict[str, str],
138 seed: str = "c1",
139 parent_ids: list[str] | None = None,
140 ) -> tuple[db.MusehubCommit, db.MusehubSnapshot]:
141 from muse.core.ids import hash_snapshot as _hash_snapshot
142 snap_id = _hash_snapshot(manifest, [])
143 snap = db.MusehubSnapshot(
144 snapshot_id=snap_id,
145 directories=[],
146 manifest_blob=msgpack.packb(manifest, use_bin_type=True),
147 entry_count=len(manifest),
148 created_at=_now(),
149 )
150 session.add(snap)
151 await session.execute(
152 pg_insert(db.MusehubSnapshotRef)
153 .values(repo_id=repo_id, snapshot_id=snap_id)
154 .on_conflict_do_nothing()
155 )
156 commit_id = fake_id(f"commit-{seed}")
157 commit = db.MusehubCommit(
158 commit_id=commit_id,
159 branch="main",
160 parent_ids=parent_ids or [],
161 message=f"commit {seed}",
162 author="gabriel",
163 timestamp=_now(),
164 snapshot_id=snap_id,
165 )
166 session.add(commit)
167 await session.execute(
168 pg_insert(db.MusehubCommitRef)
169 .values(repo_id=repo_id, commit_id=commit_id)
170 .on_conflict_do_nothing()
171 )
172 await session.execute(
173 pg_insert(db.MusehubCommitGraph)
174 .values(
175 commit_id=commit_id,
176 parent_ids=parent_ids or [],
177 generation=0,
178 snapshot_id=snap_id,
179 )
180 .on_conflict_do_nothing()
181 )
182 # Update branch head
183 branch_q = await session.execute(
184 select(db.MusehubBranch).where(
185 db.MusehubBranch.repo_id == repo_id,
186 db.MusehubBranch.name == "main",
187 )
188 )
189 branch = branch_q.scalar_one_or_none()
190 if branch:
191 branch.head_commit_id = commit_id
192 await session.commit()
193 return commit, snap
194
195
196 # ══════════════════════════════════════════════════════════════════════════════
197 # FB0 — single commit + single object → mpack_id and mpack_url returned
198 # ══════════════════════════════════════════════════════════════════════════════
199
200 @pytest.mark.asyncio
201 async def test_fb0_single_commit_returns_mpack(
202 db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
203 ) -> None:
204 """Single commit with one object → wire_fetch_mpack returns mpack_id and mpack_url."""
205 from musehub.services.musehub_wire import wire_fetch_mpack
206
207 store = _stub_backend(monkeypatch)
208 repo = await create_repo(db_session, owner="gabriel", visibility="public")
209
210 raw = b"hello world content"
211 oid = blob_id(raw)
212 await _store_object(db_session, repo.repo_id, oid, raw, store)
213 commit, _ = await _make_commit(
214 db_session, repo.repo_id, manifest={"hello.txt": oid}, seed="fb0"
215 )
216
217 result = await wire_fetch_mpack(
218 db_session, repo.repo_id, want=[commit.commit_id], have=[], force_build=True
219 )
220
221 assert result["mpack_id"].startswith("sha256:"), "mpack_id must be sha256-prefixed"
222 assert result["mpack_url"], "mpack_url must be non-empty"
223 assert result["commit_count"] == 1
224 assert result["blob_count"] == 1
225
226
227 # ══════════════════════════════════════════════════════════════════════════════
228 # FB1 — sha256(mpack_bytes) == mpack_id — the proof
229 # ══════════════════════════════════════════════════════════════════════════════
230
231 @pytest.mark.asyncio
232 async def test_fb1_mpack_id_equals_sha256_of_bytes(
233 db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
234 ) -> None:
235 """sha256(mpack_bytes) == mpack_id — content-addressing is the integrity check."""
236 from musehub.services.musehub_wire import wire_fetch_mpack
237
238 store = _stub_backend(monkeypatch)
239 repo = await create_repo(db_session, owner="gabriel", visibility="public")
240
241 raw = b"content for proof test"
242 oid = blob_id(raw)
243 await _store_object(db_session, repo.repo_id, oid, raw, store)
244 commit, _ = await _make_commit(
245 db_session, repo.repo_id, manifest={"proof.bin": oid}, seed="fb1"
246 )
247
248 result = await wire_fetch_mpack(
249 db_session, repo.repo_id, want=[commit.commit_id], have=[], force_build=True
250 )
251
252 actual_bytes = store[result["mpack_id"]]
253 expected_id = blob_id(actual_bytes)
254
255 assert result["mpack_id"] == expected_id, (
256 f"mpack_id {result['mpack_id']!r} != blob_id(stored_bytes) {expected_id!r}"
257 )
258
259
260 # ══════════════════════════════════════════════════════════════════════════════
261 # FB2 — mpack deserializes with expected commits and objects
262 # ══════════════════════════════════════════════════════════════════════════════
263
264 @pytest.mark.asyncio
265 async def test_fb2_mpack_contains_commits_and_objects(
266 db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
267 ) -> None:
268 """MPack bytes deserialize to a dict with commits and objects matching what was pushed."""
269 from musehub.services.musehub_wire import wire_fetch_mpack
270
271 store = _stub_backend(monkeypatch)
272 repo = await create_repo(db_session, owner="gabriel", visibility="public")
273
274 raws = [f"object-{i}".encode() for i in range(3)]
275 oids = [blob_id(r) for r in raws]
276 for oid, raw in zip(oids, raws):
277 await _store_object(db_session, repo.repo_id, oid, raw, store)
278
279 manifest = {f"file{i}.bin": oid for i, oid in enumerate(oids)}
280 commit, _ = await _make_commit(
281 db_session, repo.repo_id, manifest=manifest, seed="fb2"
282 )
283
284 result = await wire_fetch_mpack(
285 db_session, repo.repo_id, want=[commit.commit_id], have=[], force_build=True
286 )
287
288 mpack = parse_wire_mpack(store[result["mpack_id"]])
289
290 assert "commits" in mpack, "mpack must contain 'commits'"
291 assert "blobs" in mpack, "mpack must contain 'objects'"
292
293 mpack_commit_ids = {c["commit_id"] for c in mpack["commits"]}
294 assert commit.commit_id in mpack_commit_ids, "expected commit must be in mpack"
295
296 mpack_oids = {o["object_id"] for o in mpack["blobs"]}
297 assert set(oids) == mpack_oids, (
298 f"mpack objects {mpack_oids} != expected {set(oids)}"
299 )
300
301
302 # ══════════════════════════════════════════════════════════════════════════════
303 # FB3 — have cuts the delta
304 # ══════════════════════════════════════════════════════════════════════════════
305
306 @pytest.mark.asyncio
307 async def test_fb3_have_excludes_known_commits(
308 db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
309 ) -> None:
310 """Commits in have are excluded from the mpack delta."""
311 from musehub.services.musehub_wire import wire_fetch_mpack
312
313 store = _stub_backend(monkeypatch)
314 repo = await create_repo(db_session, owner="gabriel", visibility="public")
315
316 raw1 = b"first commit content"
317 oid1 = blob_id(raw1)
318 await _store_object(db_session, repo.repo_id, oid1, raw1, store)
319 c1, _ = await _make_commit(
320 db_session, repo.repo_id, manifest={"f1.bin": oid1}, seed="fb3-c1"
321 )
322
323 raw2 = b"second commit content"
324 oid2 = blob_id(raw2)
325 await _store_object(db_session, repo.repo_id, oid2, raw2, store)
326 c2, _ = await _make_commit(
327 db_session, repo.repo_id, manifest={"f1.bin": oid1, "f2.bin": oid2},
328 seed="fb3-c2", parent_ids=[c1.commit_id],
329 )
330
331 # Client already has c1 — only c2's delta should be in the mpack
332 result = await wire_fetch_mpack(
333 db_session, repo.repo_id, want=[c2.commit_id], have=[c1.commit_id]
334 )
335
336 mpack = parse_wire_mpack(store[result["mpack_id"]])
337 mpack_commit_ids = {c["commit_id"] for c in mpack["commits"]}
338
339 assert c2.commit_id in mpack_commit_ids, "new commit must be in mpack"
340 assert c1.commit_id not in mpack_commit_ids, "known commit must be excluded by have"
341 assert result["commit_count"] == 1
342
343 mpack_oids = {o["object_id"] for o in mpack["blobs"]}
344 assert oid1 not in mpack_oids, "object from have-side commit must be excluded"
345 assert oid2 in mpack_oids, "new object must be included"
346
347
348 # ══════════════════════════════════════════════════════════════════════════════
349 # FB4 — empty want → empty result
350 # ══════════════════════════════════════════════════════════════════════════════
351
352 @pytest.mark.asyncio
353 async def test_fb4_empty_want_returns_empty(
354 db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
355 ) -> None:
356 """Empty want list → zero commits, zero objects, no mpack produced."""
357 from musehub.services.musehub_wire import wire_fetch_mpack
358
359 _stub_backend(monkeypatch)
360 repo = await create_repo(db_session, owner="gabriel", visibility="public")
361
362 result = await wire_fetch_mpack(
363 db_session, repo.repo_id, want=[], have=[]
364 )
365
366 assert result["commit_count"] == 0
367 assert result["blob_count"] == 0
368 assert result["mpack_id"] is None
369
370
371 # ══════════════════════════════════════════════════════════════════════════════
372 # FB5 — per-object integrity (each object's content hashes to its ID)
373 # ══════════════════════════════════════════════════════════════════════════════
374
375 @pytest.mark.asyncio
376 async def test_fb5_object_content_hashes_to_id(
377 db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
378 ) -> None:
379 """Every object in the mpack has sha256(content) == object_id."""
380 from musehub.services.musehub_wire import wire_fetch_mpack
381
382 store = _stub_backend(monkeypatch)
383 repo = await create_repo(db_session, owner="gabriel", visibility="public")
384
385 raws = [f"verifiable-content-{i}".encode() for i in range(4)]
386 oids = [blob_id(r) for r in raws]
387 for oid, raw in zip(oids, raws):
388 await _store_object(db_session, repo.repo_id, oid, raw, store)
389
390 manifest = {f"f{i}.bin": oid for i, oid in enumerate(oids)}
391 commit, _ = await _make_commit(
392 db_session, repo.repo_id, manifest=manifest, seed="fb5"
393 )
394
395 result = await wire_fetch_mpack(
396 db_session, repo.repo_id, want=[commit.commit_id], have=[], force_build=True
397 )
398
399 mpack = parse_wire_mpack(store[result["mpack_id"]])
400 for obj in mpack["blobs"]:
401 actual_id = blob_id(obj["content"])
402 assert actual_id == obj["object_id"], (
403 f"object integrity failure: sha256(content)={actual_id} != id={obj['object_id']}"
404 )
405
406
407 # ══════════════════════════════════════════════════════════════════════════════
408 # FB6 — server-side merge commit snapshot is included even when CommitGraph
409 # has snapshot_id=None (defensive fallback via MusehubCommit row)
410 # ══════════════════════════════════════════════════════════════════════════════
411
412 @pytest.mark.asyncio
413 async def test_fb6_merge_commit_snapshot_included_via_commit_fallback(
414 db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
415 ) -> None:
416 """wire_fetch_mpack returns the merge snapshot even when MusehubCommitGraph.snapshot_id is NULL.
417
418 Regression test for the original wire-fetch bug: server-side merge commits
419 were not included in fetch mpack responses because _walk_commit_delta only
420 read snapshot_id from CommitGraph rows (which were NULL before the proposals
421 fix), ignoring the snapshot_id already present on the MusehubCommit row.
422
423 The fix: wire_fetch_mpack falls back to MusehubCommit.snapshot_id for any
424 commit whose CommitGraph row has snapshot_id=None.
425 """
426 from musehub.services.musehub_wire import wire_fetch_mpack
427
428 store = _stub_backend(monkeypatch)
429 repo = await create_repo(db_session, owner="gabriel", visibility="public")
430
431 # Two parent commits (client already has these)
432 raw1 = b"parent-1-content"
433 oid1 = blob_id(raw1)
434 await _store_object(db_session, repo.repo_id, oid1, raw1, store)
435 c1, _ = await _make_commit(
436 db_session, repo.repo_id, manifest={"f1.bin": oid1}, seed="fb6-c1"
437 )
438
439 raw2 = b"parent-2-content"
440 oid2 = blob_id(raw2)
441 await _store_object(db_session, repo.repo_id, oid2, raw2, store)
442 c2, _ = await _make_commit(
443 db_session, repo.repo_id, manifest={"f2.bin": oid2}, seed="fb6-c2",
444 parent_ids=[c1.commit_id],
445 )
446
447 # Merge commit: has a real snapshot on MusehubCommit, but CommitGraph row
448 # has snapshot_id=None — this is the pre-fix bug state.
449 merge_manifest = {"f1.bin": oid1, "f2.bin": oid2}
450 merge_snap_id = fake_id("fb6-merge-snap")
451 merge_snap = db.MusehubSnapshot(
452 snapshot_id=merge_snap_id,
453 directories=[],
454 manifest_blob=msgpack.packb(merge_manifest, use_bin_type=True),
455 entry_count=len(merge_manifest),
456 created_at=_now(),
457 )
458 db_session.add(merge_snap)
459 await db_session.execute(
460 pg_insert(db.MusehubSnapshotRef)
461 .values(repo_id=repo.repo_id, snapshot_id=merge_snap_id)
462 .on_conflict_do_nothing()
463 )
464
465 merge_commit_id = fake_id("fb6-merge-commit")
466 parent_ids = [c1.commit_id, c2.commit_id]
467 merge_commit = db.MusehubCommit(
468 commit_id=merge_commit_id,
469 branch="main",
470 parent_ids=parent_ids,
471 message="Merge branch 'feat' into 'main'",
472 author="gabriel",
473 timestamp=_now(),
474 snapshot_id=merge_snap_id, # MusehubCommit has it
475 )
476 db_session.add(merge_commit)
477 await db_session.execute(
478 pg_insert(db.MusehubCommitRef)
479 .values(repo_id=repo.repo_id, commit_id=merge_commit_id)
480 .on_conflict_do_nothing()
481 )
482 # CommitGraph row with snapshot_id=None — the pre-fix bug state
483 await db_session.execute(
484 pg_insert(db.MusehubCommitGraph)
485 .values(
486 commit_id=merge_commit_id,
487 parent_ids=parent_ids,
488 generation=1,
489 snapshot_id=None, # ← bug: missing snapshot_id
490 )
491 .on_conflict_do_nothing()
492 )
493 await db_session.commit()
494
495 # Client has both parents; needs the merge commit
496 result = await wire_fetch_mpack(
497 db_session, repo.repo_id,
498 want=[merge_commit_id],
499 have=[c1.commit_id, c2.commit_id],
500 )
501
502 assert result["mpack_id"] is not None, "mpack must be produced for the merge commit"
503 assert result["commit_count"] == 1
504
505 mpack = parse_wire_mpack(store[result["mpack_id"]])
506 snap_ids_in_mpack = {s["snapshot_id"] for s in mpack.get("snapshots", [])}
507 assert merge_snap_id in snap_ids_in_mpack, (
508 f"merge snapshot {merge_snap_id!r} must be in mpack snapshots; got {snap_ids_in_mpack}"
509 )
510
511
512 # ══════════════════════════════════════════════════════════════════════════════
513 # FB7 — server-side merge commit snapshot included when CommitGraph has it
514 # (the post-fix happy path; also ensures no regression)
515 # ══════════════════════════════════════════════════════════════════════════════
516
517 @pytest.mark.asyncio
518 async def test_fb7_merge_commit_snapshot_included_via_commit_graph(
519 db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
520 ) -> None:
521 """wire_fetch_mpack returns the merge snapshot when CommitGraph.snapshot_id is set.
522
523 This is the happy path after merge_proposal was fixed to always insert
524 CommitGraph rows with snapshot_id populated.
525 """
526 from musehub.services.musehub_wire import wire_fetch_mpack
527
528 store = _stub_backend(monkeypatch)
529 repo = await create_repo(db_session, owner="gabriel", visibility="public")
530
531 raw1 = b"fb7-parent-1"
532 oid1 = blob_id(raw1)
533 await _store_object(db_session, repo.repo_id, oid1, raw1, store)
534 c1, _ = await _make_commit(
535 db_session, repo.repo_id, manifest={"f1.bin": oid1}, seed="fb7-c1"
536 )
537
538 raw2 = b"fb7-parent-2"
539 oid2 = blob_id(raw2)
540 await _store_object(db_session, repo.repo_id, oid2, raw2, store)
541 c2, _ = await _make_commit(
542 db_session, repo.repo_id, manifest={"f2.bin": oid2}, seed="fb7-c2",
543 parent_ids=[c1.commit_id],
544 )
545
546 merge_manifest = {"f1.bin": oid1, "f2.bin": oid2}
547 merge_snap_id = fake_id("fb7-merge-snap")
548 merge_snap = db.MusehubSnapshot(
549 snapshot_id=merge_snap_id,
550 directories=[],
551 manifest_blob=msgpack.packb(merge_manifest, use_bin_type=True),
552 entry_count=len(merge_manifest),
553 created_at=_now(),
554 )
555 db_session.add(merge_snap)
556 await db_session.execute(
557 pg_insert(db.MusehubSnapshotRef)
558 .values(repo_id=repo.repo_id, snapshot_id=merge_snap_id)
559 .on_conflict_do_nothing()
560 )
561
562 merge_commit_id = fake_id("fb7-merge-commit")
563 parent_ids = [c1.commit_id, c2.commit_id]
564 merge_commit = db.MusehubCommit(
565 commit_id=merge_commit_id,
566 branch="main",
567 parent_ids=parent_ids,
568 message="Merge branch 'feat' into 'main'",
569 author="gabriel",
570 timestamp=_now(),
571 snapshot_id=merge_snap_id,
572 )
573 db_session.add(merge_commit)
574 await db_session.execute(
575 pg_insert(db.MusehubCommitRef)
576 .values(repo_id=repo.repo_id, commit_id=merge_commit_id)
577 .on_conflict_do_nothing()
578 )
579 # CommitGraph row WITH snapshot_id — the post-fix state
580 await db_session.execute(
581 pg_insert(db.MusehubCommitGraph)
582 .values(
583 commit_id=merge_commit_id,
584 parent_ids=parent_ids,
585 generation=1,
586 snapshot_id=merge_snap_id, # ← correct
587 )
588 .on_conflict_do_nothing()
589 )
590 await db_session.commit()
591
592 result = await wire_fetch_mpack(
593 db_session, repo.repo_id,
594 want=[merge_commit_id],
595 have=[c1.commit_id, c2.commit_id],
596 )
597
598 assert result["mpack_id"] is not None
599 assert result["commit_count"] == 1
600
601 mpack = parse_wire_mpack(store[result["mpack_id"]])
602 snap_ids_in_mpack = {s["snapshot_id"] for s in mpack.get("snapshots", [])}
603 assert merge_snap_id in snap_ids_in_mpack, (
604 f"merge snapshot {merge_snap_id!r} must be in mpack snapshots; got {snap_ids_in_mpack}"
605 )
File History 2 commits
sha256:a68b91a9b1f1705e66730a0c328710192bc5e921c829892f0f9a11678341e148 fix: wire_fetch_mpack falls back to MusehubCommit.snapshot_… Sonnet 4.6 patch 42 days ago
sha256:ef10830ce231e0a20efcb0e2586cb879471247e916616e6fdd0d51df459e2595 fix: typing audit — 0 violations, 0 untyped defs across all… Sonnet 4.6 minor 49 days ago