gabriel / muse public
test_wire_localhost.py python
735 lines 27.4 KB
Raw
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 141 days ago
1 """Muse Wire Protocol — end-to-end localhost integration tests.
2
3 Requires ``http://localhost:10003`` to be running with gabriel's identity
4 registered. All tests are auto-skipped when the hub is not reachable.
5
6 Coverage
7 --------
8 T1 Hub health + auth — whoami round-trip
9 T2 Repo lifecycle — create, list, delete hub repo
10 T3 Push (cold) — initial push of local commits to a fresh hub repo
11 T4 Clone — clone a pushed repo, verify snapshot equality
12 T5 Incremental push — push new commits, only delta transferred
13 T6 Pull — push from location A, pull from B, verify merge result
14 T7 Fetch — fetch from remote, objects arrive, local HEAD unchanged
15 T8 Force push — divergent history accepted with --force
16 T9 Cross-repo — multi-file repo (contracts-style) full push/clone cycle
17 T10 Idempotent re-push — re-push same commits, 0 new objects stored
18 """
19 from __future__ import annotations
20
21 import datetime
22 import hashlib
23 import json
24 import pathlib
25 import time
26 import urllib.error
27 import urllib.request
28 import uuid
29
30 import pytest
31
32 from muse._version import __version__
33 from muse.cli.config import get_signing_identity
34 from muse.core.msign import build_msign_header
35 from muse.core.object_store import write_object
36 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
37 from muse.core.store import (
38 CommitRecord,
39 SnapshotRecord,
40 get_head_commit_id,
41 read_commit,
42 read_snapshot,
43 write_commit,
44 write_snapshot,
45 )
46 from tests.cli_test_helper import CliRunner
47 from muse.core._types import long_id
48
49 # ---------------------------------------------------------------------------
50 # Constants
51 # ---------------------------------------------------------------------------
52
53 HUB = "http://localhost:10003"
54 OWNER = "gabriel"
55
56 runner = CliRunner()
57
58
59 # ---------------------------------------------------------------------------
60 # Hub availability guard — skip entire module if hub not reachable
61 # ---------------------------------------------------------------------------
62
63 def _hub_reachable() -> bool:
64 try:
65 urllib.request.urlopen(f"{HUB}/healthz", timeout=2)
66 return True
67 except (urllib.error.URLError, OSError):
68 return False
69
70
71 pytestmark = pytest.mark.skipif(
72 not _hub_reachable(),
73 reason="localhost hub not reachable — start musehub to run wire tests",
74 )
75
76
77 # ---------------------------------------------------------------------------
78 # Auth helper
79 # ---------------------------------------------------------------------------
80
81 def _signing():
82 """Return gabriel's signing identity for localhost."""
83 signing = get_signing_identity(remote_url=HUB)
84 if signing is None:
85 pytest.skip("No signing identity for localhost hub")
86 return signing
87
88
89 def _hub_request(method: str, path: str, body: dict | None = None) -> dict:
90 """Make a signed API request to the local hub. Returns parsed JSON."""
91 signing = _signing()
92 url = f"{HUB}{path}"
93 data: bytes | None = None
94 if body is not None:
95 data = json.dumps(body).encode()
96 auth = build_msign_header(signing, method, url, data)
97 headers: dict[str, str] = {
98 "Authorization": auth,
99 "Accept": "application/json",
100 }
101 if data is not None:
102 headers["Content-Type"] = "application/json"
103 req = urllib.request.Request(url, data=data, headers=headers, method=method)
104 try:
105 with urllib.request.urlopen(req, timeout=15) as resp:
106 return json.loads(resp.read())
107 except urllib.error.HTTPError as exc:
108 raw = exc.read().decode(errors="replace")
109 pytest.fail(f"Hub request failed: {method} {path} → {exc.code}: {raw}")
110
111
112 # ---------------------------------------------------------------------------
113 # Hub repo lifecycle fixture
114 # ---------------------------------------------------------------------------
115
116 @pytest.fixture
117 def hub_repo() -> dict:
118 """Create a private test hub repo; delete it after the test."""
119 slug = f"test-wire-{uuid.uuid4().hex[:8]}"
120 resp = _hub_request("POST", "/api/repos", {
121 "name": slug,
122 "owner": OWNER,
123 "visibility": "private",
124 "domain": "code",
125 })
126 repo_id: str = resp["repoId"]
127 yield {"repo_id": repo_id, "slug": slug, "url": f"{HUB}/{OWNER}/{slug}"}
128 # Cleanup — tolerate 404 if test already deleted it
129 try:
130 _hub_request("DELETE", f"/api/repos/{repo_id}")
131 except Exception:
132 pass
133
134
135 # ---------------------------------------------------------------------------
136 # Local repo builder
137 # ---------------------------------------------------------------------------
138
139 def _init_local_repo(
140 root: pathlib.Path,
141 hub_slug: str,
142 *,
143 branch: str = "main",
144 n_commits: int = 1,
145 file_tree: dict[str, bytes] | None = None,
146 ) -> list[str]:
147 """Initialise a .muse/ repo with N commits; return commit IDs oldest-first.
148
149 ``file_tree`` maps path → content for the first commit. Subsequent
150 commits each add/update a single generated file.
151 """
152 muse_dir = root / ".muse"
153 for sub in ("refs/heads", "objects", "commits", "snapshots"):
154 (muse_dir / sub).mkdir(parents=True, exist_ok=True)
155
156 (muse_dir / "repo.json").write_text(json.dumps({
157 "repo_id": f"test-{hub_slug}",
158 "schema_version": __version__,
159 "domain": "code",
160 }))
161 (muse_dir / "HEAD").write_text(f"ref: refs/heads/{branch}\n")
162 (muse_dir / "config.toml").write_text(
163 f'[remotes.local]\nurl = "{HUB}/{OWNER}/{hub_slug}"\n'
164 )
165
166 if file_tree is None:
167 file_tree = {f"file_{uuid.uuid4().hex[:6]}.txt": b"initial content"}
168
169 commit_ids: list[str] = []
170 manifest: dict[str, str] = {}
171
172 # First commit — full file tree
173 for path, content in file_tree.items():
174 oid = long_id(hashlib.sha256(content).hexdigest())
175 write_object(root, oid, content)
176 manifest[path] = oid
177
178 snap_id = compute_snapshot_id(manifest)
179 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=dict(manifest)))
180 now = datetime.datetime.now(tz=datetime.timezone.utc)
181 cid = compute_commit_id([], snap_id, "initial commit", now.isoformat())
182 write_commit(root, CommitRecord(
183 commit_id=cid,
184 repo_id=f"test-{hub_slug}",
185 branch=branch,
186 snapshot_id=snap_id,
187 message="initial commit",
188 committed_at=now,
189 author=OWNER,
190 ))
191 commit_ids.append(cid)
192 parent_ids = [cid]
193
194 # Additional commits
195 for i in range(1, n_commits):
196 extra = f"extra_{i}_{uuid.uuid4().hex[:4]}.txt"
197 content = f"commit {i} content".encode()
198 oid = long_id(hashlib.sha256(content).hexdigest())
199 write_object(root, oid, content)
200 manifest = dict(manifest)
201 manifest[extra] = oid
202 snap_id = compute_snapshot_id(manifest)
203 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=dict(manifest)))
204 now = datetime.datetime.now(tz=datetime.timezone.utc)
205 cid = compute_commit_id(parent_ids, snap_id, f"commit {i}", now.isoformat())
206 write_commit(root, CommitRecord(
207 commit_id=cid,
208 repo_id=f"test-{hub_slug}",
209 branch=branch,
210 snapshot_id=snap_id,
211 message=f"commit {i}",
212 committed_at=now,
213 author=OWNER,
214 parent_commit_id=parent_ids[0] if len(parent_ids) == 1 else None,
215 parent2_commit_id=parent_ids[1] if len(parent_ids) > 1 else None,
216 ))
217 commit_ids.append(cid)
218 parent_ids = [cid]
219
220 (muse_dir / "refs" / "heads" / branch).write_text(commit_ids[-1])
221 return commit_ids
222
223
224 def _add_commit(root: pathlib.Path, hub_slug: str, branch: str = "main") -> str:
225 """Append one more commit to an existing local repo. Returns new commit ID."""
226 from muse.core.store import read_snapshot as _read_snap
227 parent_cid = get_head_commit_id(root, branch)
228 parent_rec = read_commit(root, parent_cid)
229 parent_snap = _read_snap(root, parent_rec.snapshot_id)
230 manifest = dict(parent_snap.manifest) if parent_snap else {}
231
232 extra = f"extra_{uuid.uuid4().hex[:6]}.txt"
233 content = f"added: {extra}".encode()
234 oid = long_id(hashlib.sha256(content).hexdigest())
235 write_object(root, oid, content)
236 manifest[extra] = oid
237
238 snap_id = compute_snapshot_id(manifest)
239 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=dict(manifest)))
240 now = datetime.datetime.now(tz=datetime.timezone.utc)
241 cid = compute_commit_id([parent_cid], snap_id, f"new commit {extra}", now.isoformat())
242 write_commit(root, CommitRecord(
243 commit_id=cid,
244 repo_id=f"test-{hub_slug}",
245 branch=branch,
246 snapshot_id=snap_id,
247 message=f"new commit {extra}",
248 committed_at=now,
249 author=OWNER,
250 parent_commit_id=parent_cid,
251 ))
252 (root / ".muse" / "refs" / "heads" / branch).write_text(cid)
253 return cid
254
255
256 # ---------------------------------------------------------------------------
257 # T1 — Hub health + auth
258 # ---------------------------------------------------------------------------
259
260 class TestT1Auth:
261 """Tier 1: hub is up, gabriel's identity round-trips."""
262
263 def test_healthz(self) -> None:
264 resp = urllib.request.urlopen(f"{HUB}/healthz", timeout=5)
265 data = json.loads(resp.read())
266 assert data["status"] == "ok"
267 assert data["db"] is True
268
269 def test_whoami(self) -> None:
270 """Signed GET to /api/identities/me returns gabriel's handle."""
271 data = _hub_request("GET", "/api/identities/me")
272 assert data.get("handle") == OWNER or data.get("owner") == OWNER or OWNER in str(data)
273
274
275 # ---------------------------------------------------------------------------
276 # T2 — Repo lifecycle
277 # ---------------------------------------------------------------------------
278
279 class TestT2RepoLifecycle:
280 """Tier 2: create, verify presence, delete a hub repo."""
281
282 def test_create_and_list(self, hub_repo: dict) -> None:
283 slug = hub_repo["slug"]
284 # Repo should appear in refs endpoint
285 refs_url = f"{HUB}/{OWNER}/{slug}/refs"
286 resp = urllib.request.urlopen(refs_url, timeout=5)
287 data = json.loads(resp.read())
288 assert "branch_heads" in data or "branches" in data or data is not None
289
290 def test_repo_id_is_sha256(self, hub_repo: dict) -> None:
291 repo_id = hub_repo["repo_id"]
292 assert repo_id.startswith("sha256:"), (
293 f"repo_id should be sha256-addressed, got: {repo_id!r}"
294 )
295
296 def test_delete_returns_no_content(self, hub_repo: dict) -> None:
297 """Explicit delete — fixture cleanup would also cover this, but verify 204."""
298 signing = _signing()
299 url = f"{HUB}/api/repos/{hub_repo['repo_id']}"
300 auth = build_msign_header(signing, "DELETE", url, None)
301 req = urllib.request.Request(
302 url, headers={"Authorization": auth, "Accept": "application/json"}, method="DELETE"
303 )
304 try:
305 with urllib.request.urlopen(req, timeout=10) as resp:
306 assert resp.status == 204
307 except urllib.error.HTTPError as exc:
308 if exc.code == 204:
309 pass # urllib raises on 204, treat as success
310 else:
311 pytest.fail(f"Delete failed: {exc.code} {exc.read()}")
312 # Fixture cleanup will get a 404 — that's fine, it tolerates it
313
314
315 # ---------------------------------------------------------------------------
316 # T3 — Push (cold)
317 # ---------------------------------------------------------------------------
318
319 class TestT3ColdPush:
320 """Tier 3: push a local repo to a fresh hub repo."""
321
322 def test_initial_push_succeeds(
323 self, tmp_path: pathlib.Path, hub_repo: dict, monkeypatch: pytest.MonkeyPatch
324 ) -> None:
325 root = tmp_path / "local"
326 root.mkdir()
327 commit_ids = _init_local_repo(root, hub_repo["slug"], n_commits=3)
328 monkeypatch.chdir(root)
329
330 result = runner.invoke(None, ["push", "local", "main"])
331 assert result.exit_code == 0, f"push failed:\n{result.output}\n{result.stderr}"
332 assert "Pushed" in result.output or "✅" in result.output
333
334 def test_push_reports_commit_count(
335 self, tmp_path: pathlib.Path, hub_repo: dict, monkeypatch: pytest.MonkeyPatch
336 ) -> None:
337 root = tmp_path / "local"
338 root.mkdir()
339 _init_local_repo(root, hub_repo["slug"], n_commits=5)
340 monkeypatch.chdir(root)
341
342 result = runner.invoke(None, ["push", "local", "main"])
343 assert result.exit_code == 0
344 # Output should mention commit count
345 output = result.output + (result.stderr or "")
346 assert any(c.isdigit() for c in output), "Expected numeric output (commit/object counts)"
347
348 def test_push_empty_repo_succeeds(
349 self, tmp_path: pathlib.Path, hub_repo: dict, monkeypatch: pytest.MonkeyPatch
350 ) -> None:
351 """Even a single-commit repo pushes cleanly."""
352 root = tmp_path / "local"
353 root.mkdir()
354 _init_local_repo(root, hub_repo["slug"], n_commits=1)
355 monkeypatch.chdir(root)
356
357 result = runner.invoke(None, ["push", "local", "main"])
358 assert result.exit_code == 0
359
360
361 # ---------------------------------------------------------------------------
362 # T4 — Clone
363 # ---------------------------------------------------------------------------
364
365 class TestT4Clone:
366 """Tier 4: clone a pushed repo and verify snapshot equality."""
367
368 def test_clone_restores_snapshot(
369 self, tmp_path: pathlib.Path, hub_repo: dict, monkeypatch: pytest.MonkeyPatch
370 ) -> None:
371 # Push from location A
372 src = tmp_path / "source"
373 src.mkdir()
374 commit_ids = _init_local_repo(
375 src, hub_repo["slug"], n_commits=2,
376 file_tree={
377 "README.md": b"# Test repo",
378 "src/main.py": b"print('hello')",
379 }
380 )
381 monkeypatch.chdir(src)
382 push_result = runner.invoke(None, ["push", "local", "main"])
383 assert push_result.exit_code == 0, push_result.output
384
385 # Clone to location B
386 dst = tmp_path / "clone"
387 result = runner.invoke(None, ["clone", hub_repo["url"], str(dst)])
388 assert result.exit_code == 0, f"clone failed:\n{result.output}\n{result.stderr}"
389 assert dst.exists(), "Clone directory not created"
390
391 # Verify HEAD commit matches
392 cloned_head = get_head_commit_id(dst, "main")
393 assert cloned_head == commit_ids[-1], (
394 f"Cloned HEAD {cloned_head} != expected {commit_ids[-1]}"
395 )
396
397 def test_clone_restores_file_objects(
398 self, tmp_path: pathlib.Path, hub_repo: dict, monkeypatch: pytest.MonkeyPatch
399 ) -> None:
400 file_content = b"unique content for object verification " + uuid.uuid4().bytes
401 oid = long_id(hashlib.sha256(file_content).hexdigest())
402
403 src = tmp_path / "source"
404 src.mkdir()
405 _init_local_repo(src, hub_repo["slug"], file_tree={"data.bin": file_content})
406 monkeypatch.chdir(src)
407 runner.invoke(None, ["push", "local", "main"])
408
409 dst = tmp_path / "clone"
410 result = runner.invoke(None, ["clone", hub_repo["url"], str(dst)])
411 assert result.exit_code == 0
412
413 # Verify object content was transferred
414 from muse.core.object_store import read_object
415 cloned_obj = read_object(dst, oid)
416 assert cloned_obj == file_content
417
418
419 # ---------------------------------------------------------------------------
420 # T5 — Incremental push
421 # ---------------------------------------------------------------------------
422
423 class TestT5IncrementalPush:
424 """Tier 5: second push transfers only new objects."""
425
426 def test_incremental_push_succeeds(
427 self, tmp_path: pathlib.Path, hub_repo: dict, monkeypatch: pytest.MonkeyPatch
428 ) -> None:
429 root = tmp_path / "local"
430 root.mkdir()
431 _init_local_repo(root, hub_repo["slug"], n_commits=2)
432 monkeypatch.chdir(root)
433
434 # First push
435 r1 = runner.invoke(None, ["push", "local", "main"])
436 assert r1.exit_code == 0, r1.output
437
438 # Add a commit
439 new_cid = _add_commit(root, hub_repo["slug"])
440
441 # Second push — should succeed with fewer objects
442 r2 = runner.invoke(None, ["push", "local", "main"])
443 assert r2.exit_code == 0, r2.output
444 output = r2.output + (r2.stderr or "")
445 assert "Pushed" in output or "✅" in output
446
447 def test_incremental_push_head_advances(
448 self, tmp_path: pathlib.Path, hub_repo: dict, monkeypatch: pytest.MonkeyPatch
449 ) -> None:
450 root = tmp_path / "local"
451 root.mkdir()
452 _init_local_repo(root, hub_repo["slug"], n_commits=1)
453 monkeypatch.chdir(root)
454 runner.invoke(None, ["push", "local", "main"])
455
456 new_cid = _add_commit(root, hub_repo["slug"])
457 r2 = runner.invoke(None, ["push", "local", "main"])
458 assert r2.exit_code == 0
459
460 # Verify hub refs report the new head
461 refs_data = json.loads(
462 urllib.request.urlopen(f"{hub_repo['url']}/refs", timeout=5).read()
463 )
464 branch_heads = refs_data.get("branch_heads", refs_data.get("branches", {}))
465 assert "main" in branch_heads
466 # Head should now be the new commit (or its sha256: prefixed form)
467 hub_head = branch_heads["main"]
468 assert new_cid.lstrip("sha256:") in hub_head or hub_head in new_cid
469
470
471 # ---------------------------------------------------------------------------
472 # T6 — Pull
473 # ---------------------------------------------------------------------------
474
475 class TestT6Pull:
476 """Tier 6: push from A, pull from B, verify merge result."""
477
478 def test_pull_updates_local_head(
479 self, tmp_path: pathlib.Path, hub_repo: dict, monkeypatch: pytest.MonkeyPatch
480 ) -> None:
481 # Push initial commits from location A
482 src = tmp_path / "A"
483 src.mkdir()
484 commit_ids = _init_local_repo(src, hub_repo["slug"], n_commits=2)
485 monkeypatch.chdir(src)
486 runner.invoke(None, ["push", "local", "main"])
487
488 # Clone to location B
489 dst = tmp_path / "B"
490 r_clone = runner.invoke(None, ["clone", hub_repo["url"], str(dst)])
491 assert r_clone.exit_code == 0, r_clone.output
492
493 # Push a new commit from A
494 monkeypatch.chdir(src)
495 new_cid = _add_commit(src, hub_repo["slug"])
496 runner.invoke(None, ["push", "local", "main"])
497
498 # Pull from B
499 monkeypatch.chdir(dst)
500 r_pull = runner.invoke(None, ["pull", "local", "main"])
501 assert r_pull.exit_code == 0, f"pull failed:\n{r_pull.output}\n{r_pull.stderr}"
502
503 # B's HEAD should now match A's HEAD
504 b_head = get_head_commit_id(dst, "main")
505 assert b_head == new_cid, f"Pull did not advance B's HEAD: {b_head} != {new_cid}"
506
507
508 # ---------------------------------------------------------------------------
509 # T7 — Fetch
510 # ---------------------------------------------------------------------------
511
512 class TestT7Fetch:
513 """Tier 7: fetch downloads objects but does not move local HEAD."""
514
515 def test_fetch_does_not_move_head(
516 self, tmp_path: pathlib.Path, hub_repo: dict, monkeypatch: pytest.MonkeyPatch
517 ) -> None:
518 # Push initial from A
519 src = tmp_path / "A"
520 src.mkdir()
521 commit_ids = _init_local_repo(src, hub_repo["slug"], n_commits=1)
522 monkeypatch.chdir(src)
523 runner.invoke(None, ["push", "local", "main"])
524
525 # Clone to B — B now has commit_ids[-1] as HEAD
526 dst = tmp_path / "B"
527 r_clone = runner.invoke(None, ["clone", hub_repo["url"], str(dst)])
528 assert r_clone.exit_code == 0
529
530 b_head_before = get_head_commit_id(dst, "main")
531
532 # Push new commit from A
533 monkeypatch.chdir(src)
534 _add_commit(src, hub_repo["slug"])
535 runner.invoke(None, ["push", "local", "main"])
536
537 # Fetch from B — should NOT move main HEAD
538 monkeypatch.chdir(dst)
539 r_fetch = runner.invoke(None, ["fetch", "local"])
540 assert r_fetch.exit_code == 0, f"fetch failed:\n{r_fetch.output}\n{r_fetch.stderr}"
541
542 b_head_after = get_head_commit_id(dst, "main")
543 assert b_head_after == b_head_before, (
544 f"fetch must not advance HEAD: was {b_head_before}, now {b_head_after}"
545 )
546
547
548 # ---------------------------------------------------------------------------
549 # T8 — Force push
550 # ---------------------------------------------------------------------------
551
552 class TestT8ForcePush:
553 """Tier 8: divergent history rejected by default, accepted with --force."""
554
555 def test_non_fast_forward_rejected(
556 self, tmp_path: pathlib.Path, hub_repo: dict, monkeypatch: pytest.MonkeyPatch
557 ) -> None:
558 # Push 2 commits
559 src = tmp_path / "local"
560 src.mkdir()
561 commit_ids = _init_local_repo(src, hub_repo["slug"], n_commits=2)
562 monkeypatch.chdir(src)
563 runner.invoke(None, ["push", "local", "main"])
564
565 # Rewind HEAD to first commit (create divergent history)
566 head_ref = src / ".muse" / "refs" / "heads" / "main"
567 head_ref.write_text(commit_ids[0])
568
569 # Try normal push — should fail (not fast-forward)
570 r = runner.invoke(None, ["push", "local", "main"])
571 # Either exit code non-zero OR output contains rejection message
572 assert r.exit_code != 0 or "not fast-forward" in (r.output + (r.stderr or "")).lower()
573
574 def test_force_push_accepted(
575 self, tmp_path: pathlib.Path, hub_repo: dict, monkeypatch: pytest.MonkeyPatch
576 ) -> None:
577 # Push 2 commits
578 src = tmp_path / "local"
579 src.mkdir()
580 commit_ids = _init_local_repo(src, hub_repo["slug"], n_commits=2)
581 monkeypatch.chdir(src)
582 runner.invoke(None, ["push", "local", "main"])
583
584 # Rewind to first commit and add a divergent commit
585 head_ref = src / ".muse" / "refs" / "heads" / "main"
586 head_ref.write_text(commit_ids[0])
587 _add_commit(src, hub_repo["slug"])
588
589 # Force push
590 r = runner.invoke(None, ["push", "local", "main", "--force"])
591 assert r.exit_code == 0, f"force push failed:\n{r.output}\n{r.stderr}"
592
593
594 # ---------------------------------------------------------------------------
595 # T9 — Cross-repo (contracts-style multi-file repo)
596 # ---------------------------------------------------------------------------
597
598 class TestT9CrossRepo:
599 """Tier 9: rich multi-file repo — push, clone, pull full cycle."""
600
601 def test_multi_file_repo_round_trip(
602 self, tmp_path: pathlib.Path, hub_repo: dict, monkeypatch: pytest.MonkeyPatch
603 ) -> None:
604 # Build a contracts-style repo with many files
605 file_tree = {
606 "README.md": b"# contracts\nShared type contracts.",
607 "muse_contracts/__init__.py": b"",
608 "muse_contracts/wire.py": b"from dataclasses import dataclass\n",
609 "muse_contracts/issue.py": b"from typing import TypedDict\n",
610 "docs/reference/type-contracts.md": b"# Type Contracts\n",
611 "scripts/gen_type_contracts.py": b"#!/usr/bin/env python3\n",
612 "pyproject.toml": b"[tool.poetry]\nname = 'muse-contracts'\n",
613 }
614 src = tmp_path / "contracts"
615 src.mkdir()
616 commit_ids = _init_local_repo(
617 src, hub_repo["slug"], n_commits=1, file_tree=file_tree
618 )
619 monkeypatch.chdir(src)
620
621 r_push = runner.invoke(None, ["push", "local", "main"])
622 assert r_push.exit_code == 0, r_push.output
623
624 dst = tmp_path / "contracts_clone"
625 r_clone = runner.invoke(None, ["clone", hub_repo["url"], str(dst)])
626 assert r_clone.exit_code == 0, r_clone.output
627
628 # Verify all file objects transferred
629 from muse.core.object_store import read_object
630 cloned_head = get_head_commit_id(dst, "main")
631 cloned_commit = read_commit(dst, cloned_head)
632 cloned_snap = read_snapshot(dst, cloned_commit.snapshot_id)
633 assert cloned_snap is not None
634
635 for path, content in file_tree.items():
636 expected_oid = long_id(hashlib.sha256(content).hexdigest())
637 assert path in cloned_snap.manifest, f"Missing file in cloned snapshot: {path}"
638 assert cloned_snap.manifest[path] == expected_oid
639 cloned_bytes = read_object(dst, expected_oid)
640 assert cloned_bytes == content, f"Content mismatch for {path}"
641
642 def test_multi_commit_pull_from_cross_repo(
643 self, tmp_path: pathlib.Path, hub_repo: dict, monkeypatch: pytest.MonkeyPatch
644 ) -> None:
645 """Push 3 commits, clone, push 2 more, pull — verify all 5 commits."""
646 src = tmp_path / "src"
647 src.mkdir()
648 commit_ids = _init_local_repo(src, hub_repo["slug"], n_commits=3)
649 monkeypatch.chdir(src)
650 runner.invoke(None, ["push", "local", "main"])
651
652 dst = tmp_path / "dst"
653 runner.invoke(None, ["clone", hub_repo["url"], str(dst)])
654
655 monkeypatch.chdir(src)
656 new1 = _add_commit(src, hub_repo["slug"])
657 new2 = _add_commit(src, hub_repo["slug"])
658 runner.invoke(None, ["push", "local", "main"])
659
660 monkeypatch.chdir(dst)
661 r = runner.invoke(None, ["pull", "local", "main"])
662 assert r.exit_code == 0, r.output
663
664 # Walk commit chain — should have 5 commits total
665 head = get_head_commit_id(dst, "main")
666 assert head == new2
667
668 seen = []
669 cid = head
670 while cid:
671 rec = read_commit(dst, cid)
672 seen.append(cid)
673 cid = rec.parent_commit_id if rec else None
674 assert len(seen) == 5, f"Expected 5 commits in chain, got {len(seen)}"
675
676
677 # ---------------------------------------------------------------------------
678 # T10 — Idempotent re-push
679 # ---------------------------------------------------------------------------
680
681 class TestT10IdempotentPush:
682 """Tier 10: re-pushing the same commits transfers 0 new objects."""
683
684 def test_idempotent_push_zero_new_objects(
685 self, tmp_path: pathlib.Path, hub_repo: dict, monkeypatch: pytest.MonkeyPatch
686 ) -> None:
687 root = tmp_path / "local"
688 root.mkdir()
689 _init_local_repo(root, hub_repo["slug"], n_commits=3)
690 monkeypatch.chdir(root)
691
692 # First push
693 r1 = runner.invoke(None, ["push", "local", "main"])
694 assert r1.exit_code == 0, r1.output
695
696 # Second push — identical history, nothing new
697 r2 = runner.invoke(None, ["push", "local", "main"])
698 assert r2.exit_code == 0, r2.output
699
700 output = r2.output + (r2.stderr or "")
701 # Remote should report all objects already present
702 assert "already present" in output or "0 commit" in output or "✅" in output
703
704 def test_nuke_and_repush(
705 self, tmp_path: pathlib.Path, hub_repo: dict, monkeypatch: pytest.MonkeyPatch
706 ) -> None:
707 """Delete the hub repo, recreate it, re-push — full idempotency check."""
708 root = tmp_path / "local"
709 root.mkdir()
710 commit_ids = _init_local_repo(root, hub_repo["slug"], n_commits=4)
711 monkeypatch.chdir(root)
712
713 # First push
714 r1 = runner.invoke(None, ["push", "local", "main"])
715 assert r1.exit_code == 0
716
717 # Nuke the hub repo
718 _hub_request("DELETE", f"/api/repos/{hub_repo['repo_id']}")
719
720 # Recreate with same slug
721 resp = _hub_request("POST", "/api/repos", {
722 "name": hub_repo["slug"],
723 "owner": OWNER,
724 "visibility": "private",
725 "domain": "code",
726 })
727 hub_repo["repo_id"] = resp["repoId"] # update for fixture cleanup
728
729 # Re-push same local commits
730 r2 = runner.invoke(None, ["push", "local", "main"])
731 assert r2.exit_code == 0, f"re-push after nuke failed:\n{r2.output}\n{r2.stderr}"
732
733 # Verify all commits landed correctly
734 head = get_head_commit_id(root, "main")
735 assert head == commit_ids[-1]
File History 2 commits
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 141 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 144 days ago