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