gabriel / musehub public
test_snapshot_entries.py python
692 lines 23.4 KB
Raw
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 121 days ago
1 """TDD tests for the snapshot entries refactor.
2
3 Defines the new contract:
4 - Snapshot file trees are stored as normalized rows in ``musehub_snapshot_entries``
5 (snapshot_id, path, object_id, size_bytes) rather than as a JSON blob in
6 ``musehub_snapshots.manifest``.
7 - A utility ``get_snapshot_manifest`` reconstructs {path: object_id} from
8 those rows for callers that need the dict form.
9 - A utility ``upsert_snapshot_entries`` is the canonical write path, used by
10 wire_push, ingest_push, and merge_proposaloposal.
11 - Re-pushing a snapshot whose row already exists STILL writes / updates its
12 entries — the old "skip if exists" guard is the root cause of the bug being
13 fixed here.
14
15 All tests are RED until the implementation is in place.
16 """
17 from __future__ import annotations
18
19 import secrets
20
21 from muse.core.types import blob_id
22 from datetime import datetime, timezone
23
24 import pytest
25 from sqlalchemy import func, select
26 from sqlalchemy.ext.asyncio import AsyncSession
27
28 from musehub.core.genesis import compute_branch_id, compute_identity_id, compute_repo_id
29 from musehub.db import musehub_models as db
30 from musehub.models.musehub import CommitInput, SnapshotInput
31 import msgpack
32 from muse.core.mpack import MuseWireFrameWriter
33 from musehub.models.wire import (
34 WireBundle, WireCommit, WireSnapshot,
35 SFRAME_HEADER, SFRAME_OBJECT, SFRAME_COMMIT_PACK, SFRAME_END,
36 )
37 from musehub.services.musehub_sync import ingest_push
38 from musehub.services.musehub_wire import wire_push_stream
39 from musehub.types.json_types import JSONObject, JSONValue, StrDict
40
41
42 # ---------------------------------------------------------------------------
43 # Helpers
44 # ---------------------------------------------------------------------------
45
46 MANIFEST_A = {
47 "README.md": blob_id(b"obj-readme-001"),
48 "musehub/main.py": blob_id(b"obj-main-001"),
49 "musehub/db/models.py": blob_id(b"obj-models-001"),
50 }
51
52 MANIFEST_B = {
53 "README.md": blob_id(b"obj-readme-002"), # changed
54 "musehub/main.py": blob_id(b"obj-main-001"), # unchanged
55 "musehub/db/models.py": blob_id(b"obj-models-002"), # changed
56 "musehub/new_file.py": blob_id(b"obj-new-001"), # added
57 }
58
59 # Wire-compatible manifest — sha256: IDs required by wire_push_stream validation.
60 MANIFEST_WIRE = {
61 "README.md": "sha256:b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5",
62 "musehub/main.py": "sha256:0a60476c9b54c039576ddeb49f91a1251adc5840e3a9eb57fcf23e085c6a40e9",
63 "musehub/db/models.py": "sha256:1996895592963cc9535d725374cb5605d2e6dfce8d3351b8e8f14f18df368b92",
64 }
65
66
67 def _uid() -> str:
68 return blob_id(secrets.token_bytes(16))
69
70
71 _OWNER_ID = compute_identity_id(b"testuser")
72 _FIXED_TS = "2026-01-01T00:00:00+00:00"
73
74
75 def _make_repo_id(slug: str, owner_id: str = _OWNER_ID) -> str:
76 return compute_repo_id(owner_id, slug, "code", _FIXED_TS)
77
78
79 def _repo(slug: str, owner_user_id: str = _OWNER_ID) -> db.MusehubRepo:
80 repo_id = _make_repo_id(slug, owner_user_id)
81 return db.MusehubRepo(
82 repo_id=repo_id,
83 name=slug,
84 owner="testuser",
85 slug=slug,
86 visibility="private",
87 owner_user_id=owner_user_id,
88 created_at=datetime.now(timezone.utc),
89 updated_at=datetime.now(timezone.utc),
90 )
91
92
93 def _commit_input(
94 commit_id: str,
95 snapshot_id: str,
96 parent_ids: list[str] | None = None,
97 ) -> CommitInput:
98 return CommitInput(
99 commit_id=commit_id,
100 branch="dev",
101 parent_ids=parent_ids or [],
102 message="test commit",
103 author="tester",
104 timestamp="2026-01-01T00:00:00Z",
105 snapshot_id=snapshot_id,
106 )
107
108
109 def _snap_input(
110 snapshot_id: str,
111 manifest: StrDict,
112 ) -> SnapshotInput:
113 return SnapshotInput(snapshot_id=snapshot_id, manifest=manifest)
114
115
116 def _wire_bundle(
117 commit_id: str,
118 snapshot_id: str,
119 manifest: StrDict,
120 parent_id: str | None = None,
121 ) -> WireBundle:
122 return WireBundle(
123 commits=[
124 WireCommit(
125 commit_id=commit_id,
126 branch="dev",
127 parent_commit_id=parent_id or None,
128 message="test commit",
129 author="tester",
130 timestamp="2026-01-01T00:00:00Z",
131 snapshot_id=snapshot_id,
132 )
133 ],
134 snapshots=[
135 WireSnapshot(
136 snapshot_id=snapshot_id,
137 manifest=manifest,
138 created_at="2026-01-01T00:00:00Z",
139 )
140 ],
141 objects=[],
142 )
143
144
145 async def _count_entries(
146 session: AsyncSession, snapshot_id: str
147 ) -> int:
148 result = await session.execute(
149 select(func.count()).select_from(db.MusehubSnapshotEntry).where(
150 db.MusehubSnapshotEntry.snapshot_id == snapshot_id
151 )
152 )
153 return result.scalar_one()
154
155
156 # ---------------------------------------------------------------------------
157 # 1. Model existence
158 # ---------------------------------------------------------------------------
159
160
161 def test_snapshot_entry_model_exists() -> None:
162 """MusehubSnapshotEntry ORM model must exist on the db module."""
163 assert hasattr(db, "MusehubSnapshotEntry"), (
164 "db.MusehubSnapshotEntry not found — add the ORM model and migration"
165 )
166
167
168 def test_snapshot_entry_has_required_columns() -> None:
169 """MusehubSnapshotEntry must have snapshot_id, path, object_id, size_bytes."""
170 entry = db.MusehubSnapshotEntry
171 cols = {c.key for c in entry.__table__.columns}
172 assert "snapshot_id" in cols
173 assert "path" in cols
174 assert "object_id" in cols
175 assert "size_bytes" in cols, "size_bytes is the muse augmentation over git trees"
176
177
178 def test_snapshot_manifest_column_removed() -> None:
179 """MusehubSnapshot must NOT have a manifest column — it moved to entries."""
180 snap_cols = {c.key for c in db.MusehubSnapshot.__table__.columns}
181 assert "manifest" not in snap_cols, (
182 "manifest JSON blob must be removed from musehub_snapshots — "
183 "file trees now live in musehub_snapshot_entries"
184 )
185
186
187 # ---------------------------------------------------------------------------
188 # 2. Utility functions
189 # ---------------------------------------------------------------------------
190
191
192 @pytest.mark.asyncio
193 async def test_get_snapshot_manifest_returns_empty_for_unknown(
194 db_session: AsyncSession,
195 ) -> None:
196 """get_snapshot_manifest returns {} when snapshot_id has no entries."""
197 from musehub.services.musehub_snapshot import get_snapshot_manifest
198
199 result = await get_snapshot_manifest(db_session, "nonexistent-snap")
200 assert result == {}
201
202
203 @pytest.mark.asyncio
204 async def test_get_snapshot_manifest_reconstructs_from_entries(
205 db_session: AsyncSession,
206 ) -> None:
207 """get_snapshot_manifest returns the correct {path: object_id} dict."""
208 from musehub.services.musehub_snapshot import get_snapshot_manifest, upsert_snapshot_entries
209
210 snap_id = "snap-util-001"
211 slug = "repo-util-001"
212 repo_id = _make_repo_id(slug)
213 db_session.add(_repo(slug))
214 await db_session.flush()
215
216 await upsert_snapshot_entries(db_session, repo_id, snap_id, MANIFEST_A)
217 await db_session.flush()
218
219 result = await get_snapshot_manifest(db_session, snap_id)
220 assert result == MANIFEST_A
221
222
223 @pytest.mark.asyncio
224 async def test_upsert_snapshot_entries_is_idempotent(
225 db_session: AsyncSession,
226 ) -> None:
227 """Calling upsert_snapshot_entries twice with the same data is safe."""
228 from musehub.services.musehub_snapshot import upsert_snapshot_entries
229
230 snap_id = "snap-idempotent-001"
231 slug = "repo-idempotent-001"
232 repo_id = _make_repo_id(slug)
233 db_session.add(_repo(slug))
234 await db_session.flush()
235
236 await upsert_snapshot_entries(db_session, repo_id, snap_id, MANIFEST_A)
237 await db_session.flush()
238 await upsert_snapshot_entries(db_session, repo_id, snap_id, MANIFEST_A)
239 await db_session.flush()
240
241 from musehub.services.musehub_snapshot import get_snapshot_manifest
242 result = await get_snapshot_manifest(db_session, snap_id)
243 assert result == MANIFEST_A
244
245
246 @pytest.mark.asyncio
247 async def test_upsert_snapshot_entries_backfills_null_blob(
248 db_session: AsyncSession,
249 ) -> None:
250 """upsert_snapshot_entries creates a new snapshot with the correct manifest_blob.
251
252 Snapshots are content-addressed: the same snapshot_id always has the same
253 manifest. Calling upsert creates the row and the manifest is readable
254 immediately after.
255 """
256 from musehub.services.musehub_snapshot import get_snapshot_manifest, upsert_snapshot_entries
257
258 snap_id = "snap-backfill-001"
259 slug = "repo-backfill-001"
260 repo_id = _make_repo_id(slug)
261 db_session.add(_repo(slug))
262 await db_session.flush()
263
264 await upsert_snapshot_entries(db_session, repo_id, snap_id, MANIFEST_A)
265 await db_session.flush()
266
267 result = await get_snapshot_manifest(db_session, snap_id)
268 assert result == MANIFEST_A
269
270
271 # ---------------------------------------------------------------------------
272 # 3. ingest_push write path (REST API / MCP path)
273 # ---------------------------------------------------------------------------
274
275
276 @pytest.mark.asyncio
277 async def test_ingest_push_stores_snapshot_entries(
278 db_session: AsyncSession,
279 ) -> None:
280 """ingest_push must write entries to musehub_snapshot_entries."""
281 slug = "repo-ip-entries-001"
282 repo_id = _make_repo_id(slug)
283 snap_id = "snap-ip-001"
284 commit_id = _uid()
285
286 db_session.add(_repo(slug))
287 await db_session.flush()
288
289 await ingest_push(
290 db_session,
291 repo_id=repo_id,
292 branch="dev",
293 head_commit_id=commit_id,
294 commits=[_commit_input(commit_id, snap_id)],
295 snapshots=[_snap_input(snap_id, MANIFEST_A)],
296 objects=[],
297 force=True,
298 author="tester",
299 )
300
301 from musehub.services.musehub_snapshot import get_snapshot_manifest
302 manifest = await get_snapshot_manifest(db_session, snap_id)
303 assert manifest == MANIFEST_A
304
305
306 @pytest.mark.asyncio
307 async def test_ingest_push_entries_idempotent_on_repush(
308 db_session: AsyncSession,
309 ) -> None:
310 """Re-pushing the same snapshot via ingest_push does not duplicate entries."""
311 slug = "repo-ip-idem-001"
312 repo_id = _make_repo_id(slug)
313 snap_id = "snap-ip-idem-001"
314 commit_id = _uid()
315
316 db_session.add(_repo(slug))
317 await db_session.flush()
318
319 for _ in range(3):
320 await ingest_push(
321 db_session,
322 repo_id=repo_id,
323 branch="dev",
324 head_commit_id=commit_id,
325 commits=[_commit_input(commit_id, snap_id)],
326 snapshots=[_snap_input(snap_id, MANIFEST_A)],
327 objects=[],
328 force=True,
329 author="tester",
330 )
331
332 from musehub.services.musehub_snapshot import get_snapshot_manifest
333 result = await get_snapshot_manifest(db_session, snap_id)
334 assert result == MANIFEST_A
335
336
337 # ---------------------------------------------------------------------------
338 # 4. wire_push write path (muse CLI push path)
339 # ---------------------------------------------------------------------------
340
341
342 async def _run_wire_push_stream(
343 db_session: AsyncSession,
344 repo_id: str,
345 user_id: str,
346 commit_id: str,
347 snap_id: str,
348 manifest: StrDict,
349 *,
350 force: bool = False,
351 ) -> JSONObject:
352 """Drive wire_push_stream with a commit+snapshot pack (no objects).
353
354 Pre-seeds MusehubObject + MusehubObjectRef rows for all manifest values so
355 wire_push_stream's object-existence validation passes.
356 """
357 from unittest.mock import patch
358
359 # Pre-seed objects referenced by the manifest so the server's object-existence
360 # check does not reject the push.
361 for path, oid in manifest.items():
362 existing = await db_session.get(db.MusehubObject, oid)
363 if existing is None:
364 db_session.add(db.MusehubObject(
365 object_id=oid, path=path, size_bytes=1, disk_path="",
366 ))
367 ref_result = await db_session.execute(
368 __import__("sqlalchemy", fromlist=["select"]).select(db.MusehubObjectRef).where(
369 db.MusehubObjectRef.repo_id == repo_id,
370 db.MusehubObjectRef.object_id == oid,
371 )
372 )
373 if ref_result.scalar_one_or_none() is None:
374 db_session.add(db.MusehubObjectRef(repo_id=repo_id, object_id=oid))
375 await db_session.flush()
376
377 fw = MuseWireFrameWriter()
378
379 def _wrap(ft: str, data: JSONValue) -> bytes:
380 return fw.wrap(frame_type=ft, payload=msgpack.packb(data, use_bin_type=True))
381
382 body = (
383 _wrap(SFRAME_HEADER, {
384 "t": SFRAME_HEADER, "branch": "dev", "force": force,
385 "have": [], "head": commit_id, "n_objects": 0, "n_commits": 1,
386 })
387 + _wrap(SFRAME_COMMIT_PACK, {
388 "t": SFRAME_COMMIT_PACK,
389 "commits": [{
390 "commit_id": commit_id,
391 "branch": "dev",
392 "snapshot_id": snap_id,
393 "message": "test commit",
394 "author": "tester",
395 "committed_at": "2026-01-01T00:00:00Z",
396 }],
397 "snapshots": [{
398 "snapshot_id": snap_id,
399 "manifest": manifest,
400 "created_at": "2026-01-01T00:00:00Z",
401 }],
402 })
403 + _wrap(SFRAME_END, {"t": SFRAME_END, "n_objects": 0, "n_commits": 1})
404 )
405
406 async def body_iter() -> None:
407 yield body
408
409 # Pre-seeded objects exist in DB but not in storage — mock the backend so
410 # the ghost-object guard treats them as present in storage.
411 seeded_oids = set(manifest.values())
412
413 from unittest.mock import AsyncMock, MagicMock
414 mock_backend = MagicMock()
415 mock_backend.exists = AsyncMock(side_effect=lambda oid, **_: oid in seeded_oids)
416 mock_backend.put = AsyncMock(return_value="mock://object")
417 mock_backend.presign_batch = AsyncMock(return_value={})
418
419 frames: list[dict] = []
420 with patch("musehub.services.musehub_wire.settings") as mock_settings, \
421 patch("musehub.services.musehub_wire.get_backend", return_value=mock_backend):
422 mock_settings.per_repo_quota_bytes = 100 * 1024 * 1024
423 mock_settings.require_signed_commits = False
424 mock_settings.trusted_agent_ids = []
425 async for chunk in wire_push_stream(db_session, repo_id, body_iter(), pusher_id=user_id):
426 unpacker = msgpack.Unpacker(raw=False)
427 unpacker.feed(chunk)
428 frames.extend(list(unpacker))
429
430 return frames[-1] if frames else {}
431
432
433 @pytest.mark.asyncio
434 async def test_wire_push_stores_snapshot_entries(
435 db_session: AsyncSession,
436 ) -> None:
437 """wire_push_stream must write entries to musehub_snapshot_entries."""
438 slug = "repo-wire-001"
439 repo_id = _make_repo_id(slug)
440 user_id = "testuser"
441 snap_id = _uid()
442 commit_id = _uid()
443
444 db_session.add(_repo(slug))
445 await db_session.flush()
446
447 result = await _run_wire_push_stream(db_session, repo_id, user_id, commit_id, snap_id, MANIFEST_WIRE)
448
449 assert result.get("ok") is True, f"wire_push_stream failed: {result}"
450 from musehub.services.musehub_snapshot import get_snapshot_manifest
451 manifest = await get_snapshot_manifest(db_session, snap_id)
452 assert manifest == MANIFEST_WIRE
453
454
455 @pytest.mark.asyncio
456 async def test_wire_push_entries_idempotent_on_repush(
457 db_session: AsyncSession,
458 ) -> None:
459 """Re-pushing the same snapshot via wire_push_stream does not duplicate entries."""
460 slug = "repo-wire-idem-001"
461 repo_id = _make_repo_id(slug)
462 user_id = "testuser"
463 snap_id = _uid()
464 commit_id = _uid()
465
466 db_session.add(_repo(slug))
467 await db_session.flush()
468
469 await _run_wire_push_stream(db_session, repo_id, user_id, commit_id, snap_id, MANIFEST_WIRE, force=True)
470 result = await _run_wire_push_stream(db_session, repo_id, user_id, commit_id, snap_id, MANIFEST_WIRE, force=True)
471
472 assert result.get("ok") is True, f"second push failed: {result}"
473 from musehub.services.musehub_snapshot import get_snapshot_manifest
474 manifest = await get_snapshot_manifest(db_session, snap_id)
475 assert manifest == MANIFEST_WIRE
476
477
478 @pytest.mark.asyncio
479 async def test_wire_push_repairs_stale_snapshot(
480 db_session: AsyncSession,
481 ) -> None:
482 """
483 Regression test: wire_push_stream correctly stores the manifest for a new snapshot.
484
485 Verifies that pushing a snapshot_id that does not yet exist in the DB
486 correctly stores the manifest so subsequent pulls can reconstruct the file tree.
487 """
488 slug = "repo-wire-stale-001"
489 repo_id = _make_repo_id(slug)
490 user_id = "testuser"
491 snap_id = _uid()
492 commit_id = _uid()
493
494 db_session.add(_repo(slug))
495 await db_session.flush()
496
497 result = await _run_wire_push_stream(db_session, repo_id, user_id, commit_id, snap_id, MANIFEST_WIRE, force=True)
498
499 assert result.get("ok") is True, f"wire_push_stream failed: {result}"
500 from musehub.services.musehub_snapshot import get_snapshot_manifest
501 manifest = await get_snapshot_manifest(db_session, snap_id)
502 assert manifest == MANIFEST_WIRE, (
503 "manifest was not stored — wire_push_stream must write the manifest on push"
504 )
505
506
507 # ---------------------------------------------------------------------------
508 # 5. Read path — repository service functions
509 # ---------------------------------------------------------------------------
510
511
512 @pytest.mark.asyncio
513 async def test_get_file_at_ref_resolves_via_entries(
514 db_session: AsyncSession,
515 ) -> None:
516 """get_file_at_ref must resolve object_id from snapshot entries, not manifest blob."""
517 from musehub.services.musehub_repository import get_file_at_ref
518 from musehub.services.musehub_snapshot import upsert_snapshot_entries
519
520 slug = "repo-gar-001"
521 repo_id = _make_repo_id(slug)
522 snap_id = "snap-gar-001"
523 commit_id = _uid()
524
525 db_session.add(_repo(slug))
526 await db_session.flush()
527 await upsert_snapshot_entries(db_session, repo_id, snap_id, MANIFEST_A)
528 await db_session.flush()
529 db_session.add(db.MusehubCommit(
530 commit_id=commit_id, repo_id=repo_id, branch="dev",
531 parent_ids=[], message="m", author="a",
532 timestamp=datetime.now(timezone.utc), snapshot_id=snap_id,
533 ))
534 db_session.add(db.MusehubBranch(
535 branch_id=compute_branch_id(repo_id, "dev"),
536 repo_id=repo_id, name="dev", head_commit_id=commit_id,
537 ))
538 await db_session.flush()
539
540 result = await get_file_at_ref(db_session, repo_id, "dev", "README.md")
541
542 assert result is not None
543 assert result["object_id"] == MANIFEST_A["README.md"]
544 assert result["path"] == "README.md"
545
546
547 @pytest.mark.asyncio
548 async def test_get_snapshot_diff_via_entries(
549 db_session: AsyncSession,
550 ) -> None:
551 """get_snapshot_diff must diff two snapshots using entries, not manifest blobs."""
552 from musehub.services.musehub_repository import get_snapshot_diff
553 from musehub.services.musehub_snapshot import upsert_snapshot_entries
554
555 slug = "repo-diff-001"
556 repo_id = _make_repo_id(slug)
557 snap_a = "snap-diff-a"
558 snap_b = "snap-diff-b"
559
560 db_session.add(_repo(slug))
561 await db_session.flush()
562
563 await upsert_snapshot_entries(db_session, repo_id, snap_a, MANIFEST_A)
564 await upsert_snapshot_entries(db_session, repo_id, snap_b, MANIFEST_B)
565 await db_session.flush()
566
567 diff = await get_snapshot_diff(db_session, repo_id, snap_b, snap_a)
568
569 # musehub/new_file.py is in B but not A → added
570 assert "musehub/new_file.py" in diff["added"]
571 # README.md and models.py changed object_id → modified
572 assert "README.md" in diff["modified"]
573 assert "musehub/db/models.py" in diff["modified"]
574 # main.py is unchanged
575 assert "musehub/main.py" not in diff["modified"]
576 assert "musehub/main.py" not in diff["added"]
577 assert "musehub/main.py" not in diff["removed"]
578
579
580 @pytest.mark.asyncio
581 async def test_get_file_last_commits_via_entries(
582 db_session: AsyncSession,
583 ) -> None:
584 """get_file_last_commits must walk entries, not manifest blobs."""
585 from musehub.services.musehub_repository import get_file_last_commits
586 from musehub.services.musehub_snapshot import upsert_snapshot_entries
587
588 slug = "repo-flc-001"
589 repo_id = _make_repo_id(slug)
590 snap_1 = "snap-flc-001"
591 snap_2 = "snap-flc-002"
592 commit_1 = _uid()
593 commit_2 = _uid()
594
595 db_session.add(_repo(slug))
596 await db_session.flush()
597
598 await upsert_snapshot_entries(db_session, repo_id, snap_1, MANIFEST_A)
599 await upsert_snapshot_entries(db_session, repo_id, snap_2, MANIFEST_B)
600 await db_session.flush()
601
602 # commit_1 (older): only MANIFEST_A
603 db_session.add(db.MusehubCommit(
604 commit_id=commit_1, repo_id=repo_id, branch="dev",
605 parent_ids=[], message="initial", author="a",
606 timestamp=datetime(2026, 1, 1, tzinfo=timezone.utc), snapshot_id=snap_1,
607 ))
608 # commit_2 (newer): MANIFEST_B — README.md changed
609 db_session.add(db.MusehubCommit(
610 commit_id=commit_2, repo_id=repo_id, branch="dev",
611 parent_ids=[commit_1], message="update readme", author="a",
612 timestamp=datetime(2026, 1, 2, tzinfo=timezone.utc), snapshot_id=snap_2,
613 ))
614 await db_session.flush()
615
616 result = await get_file_last_commits(db_session, repo_id, ["README.md", "musehub/main.py"])
617
618 # README.md changed in commit_2 — should be attributed there
619 assert "README.md" in result
620 assert result["README.md"]["sha"] == commit_2
621
622 # main.py was unchanged in commit_2 — attributed to commit_1
623 assert "musehub/main.py" in result
624 assert result["musehub/main.py"]["sha"] == commit_1
625
626
627 @pytest.mark.asyncio
628 async def test_list_tree_resolves_via_entries(
629 db_session: AsyncSession,
630 ) -> None:
631 """list_tree must build the directory listing from snapshot entries."""
632 from musehub.services.musehub_repository import list_tree
633 from musehub.services.musehub_snapshot import upsert_snapshot_entries
634
635 slug = "repo-lt-001"
636 repo_id = _make_repo_id(slug)
637 snap_id = "snap-lt-001"
638 commit_id = _uid()
639
640 db_session.add(_repo(slug))
641 await db_session.flush()
642 await upsert_snapshot_entries(db_session, repo_id, snap_id, MANIFEST_A)
643 await db_session.flush()
644 db_session.add(db.MusehubCommit(
645 commit_id=commit_id, repo_id=repo_id, branch="dev",
646 parent_ids=[], message="m", author="a",
647 timestamp=datetime.now(timezone.utc), snapshot_id=snap_id,
648 ))
649 db_session.add(db.MusehubBranch(
650 branch_id=compute_branch_id(repo_id, "dev"),
651 repo_id=repo_id, name="dev", head_commit_id=commit_id,
652 ))
653 await db_session.flush()
654
655 tree = await list_tree(db_session, repo_id, "t", repo_id, ref="dev", dir_path="")
656
657 names = {e.name for e in tree.entries}
658 assert "README.md" in names
659 assert "musehub" in names # directory inferred from paths
660
661
662 # ---------------------------------------------------------------------------
663 # 6. wire fetch — object dedup uses entries
664 # ---------------------------------------------------------------------------
665
666
667 @pytest.mark.asyncio
668 async def test_wire_fetch_includes_objects_from_entries(
669 db_session: AsyncSession,
670 ) -> None:
671 """
672 wire_fetch / _to_wire_snapshot must reconstruct manifest from entries
673 so that object deduplication works correctly on pull.
674 """
675 from musehub.services.musehub_wire import _to_wire_snapshot
676 from musehub.services.musehub_snapshot import upsert_snapshot_entries
677
678 slug = "repo-fetch-001"
679 repo_id = _make_repo_id(slug)
680 snap_id = _uid()
681
682 db_session.add(_repo(slug))
683 await db_session.flush()
684 await upsert_snapshot_entries(db_session, repo_id, snap_id, MANIFEST_A)
685 await db_session.flush()
686
687 snap_row = await db_session.get(db.MusehubSnapshot, snap_id)
688 assert snap_row is not None
689
690 wire_snap = await _to_wire_snapshot(db_session, snap_row)
691
692 assert wire_snap.manifest == MANIFEST_A
File History 1 commit
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 121 days ago