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