test_local_file_transport.py
python
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