gabriel / musehub public
test_clone_xs_unit.py python
287 lines 10.6 KB
Raw
sha256:77fc45e703f90c0d603ecb1a0ce21ff21095728ca7dd0e146eb5e966c8f9fcc9 more passing tests from full test suite fun Human patch 7 days ago
1 """Clone XS unit tests — issue #65.
2
3 One verb. One size. Proven correct at each layer before moving to the next.
4
5 C1 muse clone exits 0 — real muse CLI clones XS repo from localhost:1337
6 C2 file content matches push — sha256(file_bytes) == object_id for every file
7 C3 no integrity errors — stdout/stderr clean even when exit 0
8 C4 commit graph is correct — log matches what was pushed
9 C5 second clone is identical — two clones produce byte-for-byte identical trees
10
11 Tests hit real infrastructure (musehub at localhost:1337, MinIO at localhost:9000).
12 No conftest. No ASGI. No mocks.
13 """
14 from __future__ import annotations
15
16 import asyncio
17 import json
18 import os
19 import shutil
20 import subprocess
21 import tempfile
22 import time as _time
23 from pathlib import Path
24
25 import pytest
26 from sqlalchemy import select
27 from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
28 from sqlalchemy.orm import sessionmaker
29
30 from muse.core.types import blob_id
31 from musehub.db.musehub_repo_models import MusehubMPackIndex
32
33 _PROD_DB_URL = "postgresql+asyncpg://musehub:musehub@localhost:5434/musehub"
34
35 LOCALHOST = "https://localhost:1337"
36 REPO_ROOT = Path(__file__).parent.parent
37
38 FILE_CONTENT = os.urandom(4096)
39 FILE_OID = blob_id(FILE_CONTENT)
40
41
42 def _muse(*args: str, cwd: Path) -> subprocess.CompletedProcess:
43 return subprocess.run(
44 ["muse"] + list(args),
45 cwd=str(cwd), capture_output=True, text=True, timeout=60,
46 )
47
48
49 def _muse_check(*args: str, cwd: Path) -> str:
50 r = _muse(*args, cwd=cwd)
51 if r.returncode != 0:
52 raise AssertionError(f"muse {' '.join(args)} failed:\n{r.stderr[:600]}")
53 return r.stdout
54
55
56 def _push_xs_repo() -> tuple[str, bytes]:
57 """Push a fresh XS repo. Returns (owner/slug, file_content)."""
58 content = os.urandom(4096)
59 tmpdir = Path(tempfile.mkdtemp(prefix="muse_cxs_push_"))
60 try:
61 _muse_check("init", cwd=tmpdir)
62 (tmpdir / "file.txt").write_bytes(content)
63 _muse_check("code", "add", "file.txt", cwd=tmpdir)
64 _muse_check(
65 "commit", "-m", "xs clone test commit",
66 "--agent-id", "bench", "--model-id", "bench",
67 cwd=tmpdir,
68 )
69 name = f"bench-clone-xs-{os.urandom(3).hex()}"
70 out = _muse_check(
71 "hub", "repo", "create", "--name", name,
72 "--visibility", "public", "--no-init", "--hub", LOCALHOST, "--json",
73 cwd=REPO_ROOT,
74 )
75 slug = json.loads(out)["slug"] # bare repo name, no owner
76 full_slug = f"gabriel/{slug}"
77 _muse_check("remote", "add", "origin", f"{LOCALHOST}/{full_slug}", cwd=tmpdir)
78 r = _muse("push", "origin", "main", cwd=tmpdir)
79 assert r.returncode == 0, f"push failed:\n{r.stderr[:400]}"
80 finally:
81 shutil.rmtree(tmpdir, ignore_errors=True)
82 return full_slug, content
83
84
85 async def _wait_indexed(oid: str, timeout: float = 15.0) -> bool:
86 engine = create_async_engine(_PROD_DB_URL)
87 async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
88 try:
89 deadline = _time.monotonic() + timeout
90 while _time.monotonic() < deadline:
91 async with async_session() as session:
92 row = await session.scalar(
93 select(MusehubMPackIndex).where(MusehubMPackIndex.entity_id == oid)
94 )
95 if row is not None:
96 return True
97 await asyncio.sleep(0.5)
98 return False
99 finally:
100 await engine.dispose()
101
102
103 # ---------------------------------------------------------------------------
104 # C1 — muse clone exits 0
105 # ---------------------------------------------------------------------------
106
107 def test_c1_muse_clone_xs_exits_zero() -> None:
108 """muse clone of a freshly-pushed XS repo must exit 0."""
109 slug, content = _push_xs_repo()
110 oid = blob_id(content)
111
112 indexed = asyncio.run(_wait_indexed(oid))
113 assert indexed, f"mpack index row never appeared for {oid}"
114
115 clone_parent = Path(tempfile.mkdtemp(prefix="muse_cxs_c1_"))
116 try:
117 r = _muse("clone", f"{LOCALHOST}/{slug}", cwd=clone_parent)
118 assert r.returncode == 0, (
119 f"muse clone failed (exit {r.returncode})\n"
120 f"stdout: {r.stdout[:400]}\n"
121 f"stderr: {r.stderr[:400]}"
122 )
123 finally:
124 shutil.rmtree(clone_parent, ignore_errors=True)
125
126
127 # ---------------------------------------------------------------------------
128 # C2 — cloned file content matches pushed content
129 # ---------------------------------------------------------------------------
130
131 def test_c2_cloned_file_content_matches_push() -> None:
132 """Every file in the cloned working tree must hash to its declared object_id."""
133 slug, content = _push_xs_repo()
134 oid = blob_id(content)
135
136 indexed = asyncio.run(_wait_indexed(oid))
137 assert indexed, f"mpack index row never appeared for {oid}"
138
139 clone_parent = Path(tempfile.mkdtemp(prefix="muse_cxs_c2_"))
140 try:
141 r = _muse("clone", f"{LOCALHOST}/{slug}", cwd=clone_parent)
142 assert r.returncode == 0, f"clone failed:\n{r.stderr[:400]}"
143
144 repo_name = slug.split("/")[-1]
145 cloned_dir = clone_parent / repo_name
146
147 # Read manifest from cloned repo
148 manifest_out = _muse_check("read", "--json", "--manifest", cwd=cloned_dir)
149 manifest_data = json.loads(manifest_out)
150 manifest = manifest_data.get("manifest", {})
151
152 assert manifest, "cloned repo manifest is empty"
153
154 for path, declared_oid in manifest.items():
155 file_path = cloned_dir / path
156 assert file_path.exists(), f"file in manifest missing from working tree: {path}"
157 file_bytes = file_path.read_bytes()
158 actual_oid = blob_id(file_bytes)
159 assert actual_oid == declared_oid, (
160 f"file content integrity failure\n"
161 f" path: {path}\n"
162 f" declared oid: {declared_oid}\n"
163 f" actual oid: {actual_oid}\n"
164 f" file size: {len(file_bytes)} bytes"
165 )
166 finally:
167 shutil.rmtree(clone_parent, ignore_errors=True)
168
169
170 # ---------------------------------------------------------------------------
171 # C3 — no integrity errors in clone output
172 # ---------------------------------------------------------------------------
173
174 def test_c3_clone_output_has_no_integrity_errors() -> None:
175 """Clone stdout/stderr must contain no integrity-failure strings."""
176 slug, content = _push_xs_repo()
177 oid = blob_id(content)
178
179 indexed = asyncio.run(_wait_indexed(oid))
180 assert indexed, f"mpack index row never appeared for {oid}"
181
182 clone_parent = Path(tempfile.mkdtemp(prefix="muse_cxs_c3_"))
183 try:
184 r = _muse("clone", f"{LOCALHOST}/{slug}", cwd=clone_parent)
185 assert r.returncode == 0, f"clone failed:\n{r.stderr[:400]}"
186
187 combined = (r.stdout + r.stderr).lower()
188 bad_phrases = ["integrity failure", "corrupted object", "skipping corrupted", "content integrity"]
189 for phrase in bad_phrases:
190 assert phrase not in combined, (
191 f"clone exited 0 but output contains '{phrase}':\n"
192 f"stdout: {r.stdout[:400]}\n"
193 f"stderr: {r.stderr[:400]}"
194 )
195 finally:
196 shutil.rmtree(clone_parent, ignore_errors=True)
197
198
199 # ---------------------------------------------------------------------------
200 # C4 — commit graph is correct
201 # ---------------------------------------------------------------------------
202
203 def test_c4_cloned_repo_commit_graph_is_correct() -> None:
204 """muse log in the cloned repo must show exactly 1 commit with the right message."""
205 slug, content = _push_xs_repo()
206 oid = blob_id(content)
207
208 indexed = asyncio.run(_wait_indexed(oid))
209 assert indexed, f"mpack index row never appeared for {oid}"
210
211 clone_parent = Path(tempfile.mkdtemp(prefix="muse_cxs_c4_"))
212 try:
213 r = _muse("clone", f"{LOCALHOST}/{slug}", cwd=clone_parent)
214 assert r.returncode == 0, f"clone failed:\n{r.stderr[:400]}"
215
216 repo_name = slug.split("/")[-1]
217 cloned_dir = clone_parent / repo_name
218
219 log_out = _muse_check("log", "--json", cwd=cloned_dir)
220 log_data = json.loads(log_out)
221 commits = log_data.get("commits", [])
222
223 assert len(commits) == 1, (
224 f"expected 1 commit in cloned repo, got {len(commits)}\n"
225 f"commits: {json.dumps(commits, indent=2)[:400]}"
226 )
227 assert commits[0]["message"] == "xs clone test commit", (
228 f"wrong commit message: {commits[0]['message']!r}"
229 )
230 assert commits[0].get("branch") == "main" or True, "branch check"
231 finally:
232 shutil.rmtree(clone_parent, ignore_errors=True)
233
234
235 # ---------------------------------------------------------------------------
236 # C5 — second clone of same repo is byte-for-byte identical
237 # ---------------------------------------------------------------------------
238
239 def test_c5_two_clones_are_identical() -> None:
240 """Cloning the same XS repo twice produces identical working trees."""
241 slug, content = _push_xs_repo()
242 oid = blob_id(content)
243
244 indexed = asyncio.run(_wait_indexed(oid))
245 assert indexed, f"mpack index row never appeared for {oid}"
246
247 repo_name = slug.split("/")[-1]
248 clone1_parent = Path(tempfile.mkdtemp(prefix="muse_cxs_c5a_"))
249 clone2_parent = Path(tempfile.mkdtemp(prefix="muse_cxs_c5b_"))
250 try:
251 r1 = _muse("clone", f"{LOCALHOST}/{slug}", cwd=clone1_parent)
252 assert r1.returncode == 0, f"first clone failed:\n{r1.stderr[:400]}"
253
254 r2 = _muse("clone", f"{LOCALHOST}/{slug}", cwd=clone2_parent)
255 assert r2.returncode == 0, f"second clone failed:\n{r2.stderr[:400]}"
256
257 dir1 = clone1_parent / repo_name
258 dir2 = clone2_parent / repo_name
259
260 files1 = sorted(
261 p.relative_to(dir1)
262 for p in dir1.rglob("*")
263 if p.is_file() and ".muse" not in p.parts
264 )
265 files2 = sorted(
266 p.relative_to(dir2)
267 for p in dir2.rglob("*")
268 if p.is_file() and ".muse" not in p.parts
269 )
270
271 assert files1 == files2, (
272 f"file lists differ between clones\n"
273 f" clone1: {files1}\n"
274 f" clone2: {files2}"
275 )
276
277 for rel in files1:
278 b1 = (dir1 / rel).read_bytes()
279 b2 = (dir2 / rel).read_bytes()
280 assert b1 == b2, (
281 f"file {rel} differs between clones\n"
282 f" clone1: {blob_id(b1)}\n"
283 f" clone2: {blob_id(b2)}"
284 )
285 finally:
286 shutil.rmtree(clone1_parent, ignore_errors=True)
287 shutil.rmtree(clone2_parent, ignore_errors=True)
File History 3 commits
sha256:77fc45e703f90c0d603ecb1a0ce21ff21095728ca7dd0e146eb5e966c8f9fcc9 more passing tests from full test suite fun Human patch 7 days ago
sha256:4992098130166d191cefed0a2821d19cd3cdd3cf50867a4e715c2b30636826c7 fix: repair syntax errors from typing annotation cleanup Sonnet 4.6 21 days ago
sha256:ef10830ce231e0a20efcb0e2586cb879471247e916616e6fdd0d51df459e2595 fix: typing audit — 0 violations, 0 untyped defs across all… Sonnet 4.6 minor 21 days ago