gabriel / muse public
test_local_file_transport.py python
616 lines 24.4 KB
Raw
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 27 days ago
1 """Comprehensive tests for LocalFileTransport — unit, integration, security, stress.
2
3 Coverage matrix
4 ---------------
5 Unit
6 _repo_root : valid URL → resolved path; bad scheme; missing .muse/
7 fetch_remote_info : reads repo.json + branch heads
8 fetch_pack : delegates to build_pack with correct args
9 push_pack : fast-forward check; force flag; ref write; result shape
10 make_transport : file:// → LocalFileTransport; https:// → HttpTransport
11
12 Integration (two real repos on disk)
13 push from A → B via file://
14 pull-equivalent: fetch_remote_info + fetch_pack from B after push
15 round-trip: push A→B, verify B branch heads, then fetch B→A-mirror
16
17 Security
18 _repo_root with symlink target that has no .muse/ is rejected
19 push_pack with path-traversal branch name is rejected
20 push_pack with null-byte branch name is rejected
21 push_pack with non-fast-forward is rejected unless force=True
22
23 Stress
24 push bundle with 50 commits and 200 objects
25 """
26
27 from __future__ import annotations
28
29 import base64
30 import datetime
31 import hashlib
32 import json
33 import os
34 import pathlib
35
36 import pytest
37
38 from muse._version import __version__
39 from muse.core.object_store import write_object
40 from muse.core.pack import PackBundle, RemoteInfo
41 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
42 from muse.core.store import (
43 CommitRecord,
44 SnapshotRecord,
45 get_all_branch_heads,
46 get_head_commit_id,
47 read_commit,
48 write_commit,
49 write_snapshot,
50 )
51
52 from muse.core._types import Manifest
53 from muse.core.transport import (
54 HttpTransport,
55 LocalFileTransport,
56 TransportError,
57 make_transport,
58 )
59
60
61 # ---------------------------------------------------------------------------
62 # Helpers
63 # ---------------------------------------------------------------------------
64
65
66 def _sha(b: bytes) -> str:
67 return hashlib.sha256(b).hexdigest()
68
69
70 def _make_repo(path: pathlib.Path, branch: str = "main") -> pathlib.Path:
71 """Create a minimal initialised Muse repo at *path*."""
72 muse = path / ".muse"
73 (muse / "refs" / "heads").mkdir(parents=True)
74 (muse / "objects").mkdir()
75 (muse / "commits").mkdir()
76 (muse / "snapshots").mkdir()
77 (muse / "repo.json").write_text(
78 json.dumps({"repo_id": f"repo-{path.name}", "schema_version": __version__, "domain": "midi", "default_branch": branch})
79 )
80 (muse / "HEAD").write_text(f"ref: refs/heads/{branch}\n")
81 return path
82
83
84 def _add_commit(
85 root: pathlib.Path,
86 label: str,
87 branch: str = "main",
88 parent: str | None = None,
89 content: bytes = b"hello",
90 ) -> str:
91 """Write a commit with a real content-addressed ID and return it.
92
93 *label* is used only to derive a unique message so that different calls
94 with different labels produce different commit IDs even when all other
95 inputs are the same.
96 """
97 oid = _sha(content)
98 write_object(root, oid, content)
99 manifest: Manifest = {"file.txt": oid}
100 snap_id = compute_snapshot_id(manifest)
101 snap = SnapshotRecord(snapshot_id=snap_id, manifest=manifest)
102 write_snapshot(root, snap)
103 message = f"commit {label[:8]}"
104 committed_at = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
105 parent_ids = [parent] if parent else []
106 real_cid = compute_commit_id(parent_ids, snap_id, message, committed_at.isoformat())
107 commit = CommitRecord(
108 commit_id=real_cid,
109 repo_id=f"repo-{root.name}",
110 branch=branch,
111 snapshot_id=snap_id,
112 message=message,
113 committed_at=committed_at,
114 parent_commit_id=parent,
115 )
116 write_commit(root, commit)
117 (root / ".muse" / "refs" / "heads" / branch).write_text(real_cid)
118 return real_cid
119
120
121 # ---------------------------------------------------------------------------
122 # Unit — _repo_root
123 # ---------------------------------------------------------------------------
124
125
126 class TestRepoRoot:
127 def test_valid_url_returns_resolved_path(self, tmp_path: pathlib.Path) -> None:
128 repo = _make_repo(tmp_path / "myrepo")
129 url = f"file://{repo}"
130 result = LocalFileTransport._repo_root(url)
131 assert result == repo.resolve()
132
133 def test_invalid_scheme_raises_transport_error(self, tmp_path: pathlib.Path) -> None:
134 with pytest.raises(TransportError, match="file://"):
135 LocalFileTransport._repo_root("https://hub.example.com/repos/r1")
136
137 def test_missing_muse_dir_raises_404(self, tmp_path: pathlib.Path) -> None:
138 with pytest.raises(TransportError) as exc_info:
139 LocalFileTransport._repo_root(f"file://{tmp_path}")
140 assert exc_info.value.status_code == 404
141 assert ".muse/" in str(exc_info.value)
142
143 def test_path_with_double_dots_normalized(self, tmp_path: pathlib.Path) -> None:
144 """resolve() must collapse .. so the check is on the canonical path."""
145 repo = _make_repo(tmp_path / "repo")
146 # Construct a URL with a harmless .. that stays inside the repo.
147 url = f"file://{repo}/subdir/../"
148 # The path resolves to the repo root — .muse/ exists there.
149 result = LocalFileTransport._repo_root(url)
150 assert result == repo.resolve()
151
152 def test_symlink_target_with_no_muse_is_rejected(self, tmp_path: pathlib.Path) -> None:
153 """A symlink that resolves to a dir without .muse/ must raise TransportError."""
154 target = tmp_path / "innocent"
155 target.mkdir()
156 link = tmp_path / "evil_link"
157 link.symlink_to(target)
158 with pytest.raises(TransportError) as exc_info:
159 LocalFileTransport._repo_root(f"file://{link}")
160 assert exc_info.value.status_code == 404
161
162 def test_symlink_to_valid_repo_is_accepted(self, tmp_path: pathlib.Path) -> None:
163 """A symlink that resolves to a valid repo is accepted after resolve()."""
164 repo = _make_repo(tmp_path / "real_repo")
165 link = tmp_path / "alias"
166 link.symlink_to(repo)
167 result = LocalFileTransport._repo_root(f"file://{link}")
168 # Should return the canonical (resolved) path, not the symlink.
169 assert result == repo.resolve()
170
171
172 # ---------------------------------------------------------------------------
173 # Unit — fetch_remote_info
174 # ---------------------------------------------------------------------------
175
176
177 class TestFetchRemoteInfo:
178 def test_reads_repo_json_and_branch_heads(self, tmp_path: pathlib.Path) -> None:
179 repo = _make_repo(tmp_path / "remote")
180 cid = _add_commit(repo, "a" * 64)
181 t = LocalFileTransport()
182 info = t.fetch_remote_info(f"file://{repo}", signing=None)
183 assert info["repo_id"] == f"repo-{repo.name}"
184 assert info["domain"] == "midi"
185 assert info["default_branch"] == "main"
186 assert info["branch_heads"]["main"] == cid
187
188 def test_multiple_branches_returned(self, tmp_path: pathlib.Path) -> None:
189 repo = _make_repo(tmp_path / "remote")
190 cid_main = _add_commit(repo, "a" * 64, branch="main")
191 cid_dev = _add_commit(repo, "b" * 64, branch="dev")
192 t = LocalFileTransport()
193 info = t.fetch_remote_info(f"file://{repo}", signing=None)
194 assert info["branch_heads"]["main"] == cid_main
195 assert info["branch_heads"]["dev"] == cid_dev
196
197 def test_token_is_ignored(self, tmp_path: pathlib.Path) -> None:
198 """LocalFileTransport ignores the token arg — no auth for local repos."""
199 repo = _make_repo(tmp_path / "remote")
200 _add_commit(repo, "c" * 64)
201 t = LocalFileTransport()
202 info = t.fetch_remote_info(f"file://{repo}", signing="should-be-ignored")
203 assert info["repo_id"] == f"repo-{repo.name}"
204
205 def test_corrupted_repo_json_raises_transport_error(self, tmp_path: pathlib.Path) -> None:
206 repo = _make_repo(tmp_path / "bad")
207 (repo / ".muse" / "repo.json").write_text("NOT JSON")
208 t = LocalFileTransport()
209 with pytest.raises(TransportError, match="repo.json"):
210 t.fetch_remote_info(f"file://{repo}", signing=None)
211
212
213 # ---------------------------------------------------------------------------
214 # Unit — fetch_pack
215 # ---------------------------------------------------------------------------
216
217
218 class TestFetchPack:
219 def test_returns_pack_bundle_for_wanted_commit(self, tmp_path: pathlib.Path) -> None:
220 repo = _make_repo(tmp_path / "remote")
221 cid = _add_commit(repo, _sha(b"commit-1"))
222 t = LocalFileTransport()
223 bundle = t.fetch_pack(f"file://{repo}", signing=None, want=[cid], have=[])
224 commits = bundle.get("commits") or []
225 commit_ids = [c["commit_id"] for c in commits]
226 assert cid in commit_ids
227
228 def test_empty_want_returns_empty_bundle(self, tmp_path: pathlib.Path) -> None:
229 repo = _make_repo(tmp_path / "remote")
230 _add_commit(repo, _sha(b"commit-1"))
231 t = LocalFileTransport()
232 bundle = t.fetch_pack(f"file://{repo}", signing=None, want=[], have=[])
233 commits = bundle.get("commits") or []
234 assert commits == []
235
236 def test_have_excludes_already_known_commits(self, tmp_path: pathlib.Path) -> None:
237 repo = _make_repo(tmp_path / "remote")
238 cid1 = _add_commit(repo, _sha(b"commit-1"))
239 cid2 = _add_commit(repo, _sha(b"commit-2"), parent=cid1)
240 t = LocalFileTransport()
241 bundle = t.fetch_pack(f"file://{repo}", signing=None, want=[cid2], have=[cid1])
242 commits = bundle.get("commits") or []
243 commit_ids = {c["commit_id"] for c in commits}
244 # cid2 is wanted; cid1 is in have so it may be excluded.
245 assert cid2 in commit_ids
246
247
248 # ---------------------------------------------------------------------------
249 # Unit — push_pack
250 # ---------------------------------------------------------------------------
251
252
253 class TestPushPack:
254 def _minimal_bundle(
255 self, commit_id: str, branch: str = "main", parent: str | None = None
256 ) -> PackBundle:
257 content = b"test-content"
258 oid = _sha(content)
259 snap_id = _sha(commit_id.encode())
260 return PackBundle(
261 commits=[{
262 "commit_id": commit_id,
263 "repo_id": "test",
264 "branch": branch,
265 "snapshot_id": snap_id,
266 "message": "test",
267 "committed_at": "2026-01-01T00:00:00+00:00",
268 "parent_commit_id": parent,
269 "parent2_commit_id": None,
270 "author": "test",
271 "metadata": {},
272 "structured_delta": None,
273 "sem_ver_bump": "none",
274 "breaking_changes": [],
275 "agent_id": "",
276 "model_id": "",
277 "toolchain_id": "",
278 "prompt_hash": "",
279 "signature": "",
280 "signer_key_id": "",
281 "format_version": 5,
282 "reviewed_by": [],
283 "test_runs": 0,
284 }],
285 snapshots=[{
286 "snapshot_id": snap_id,
287 "manifest": {"file.txt": oid},
288 "created_at": "2026-01-01T00:00:00+00:00",
289 }],
290 objects=[{"object_id": oid, "content_b64": base64.b64encode(content).decode()}],
291 branch_heads={branch: commit_id},
292 )
293
294 def test_successful_push_returns_ok(self, tmp_path: pathlib.Path) -> None:
295 remote = _make_repo(tmp_path / "remote")
296 cid = _sha(b"new-commit")
297 bundle = self._minimal_bundle(cid)
298 t = LocalFileTransport()
299 result = t.push_pack(f"file://{remote}", None, bundle, "main", force=False)
300 assert result["ok"] is True
301 assert get_head_commit_id(remote, "main") == cid
302
303 def test_push_updates_ref_file(self, tmp_path: pathlib.Path) -> None:
304 remote = _make_repo(tmp_path / "remote")
305 cid = _sha(b"tip")
306 bundle = self._minimal_bundle(cid)
307 LocalFileTransport().push_pack(f"file://{remote}", None, bundle, "main", force=False)
308 ref = (remote / ".muse" / "refs" / "heads" / "main").read_text()
309 assert ref == cid
310
311 def test_push_result_includes_all_branch_heads(self, tmp_path: pathlib.Path) -> None:
312 remote = _make_repo(tmp_path / "remote")
313 _add_commit(remote, "e" * 64, branch="dev")
314 cid = _sha(b"tip")
315 bundle = self._minimal_bundle(cid)
316 result = LocalFileTransport().push_pack(f"file://{remote}", None, bundle, "main", force=False)
317 assert "main" in result["branch_heads"]
318 assert "dev" in result["branch_heads"]
319
320 def test_fast_forward_check_rejects_diverged_push(self, tmp_path: pathlib.Path) -> None:
321 remote = _make_repo(tmp_path / "remote")
322 existing = _add_commit(remote, "f" * 64)
323 # Bundle that doesn't include the existing commit in its ancestry.
324 new_cid = _sha(b"diverged")
325 bundle = self._minimal_bundle(new_cid) # no parent → diverged
326 result = LocalFileTransport().push_pack(
327 f"file://{remote}", None, bundle, "main", force=False
328 )
329 assert result["ok"] is False
330 assert "diverged" in result["message"]
331
332 def test_force_flag_overrides_fast_forward_check(self, tmp_path: pathlib.Path) -> None:
333 remote = _make_repo(tmp_path / "remote")
334 _add_commit(remote, "f" * 64)
335 new_cid = _sha(b"force-rewrite")
336 bundle = self._minimal_bundle(new_cid)
337 result = LocalFileTransport().push_pack(
338 f"file://{remote}", None, bundle, "main", force=True
339 )
340 assert result["ok"] is True
341 assert get_head_commit_id(remote, "main") == new_cid
342
343 def test_push_creates_new_branch(self, tmp_path: pathlib.Path) -> None:
344 remote = _make_repo(tmp_path / "remote")
345 cid = _sha(b"feature-tip")
346 bundle = self._minimal_bundle(cid, branch="feature/my-branch")
347 result = LocalFileTransport().push_pack(
348 f"file://{remote}", None, bundle, "feature/my-branch", force=False
349 )
350 assert result["ok"] is True
351 ref_file = remote / ".muse" / "refs" / "heads" / "feature" / "my-branch"
352 assert ref_file.exists()
353 assert ref_file.read_text() == cid
354
355
356 # ---------------------------------------------------------------------------
357 # Security — branch name and path traversal
358 # ---------------------------------------------------------------------------
359
360
361 class TestPushPackSecurity:
362 def _remote(self, tmp_path: pathlib.Path) -> pathlib.Path:
363 return _make_repo(tmp_path / "remote")
364
365 def _bundle(self, branch: str = "main") -> PackBundle:
366 cid = _sha(b"payload")
367 return PackBundle(
368 commits=[],
369 snapshots=[],
370 objects=[],
371 branch_heads={branch: cid},
372 )
373
374 def test_path_traversal_branch_rejected(self, tmp_path: pathlib.Path) -> None:
375 remote = self._remote(tmp_path)
376 bundle = self._bundle("main")
377 bundle["branch_heads"] = {"main": _sha(b"tip")}
378 result = LocalFileTransport().push_pack(
379 f"file://{remote}", None, bundle, "../evil", force=True
380 )
381 assert result["ok"] is False
382
383 def test_double_dot_branch_rejected(self, tmp_path: pathlib.Path) -> None:
384 remote = self._remote(tmp_path)
385 result = LocalFileTransport().push_pack(
386 f"file://{remote}", None, {}, "foo..bar", force=True
387 )
388 assert result["ok"] is False
389
390 def test_null_byte_branch_rejected(self, tmp_path: pathlib.Path) -> None:
391 remote = self._remote(tmp_path)
392 result = LocalFileTransport().push_pack(
393 f"file://{remote}", None, {}, "main\x00evil", force=True
394 )
395 assert result["ok"] is False
396
397 def test_backslash_branch_rejected(self, tmp_path: pathlib.Path) -> None:
398 remote = self._remote(tmp_path)
399 result = LocalFileTransport().push_pack(
400 f"file://{remote}", None, {}, "main\\evil", force=True
401 )
402 assert result["ok"] is False
403
404 def test_cr_lf_branch_rejected(self, tmp_path: pathlib.Path) -> None:
405 remote = self._remote(tmp_path)
406 for bad in ("main\r", "main\ninjected", "main\r\nevil"):
407 result = LocalFileTransport().push_pack(
408 f"file://{remote}", None, {}, bad, force=True
409 )
410 assert result["ok"] is False, f"Expected rejection for branch={bad!r}"
411
412 def test_empty_branch_rejected(self, tmp_path: pathlib.Path) -> None:
413 remote = self._remote(tmp_path)
414 result = LocalFileTransport().push_pack(
415 f"file://{remote}", None, {}, "", force=True
416 )
417 assert result["ok"] is False
418
419 def test_symlink_in_heads_dir_cannot_escape(self, tmp_path: pathlib.Path) -> None:
420 """Pre-placed symlink in .muse/refs/heads/ that points outside cannot be followed."""
421 remote = self._remote(tmp_path)
422 outside = tmp_path / "outside.txt"
423 outside.write_text("sensitive")
424 heads_dir = remote / ".muse" / "refs" / "heads"
425 link = heads_dir / "evil"
426 link.symlink_to(outside)
427 # contain_path resolves the symlink — result is outside heads_dir → rejected.
428 cid = _sha(b"tip")
429 bundle = PackBundle(
430 commits=[],
431 snapshots=[],
432 objects=[],
433 branch_heads={"evil": cid},
434 )
435 result = LocalFileTransport().push_pack(
436 f"file://{remote}", None, bundle, "evil", force=True
437 )
438 # Symlink escapes the base → contain_path raises → push_pack returns ok=False.
439 # If the symlink doesn't escape (OS resolved it back inside), the push may succeed;
440 # either way, the outside file must not be overwritten with the commit ID.
441 if not result["ok"]:
442 assert "unsafe" in result["message"]
443 # The outside file must be untouched regardless of result.
444 assert outside.read_text() == "sensitive"
445
446
447 # ---------------------------------------------------------------------------
448 # make_transport factory
449 # ---------------------------------------------------------------------------
450
451
452 class TestMakeTransport:
453 def test_file_url_returns_local_transport(self) -> None:
454 assert isinstance(make_transport("file:///some/path"), LocalFileTransport)
455
456 def test_https_url_returns_http_transport(self) -> None:
457 assert isinstance(make_transport("https://hub.example.com/repos/r1"), HttpTransport)
458
459 def test_http_url_returns_http_transport(self) -> None:
460 assert isinstance(make_transport("http://hub.example.com/repos/r1"), HttpTransport)
461
462 def test_empty_url_returns_http_transport(self) -> None:
463 assert isinstance(make_transport(""), HttpTransport)
464
465
466 # ---------------------------------------------------------------------------
467 # Integration — full round-trip between two real repos
468 # ---------------------------------------------------------------------------
469
470
471 class TestIntegrationRoundTrip:
472 def test_push_then_fetch_info(self, tmp_path: pathlib.Path) -> None:
473 """Push from local → remote; remote branch heads should reflect the push."""
474 local = _make_repo(tmp_path / "local")
475 remote = _make_repo(tmp_path / "remote")
476 cid = _add_commit(local, _sha(b"initial"), branch="main")
477
478 from muse.core.pack import build_pack
479 bundle = build_pack(local, commit_ids=[cid], have=[])
480
481 t = LocalFileTransport()
482 result = t.push_pack(f"file://{remote}", None, bundle, "main", force=False)
483 assert result["ok"] is True
484
485 info = t.fetch_remote_info(f"file://{remote}", None)
486 assert info["branch_heads"]["main"] == cid
487
488 def test_fetch_pack_after_push(self, tmp_path: pathlib.Path) -> None:
489 """After pushing A→B, fetching from B should give back the same commit."""
490 src = _make_repo(tmp_path / "src")
491 dst = _make_repo(tmp_path / "dst")
492 cid = _add_commit(src, _sha(b"content"), branch="main")
493
494 from muse.core.pack import build_pack
495 bundle = build_pack(src, commit_ids=[cid], have=[])
496 LocalFileTransport().push_pack(f"file://{dst}", None, bundle, "main", force=False)
497
498 fetched_bundle = LocalFileTransport().fetch_pack(
499 f"file://{dst}", None, want=[cid], have=[]
500 )
501 fetched_ids = {c["commit_id"] for c in (fetched_bundle.get("commits") or [])}
502 assert cid in fetched_ids
503
504 def test_multi_branch_round_trip(self, tmp_path: pathlib.Path) -> None:
505 """Push two branches; remote should have both."""
506 local = _make_repo(tmp_path / "local")
507 remote = _make_repo(tmp_path / "remote")
508
509 cid_main = _add_commit(local, _sha(b"main-commit"), branch="main")
510 cid_dev = _add_commit(local, _sha(b"dev-commit"), branch="dev")
511
512 from muse.core.pack import build_pack
513 t = LocalFileTransport()
514 url = f"file://{remote}"
515
516 bundle_main = build_pack(local, commit_ids=[cid_main], have=[])
517 t.push_pack(url, None, bundle_main, "main", force=False)
518
519 bundle_dev = build_pack(local, commit_ids=[cid_dev], have=[])
520 t.push_pack(url, None, bundle_dev, "dev", force=False)
521
522 info = t.fetch_remote_info(url, None)
523 assert info["branch_heads"]["main"] == cid_main
524 assert info["branch_heads"]["dev"] == cid_dev
525
526 def test_incremental_push_is_fast_forward(self, tmp_path: pathlib.Path) -> None:
527 """Second push whose parent is the remote tip is accepted (fast-forward)."""
528 local = _make_repo(tmp_path / "local")
529 remote = _make_repo(tmp_path / "remote")
530
531 cid1 = _add_commit(local, "commit-1", branch="main")
532
533 from muse.core.pack import build_pack
534 t = LocalFileTransport()
535 url = f"file://{remote}"
536
537 b1 = build_pack(local, commit_ids=[cid1], have=[])
538 t.push_pack(url, None, b1, "main", force=False)
539
540 # Second commit with cid1 as parent.
541 cid2 = _add_commit(local, "commit-2", branch="main", parent=cid1)
542 b2 = build_pack(local, commit_ids=[cid2], have=[cid1])
543 result = t.push_pack(url, None, b2, "main", force=False)
544
545 assert result["ok"] is True
546 assert get_head_commit_id(remote, "main") == cid2
547
548
549 # ---------------------------------------------------------------------------
550 # Stress — large bundle
551 # ---------------------------------------------------------------------------
552
553
554 class TestStress:
555 def test_push_large_bundle(self, tmp_path: pathlib.Path) -> None:
556 """Push a bundle with 50 commits and 200 distinct objects."""
557 remote = _make_repo(tmp_path / "remote")
558 local = _make_repo(tmp_path / "local")
559
560 prev_cid: str | None = None
561 last_cid = ""
562 committed_at = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
563 for i in range(50):
564 # Write 4 objects per commit (200 total).
565 manifest: Manifest = {}
566 for j in range(4):
567 blob = f"blob-{i}-{j}".encode()
568 oid = _sha(blob)
569 write_object(local, oid, blob)
570 manifest[f"file_{i}_{j}.txt"] = oid
571 snap_id = compute_snapshot_id(manifest)
572 snap = SnapshotRecord(snapshot_id=snap_id, manifest=manifest)
573 write_snapshot(local, snap)
574 message = f"commit {i}"
575 parent_ids = [prev_cid] if prev_cid else []
576 cid = compute_commit_id(parent_ids, snap_id, message, committed_at.isoformat())
577 commit = CommitRecord(
578 commit_id=cid,
579 repo_id=f"repo-{local.name}",
580 branch="main",
581 snapshot_id=snap_id,
582 message=message,
583 committed_at=committed_at,
584 parent_commit_id=prev_cid,
585 )
586 write_commit(local, commit)
587 prev_cid = cid
588 last_cid = cid
589
590 (local / ".muse" / "refs" / "heads" / "main").write_text(last_cid)
591
592 from muse.core.pack import build_pack
593 bundle = build_pack(local, commit_ids=[last_cid], have=[])
594 result = LocalFileTransport().push_pack(
595 f"file://{remote}", None, bundle, "main", force=False
596 )
597 assert result["ok"] is True
598 assert get_head_commit_id(remote, "main") == last_cid
599
600 def test_fetch_pack_large_bundle(self, tmp_path: pathlib.Path) -> None:
601 """Fetch from a remote with 20 commits; verify all are returned."""
602 remote = _make_repo(tmp_path / "remote")
603 all_cids: list[str] = []
604 prev: str | None = None
605
606 for i in range(20):
607 cid = _add_commit(remote, f"remote-commit-{i}", parent=prev)
608 all_cids.append(cid)
609 prev = cid
610
611 last = all_cids[-1]
612 bundle = LocalFileTransport().fetch_pack(
613 f"file://{remote}", None, want=[last], have=[]
614 )
615 fetched_ids = {c["commit_id"] for c in (bundle.get("commits") or [])}
616 assert last in fetched_ids
File History 3 commits
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 27 days ago
sha256:d8316ffae901be06347e16ab55be11868eb519dd16ade3e8aa16a99e662f7e62 baseline: rc14 re-baseline after rc3 store corruption recovery Human patch 27 days ago
sha256:1c4b3e3a9a1f300774c3ee662b572a698d5fd405bf765a71e6011a2e9c3eaaaa feat: Muse — version control for the agent era Human 101 days ago