gabriel / musehub public
test_wire_presign_push.py python
696 lines 23.2 KB
Raw
sha256:763eb2cb8675073b84c19345b27586d2ed939a9aee97c5479b69f502f1a70eff fix(tests): update test suite to match current implementation Sonnet 4.6 patch 120 days ago
1 """TDD — R2 presigned push path.
2
3 Problem
4 -------
5 Cloudflare times out streaming POSTs after ~100 seconds. Repos with
6 more than ~500 objects (e.g. the muse repo at 6905 objects / 191 MB raw)
7 fail mid-upload. The fix: for large pushes the client calls
8 ``POST /push/presign`` first, PUTs objects directly to R2 (bypassing CF),
9 then sends a compressed H+C+E stream with zero O frames. The server's
10 existing referential integrity check (lines 1758-1786 of musehub_wire.py)
11 already handles objects that arrive pre-stored.
12
13 Test plan
14 ---------
15 P1 wire_push_presign — LocalBackend: missing objects land in stream_these,
16 already-stored objects land in already_stored, presigned_urls is empty.
17
18 P2 wire_push_presign — mocked S3Backend: missing objects get presigned PUT
19 URLs; already-stored objects land in already_stored.
20
21 P3 wire_push_presign — empty object list returns all-empty response.
22
23 P4 push/stream with zero O frames and all objects pre-stored in the DB
24 finalises the commit successfully (referential integrity passes).
25
26 P5 push/stream with zero O frames but objects NOT in storage returns 422
27 (referential integrity rejects the push).
28
29 P6 Full round-trip: presign → store objects via backend.put → push stream
30 with zero O frames → /refs reflects the new head.
31
32 P7 S3Backend.presign_batch generates correctly-keyed URLs for a batch of
33 object IDs (mocked boto3; no real AWS call).
34
35 Confirm step (missing from initial implementation — discovered via staging test)
36 --------------------------------------------------------------------------------
37 The presigned PUT goes directly to R2, bypassing the server entirely. The
38 server therefore has no DB record for those objects, so the referential
39 integrity check on the zero-O-frame push/stream rejects with 422.
40
41 Fix: the client calls ``POST /push/confirm`` after all R2 PUTs succeed.
42 The server inserts a ``musehub_objects`` row (using ``backend.uri_for(oid)``)
43 and a ``musehub_object_refs`` row for each confirmed object, then the zero-O
44 push/stream passes the integrity check.
45
46 P9 wire_push_confirm inserts DB rows for confirmed objects; subsequent
47 push/stream with zero O frames succeeds.
48 P10 wire_push_confirm is idempotent — confirming the same object twice
49 does not duplicate rows or raise.
50 P11 Full end-to-end: presign → R2 PUT → confirm → zero-O push/stream →
51 commit is accessible via wire_refs.
52 """
53 from __future__ import annotations
54 from collections.abc import AsyncIterator
55
56 import asyncio
57 import zlib
58 from datetime import datetime, timezone
59 from unittest.mock import AsyncMock, MagicMock, patch
60
61 import msgpack
62 import pytest
63 from sqlalchemy.dialects.postgresql import insert as pg_insert
64 from sqlalchemy.ext.asyncio import AsyncSession
65
66 from muse.core.types import blob_id, fake_id
67 from musehub.db import musehub_models as db
68 from musehub.models.wire import (
69 SFRAME_COMMIT_PACK,
70 SFRAME_END,
71 SFRAME_HEADER,
72 SFRAME_OBJECT,
73 )
74 from muse.core.mpack import MuseWireFrameWriter
75 from musehub.types.json_types import JSONObject
76 from tests.factories import create_repo
77
78 _fw = MuseWireFrameWriter()
79
80
81 # ---------------------------------------------------------------------------
82 # Helpers
83 # ---------------------------------------------------------------------------
84
85 def _now() -> datetime:
86 return datetime.now(tz=timezone.utc)
87
88
89 def _uid(seed: str) -> str:
90 return fake_id(seed)
91
92
93 def _pack(data: JSONObject) -> bytes:
94 return msgpack.packb(data, use_bin_type=True)
95
96
97 def _wrap(ft: str, data: JSONObject) -> bytes:
98 return _fw.wrap(frame_type=ft, payload=_pack(data))
99
100
101 def _header_frame(
102 n_objects: int = 0,
103 n_commits: int = 1,
104 branch: str = "main",
105 force: bool = True,
106 ) -> bytes:
107 return _wrap(SFRAME_HEADER, {
108 "t": SFRAME_HEADER,
109 "branch": branch,
110 "force": force,
111 "n_objects": n_objects,
112 "n_commits": n_commits,
113 })
114
115
116 def _end_frame(n_objects: int = 0, n_commits: int = 1) -> bytes:
117 return _wrap(SFRAME_END, {
118 "t": SFRAME_END,
119 "n_objects": n_objects,
120 "n_commits": n_commits,
121 })
122
123
124 def _commit_pack_frame(commits: list[dict], snapshots: list[dict] | None = None) -> bytes:
125 return _wrap(SFRAME_COMMIT_PACK, {
126 "t": SFRAME_COMMIT_PACK,
127 "commits": commits,
128 "snapshots": snapshots or [],
129 })
130
131
132 def _o_frame(oid: str, content: bytes, path: str = "file.py") -> bytes:
133 return _wrap(SFRAME_OBJECT, {
134 "t": SFRAME_OBJECT,
135 "id": oid,
136 "path": path,
137 "content": zlib.compress(content, level=1),
138 "enc": "zlib",
139 "size": len(content),
140 })
141
142
143 async def _store_object(
144 session: AsyncSession,
145 repo_id: str,
146 oid: str,
147 content: bytes,
148 *,
149 owner: str,
150 slug: str,
151 ) -> None:
152 """Insert object directly into DB + local storage (bypasses wire protocol)."""
153 from musehub.services.musehub_wire import get_backend
154 from musehub.storage.backends import repo_root_for
155 backend = get_backend()
156 repo_root = repo_root_for(owner, slug)
157 uri = await backend.put(oid, content, repo_root=repo_root)
158 await session.execute(
159 pg_insert(db.MusehubObject)
160 .values(
161 object_id=oid,
162 path="",
163 size_bytes=len(content),
164 disk_path=uri.replace("local://", ""),
165 storage_uri=uri,
166 )
167 .on_conflict_do_nothing(index_elements=["object_id"])
168 )
169 await session.execute(
170 pg_insert(db.MusehubObjectRef)
171 .values(repo_id=repo_id, object_id=oid)
172 .on_conflict_do_nothing()
173 )
174 await session.commit()
175
176
177 async def _make_commit_and_snapshot(
178 session: AsyncSession,
179 repo_id: str,
180 *,
181 manifest: dict[str, str],
182 commit_seed: str = "c1",
183 parent_ids: list[str] | None = None,
184 ) -> tuple[db.MusehubCommit, db.MusehubSnapshot]:
185 snap_id = _uid(f"snap-{commit_seed}")
186 snap = db.MusehubSnapshot(
187 snapshot_id=snap_id,
188 repo_id=repo_id,
189 directories=[],
190 manifest_blob=msgpack.packb(manifest, use_bin_type=True),
191 entry_count=len(manifest),
192 created_at=_now(),
193 )
194 session.add(snap)
195 commit_id = _uid(f"commit-{commit_seed}")
196 commit = db.MusehubCommit(
197 commit_id=commit_id,
198 repo_id=repo_id,
199 branch="main",
200 parent_ids=parent_ids or [],
201 message=f"commit {commit_seed}",
202 author="gabriel",
203 timestamp=_now(),
204 snapshot_id=snap_id,
205 )
206 session.add(commit)
207 await session.commit()
208 return commit, snap
209
210
211 async def _collect_frames(gen: AsyncIterator[bytes]) -> list[JSONObject]:
212 chunks: list[bytes] = []
213 async for chunk in gen:
214 chunks.append(chunk)
215 unpacker = msgpack.Unpacker(raw=False)
216 for chunk in chunks:
217 unpacker.feed(chunk)
218 return list(unpacker)
219
220
221 async def _body_iter(raw: bytes) -> None:
222 yield raw
223
224
225 # ---------------------------------------------------------------------------
226 # P1 — LocalBackend: missing → stream_these, present → already_stored
227 # ---------------------------------------------------------------------------
228
229 @pytest.mark.asyncio
230 async def test_p1_local_backend_missing_goes_to_stream_these(
231 db_session: AsyncSession,
232 ) -> None:
233 """LocalBackend.presign_batch returns {} → missing oids land in stream_these."""
234 from musehub.services.musehub_wire import wire_push_presign
235
236 repo = await create_repo(db_session, owner="gabriel")
237 repo_id = str(repo.repo_id)
238
239 content_a = b"object-A content for presign test"
240 content_b = b"object-B content for presign test"
241 oid_a = blob_id(content_a)
242 oid_b = blob_id(content_b)
243
244 # Pre-store A so it lands in already_stored.
245 await _store_object(db_session, repo_id, oid_a, content_a, owner=repo.owner, slug=repo.slug)
246
247 result = await wire_push_presign(
248 db_session,
249 repo_id,
250 [oid_a, oid_b],
251 )
252
253 assert oid_a in result["already_stored"]
254 assert oid_b not in result["already_stored"]
255 # LocalBackend returns no presigned URLs.
256 assert result["presigned_urls"] == {}
257 # Missing object with no presigned URL goes to stream_these.
258 assert oid_b in result["stream_these"]
259 assert oid_a not in result["stream_these"]
260
261
262 # ---------------------------------------------------------------------------
263 # P2 — S3Backend: missing → presigned_urls
264 # ---------------------------------------------------------------------------
265
266 @pytest.mark.asyncio
267 async def test_p2_s3_backend_missing_gets_presigned_url(
268 db_session: AsyncSession,
269 ) -> None:
270 """S3Backend.presign_batch is called for missing objects; URLs are returned."""
271 from musehub.services.musehub_wire import wire_push_presign
272
273 repo = await create_repo(db_session, owner="gabriel")
274 repo_id = str(repo.repo_id)
275
276 content = b"object content for s3 presign test"
277 oid = blob_id(content)
278 fake_url = f"https://r2.example.com/objects/{oid}?X-Amz-Signature=abc"
279
280 mock_backend = MagicMock()
281 mock_backend.presign_batch = AsyncMock(return_value={oid: fake_url})
282
283 with patch("musehub.services.musehub_wire.get_backend", return_value=mock_backend):
284 result = await wire_push_presign(
285 db_session,
286 repo_id,
287 [oid],
288 )
289
290 assert result["presigned_urls"] == {oid: fake_url}
291 assert result["already_stored"] == []
292 assert result["stream_these"] == []
293 mock_backend.presign_batch.assert_called_once_with([oid], "put", 3600)
294
295
296 # ---------------------------------------------------------------------------
297 # P3 — empty object list → all-empty response
298 # ---------------------------------------------------------------------------
299
300 @pytest.mark.asyncio
301 async def test_p3_empty_object_list(db_session: AsyncSession) -> None:
302 from musehub.services.musehub_wire import wire_push_presign
303
304 repo = await create_repo(db_session, owner="gabriel")
305
306 result = await wire_push_presign(db_session, str(repo.repo_id), [])
307
308 assert result["presigned_urls"] == {}
309 assert result["already_stored"] == []
310 assert result["stream_these"] == []
311
312
313 # ---------------------------------------------------------------------------
314 # P4 — zero O frames, all objects pre-stored → commit finalises
315 # ---------------------------------------------------------------------------
316
317 @pytest.mark.asyncio
318 async def test_p4_zero_o_frames_all_prestored_commit_finalises(
319 db_session: AsyncSession,
320 ) -> None:
321 """H + C + E with n_objects=0 succeeds when snapshot objects are in the DB."""
322 from musehub.services.musehub_wire import wire_push_stream
323
324 repo = await create_repo(db_session, owner="gabriel")
325 repo_id = str(repo.repo_id)
326
327 content = b"pre-stored object content p4"
328 oid = blob_id(content)
329 await _store_object(db_session, repo_id, oid, content, owner=repo.owner, slug=repo.slug)
330
331 snap_id = _uid("snap-p4")
332 commit_id = _uid("commit-p4")
333
334 commit_wire = {
335 "commit_id": commit_id,
336 "parent_ids": [],
337 "message": "presign round-trip",
338 "author": "gabriel",
339 "timestamp": _now().isoformat(),
340 "snapshot_id": snap_id,
341 "branch": "main",
342 }
343 snap_wire = {
344 "snapshot_id": snap_id,
345 "manifest": {"file.py": oid},
346 "directories": [],
347 "created_at": _now().isoformat(),
348 }
349
350 raw = (
351 _header_frame(n_objects=0, n_commits=1)
352 + _commit_pack_frame([commit_wire], [snap_wire])
353 + _end_frame(n_objects=0, n_commits=1)
354 )
355
356 frames = await _collect_frames(
357 wire_push_stream(db_session, repo_id, _body_iter(raw), "gabriel")
358 )
359
360 # No error frames.
361 errors = [f for f in frames if f.get("t") == "X"]
362 assert errors == [], f"unexpected error frames: {errors}"
363
364 # Commit exists in DB.
365 from sqlalchemy import select
366 row = (await db_session.execute(
367 select(db.MusehubCommit).where(db.MusehubCommit.commit_id == commit_id)
368 )).scalar_one_or_none()
369 assert row is not None
370
371
372 # ---------------------------------------------------------------------------
373 # P5 — zero O frames, objects NOT stored → 422
374 # ---------------------------------------------------------------------------
375
376 @pytest.mark.asyncio
377 async def test_p5_zero_o_frames_missing_objects_yields_422(
378 db_session: AsyncSession,
379 ) -> None:
380 """H + C + E with n_objects=0 but snapshot references absent objects → 422."""
381 from musehub.services.musehub_wire import wire_push_stream
382
383 repo = await create_repo(db_session, owner="gabriel")
384 repo_id = str(repo.repo_id)
385
386 # This oid is NOT stored anywhere.
387 oid = blob_id(b"ghost object never uploaded")
388
389 snap_id = _uid("snap-p5")
390 commit_id = _uid("commit-p5")
391
392 commit_wire = {
393 "commit_id": commit_id,
394 "parent_ids": [],
395 "message": "should fail",
396 "author": "gabriel",
397 "timestamp": _now().isoformat(),
398 "snapshot_id": snap_id,
399 "branch": "main",
400 }
401 snap_wire = {
402 "snapshot_id": snap_id,
403 "manifest": {"missing.py": oid},
404 "directories": [],
405 "created_at": _now().isoformat(),
406 }
407
408 raw = (
409 _header_frame(n_objects=0, n_commits=1)
410 + _commit_pack_frame([commit_wire], [snap_wire])
411 + _end_frame(n_objects=0, n_commits=1)
412 )
413
414 frames = await _collect_frames(
415 wire_push_stream(db_session, repo_id, _body_iter(raw), "gabriel")
416 )
417
418 error_frames = [f for f in frames if f.get("t") == "X"]
419 assert error_frames, "expected an error frame for missing objects"
420 assert error_frames[0].get("code") == 422
421
422
423 # ---------------------------------------------------------------------------
424 # P6 — full round-trip: presign → store → push stream → /refs reflects head
425 # ---------------------------------------------------------------------------
426
427 @pytest.mark.asyncio
428 async def test_p6_full_round_trip_presign_store_push(
429 db_session: AsyncSession,
430 ) -> None:
431 """presign returns oids to upload; after manual storage, zero-O-frame push works."""
432 from musehub.services.musehub_wire import wire_push_presign, wire_push_stream
433
434 repo = await create_repo(db_session, owner="gabriel")
435 repo_id = str(repo.repo_id)
436
437 content = b"p6 round-trip object bytes"
438 oid = blob_id(content)
439
440 # Step 1: Ask presign endpoint what to upload.
441 presign_result = await wire_push_presign(db_session, repo_id, [oid])
442 assert oid in presign_result["stream_these"] # LocalBackend → no presigned URL
443
444 # Step 2: Client "uploads" directly to storage (simulates R2 PUT via presigned URL).
445 await _store_object(db_session, repo_id, oid, content, owner=repo.owner, slug=repo.slug)
446
447 # Step 3: Now presign endpoint sees it as already_stored.
448 presign_result2 = await wire_push_presign(db_session, repo_id, [oid])
449 assert oid in presign_result2["already_stored"]
450 assert presign_result2["stream_these"] == []
451
452 # Step 4: Push H + C + E with zero O frames.
453 snap_id = _uid("snap-p6")
454 commit_id = _uid("commit-p6")
455 commit_wire = {
456 "commit_id": commit_id,
457 "parent_ids": [],
458 "message": "p6 round-trip",
459 "author": "gabriel",
460 "timestamp": _now().isoformat(),
461 "snapshot_id": snap_id,
462 "branch": "main",
463 }
464 snap_wire = {
465 "snapshot_id": snap_id,
466 "manifest": {"round_trip.py": oid},
467 "directories": [],
468 "created_at": _now().isoformat(),
469 }
470 raw = (
471 _header_frame(n_objects=0, n_commits=1)
472 + _commit_pack_frame([commit_wire], [snap_wire])
473 + _end_frame(n_objects=0, n_commits=1)
474 )
475 frames = await _collect_frames(
476 wire_push_stream(db_session, repo_id, _body_iter(raw), "gabriel")
477 )
478
479 errors = [f for f in frames if f.get("t") == "X"]
480 assert errors == [], f"push failed: {errors}"
481
482 # Step 5: Verify commit is in the DB.
483 from sqlalchemy import select
484 row = (await db_session.execute(
485 select(db.MusehubCommit).where(db.MusehubCommit.commit_id == commit_id)
486 )).scalar_one_or_none()
487 assert row is not None
488 assert row.branch == "main"
489
490
491 # ---------------------------------------------------------------------------
492 # P7 — S3Backend.presign_batch produces correctly-keyed URLs
493 # ---------------------------------------------------------------------------
494
495 @pytest.mark.asyncio
496 async def test_p7_s3_backend_presign_batch_url_format() -> None:
497 """S3Backend.presign_batch calls boto3 with the right key format per oid."""
498 from musehub.storage.backends import S3Backend
499
500 oid = blob_id(b"test object for presign key format")
501 expected_key = f"objects/{oid}"
502 fake_url = f"https://bucket.r2.cloudflarestorage.com/{expected_key}?sig=abc"
503
504 mock_client = MagicMock()
505 mock_client.generate_presigned_url.return_value = fake_url
506
507 backend = S3Backend(
508 bucket="test-bucket",
509 region="auto",
510 endpoint_url="https://r2.example.com",
511 access_key_id="key",
512 secret_access_key="secret",
513 )
514 backend._client = mock_client
515
516 result = await backend.presign_batch([oid], "put", 3600)
517
518 assert result == {oid: fake_url}
519 mock_client.generate_presigned_url.assert_called_once_with(
520 "put_object",
521 Params={"Bucket": "test-bucket", "Key": expected_key},
522 ExpiresIn=3600,
523 )
524
525
526 # ---------------------------------------------------------------------------
527 # P9 — confirm inserts DB rows; zero-O push/stream then succeeds
528 # ---------------------------------------------------------------------------
529
530 @pytest.mark.asyncio
531 async def test_p9_confirm_registers_objects_and_push_succeeds(
532 db_session: AsyncSession,
533 ) -> None:
534 """wire_push_confirm creates DB records; referential integrity check passes."""
535 from musehub.services.musehub_wire import wire_push_confirm, wire_push_stream
536
537 repo = await create_repo(db_session, owner="gabriel")
538 repo_id = str(repo.repo_id)
539
540 content = b"p9 object bytes confirm test"
541 oid = blob_id(content)
542
543 # Simulate: client PUT raw bytes to R2 (storage has bytes but no DB row).
544 from musehub.services.musehub_wire import get_backend
545 from musehub.storage.backends import repo_root_for
546 backend = get_backend()
547 await backend.put(oid, content, repo_root=repo_root_for(repo.owner, repo.slug))
548
549 # Confirm step: server registers the object.
550 await wire_push_confirm(
551 db_session,
552 repo_id,
553 objects=[{"object_id": oid, "size_bytes": len(content), "path": "p9.py"}],
554 )
555
556 # Now zero-O push/stream should pass referential integrity.
557 snap_id = _uid("snap-p9")
558 commit_id = _uid("commit-p9")
559 commit_wire = {
560 "commit_id": commit_id,
561 "parent_ids": [],
562 "message": "p9 confirm test",
563 "author": "gabriel",
564 "timestamp": _now().isoformat(),
565 "snapshot_id": snap_id,
566 "branch": "main",
567 }
568 snap_wire = {
569 "snapshot_id": snap_id,
570 "manifest": {"p9.py": oid},
571 "directories": [],
572 "created_at": _now().isoformat(),
573 }
574 raw = (
575 _header_frame(n_objects=0, n_commits=1)
576 + _commit_pack_frame([commit_wire], [snap_wire])
577 + _end_frame(n_objects=0, n_commits=1)
578 )
579 frames = await _collect_frames(
580 wire_push_stream(db_session, repo_id, _body_iter(raw), "gabriel")
581 )
582 errors = [f for f in frames if f.get("t") == "X"]
583 assert errors == [], f"push failed after confirm: {errors}"
584
585 from sqlalchemy import select
586 row = (await db_session.execute(
587 select(db.MusehubCommit).where(db.MusehubCommit.commit_id == commit_id)
588 )).scalar_one_or_none()
589 assert row is not None
590
591
592 # ---------------------------------------------------------------------------
593 # P10 — confirm is idempotent
594 # ---------------------------------------------------------------------------
595
596 @pytest.mark.asyncio
597 async def test_p10_confirm_is_idempotent(db_session: AsyncSession) -> None:
598 """Confirming the same object twice does not raise or duplicate rows."""
599 from musehub.services.musehub_wire import wire_push_confirm
600 from musehub.services.musehub_wire import get_backend
601 from musehub.storage.backends import repo_root_for
602
603 repo = await create_repo(db_session, owner="gabriel")
604 repo_id = str(repo.repo_id)
605
606 content = b"p10 idempotent confirm bytes"
607 oid = blob_id(content)
608 backend = get_backend()
609 await backend.put(oid, content, repo_root=repo_root_for(repo.owner, repo.slug))
610
611 obj_entry = [{"object_id": oid, "size_bytes": len(content), "path": "p10.py"}]
612
613 await wire_push_confirm(db_session, repo_id, obj_entry)
614 await wire_push_confirm(db_session, repo_id, obj_entry) # second call must not raise
615
616 from sqlalchemy import select, func
617 count = (await db_session.execute(
618 select(func.count()).where(db.MusehubObject.object_id == oid)
619 )).scalar()
620 assert count == 1
621
622
623 # ---------------------------------------------------------------------------
624 # P11 — full end-to-end: presign → R2 PUT → confirm → zero-O stream → refs
625 # ---------------------------------------------------------------------------
626
627 @pytest.mark.asyncio
628 async def test_p11_full_end_to_end_with_confirm(db_session: AsyncSession) -> None:
629 """presign → backend.put (simulate R2) → confirm → push/stream → commit in refs."""
630 from musehub.services.musehub_wire import (
631 wire_push_presign,
632 wire_push_confirm,
633 wire_push_stream,
634 )
635 from musehub.services.musehub_wire import get_backend
636
637 repo = await create_repo(db_session, owner="gabriel")
638 repo_id = str(repo.repo_id)
639
640 content = b"p11 end to end object bytes"
641 oid = blob_id(content)
642
643 # Step 1: presign → object is missing, goes to stream_these (LocalBackend)
644 presign_result = await wire_push_presign(db_session, repo_id, [oid])
645 assert oid in presign_result["stream_these"]
646
647 # Step 2: client "PUT" to R2 (simulate by calling backend.put directly)
648 from musehub.storage.backends import repo_root_for
649 backend = get_backend()
650 await backend.put(oid, content, repo_root=repo_root_for(repo.owner, repo.slug))
651
652 # Step 3: confirm — registers DB row
653 await wire_push_confirm(
654 db_session,
655 repo_id,
656 objects=[{"object_id": oid, "size_bytes": len(content), "path": "p11.py"}],
657 )
658
659 # Step 4: presign now shows object as already_stored
660 presign_result2 = await wire_push_presign(db_session, repo_id, [oid])
661 assert oid in presign_result2["already_stored"]
662
663 # Step 5: zero-O push/stream
664 snap_id = _uid("snap-p11")
665 commit_id = _uid("commit-p11")
666 commit_wire = {
667 "commit_id": commit_id,
668 "parent_ids": [],
669 "message": "p11 end to end",
670 "author": "gabriel",
671 "timestamp": _now().isoformat(),
672 "snapshot_id": snap_id,
673 "branch": "main",
674 }
675 snap_wire = {
676 "snapshot_id": snap_id,
677 "manifest": {"p11.py": oid},
678 "directories": [],
679 "created_at": _now().isoformat(),
680 }
681 raw = (
682 _header_frame(n_objects=0, n_commits=1)
683 + _commit_pack_frame([commit_wire], [snap_wire])
684 + _end_frame(n_objects=0, n_commits=1)
685 )
686 frames = await _collect_frames(
687 wire_push_stream(db_session, repo_id, _body_iter(raw), "gabriel")
688 )
689 errors = [f for f in frames if f.get("t") == "X"]
690 assert errors == [], f"end-to-end push failed: {errors}"
691
692 from sqlalchemy import select
693 commit_row = (await db_session.execute(
694 select(db.MusehubCommit).where(db.MusehubCommit.commit_id == commit_id)
695 )).scalar_one_or_none()
696 assert commit_row is not None
File History 1 commit
sha256:763eb2cb8675073b84c19345b27586d2ed939a9aee97c5479b69f502f1a70eff fix(tests): update test suite to match current implementation Sonnet 4.6 patch 120 days ago