gabriel / musehub public
test_fetch_xs_unit.py python
284 lines 10.9 KB
Raw
sha256:77fc45e703f90c0d603ecb1a0ce21ff21095728ca7dd0e146eb5e966c8f9fcc9 more passing tests from full test suite fun Human patch 20 hours ago
1 """Fetch XS unit tests — issue #66.
2
3 One verb. One size. Proven correct at each layer before moving to the next.
4
5 F1 muse fetch exits 0 — push XS, clone it, push delta, fetch exits 0
6 F2 fetched object content correct — delta file object is locally readable with correct bytes
7 F3 no integrity errors — fetch stdout/stderr clean even when exit 0
8 F4 fetch is idempotent — two fetches in a row both exit 0, same result
9 F5 fetch does not clobber local — original file still intact after fetching delta
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 socket
21 import subprocess
22 import tempfile
23 import time as _time
24 from pathlib import Path
25
26 import pytest
27
28
29 def _port_open(host: str, port: int) -> bool:
30 try:
31 with socket.create_connection((host, port), timeout=1):
32 return True
33 except OSError:
34 return False
35
36
37 pytestmark = pytest.mark.skipif(
38 not (_port_open("localhost", 1337) and _port_open("localhost", 9000)),
39 reason="live infrastructure not available — start with docker compose up minio createbuckets musehub",
40 )
41 from sqlalchemy import select
42 from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
43 from sqlalchemy.orm import sessionmaker
44
45 from muse.core.types import blob_id
46 from musehub.db.musehub_repo_models import MusehubMPackIndex
47
48 _PROD_DB_URL = "postgresql+asyncpg://musehub:musehub@localhost:5434/musehub"
49
50 LOCALHOST = "https://localhost:1337"
51 REPO_ROOT = Path(__file__).parent.parent
52
53
54 def _muse(*args: str, cwd: Path) -> subprocess.CompletedProcess:
55 return subprocess.run(
56 ["muse"] + list(args),
57 cwd=str(cwd), capture_output=True, text=True, timeout=60,
58 )
59
60
61 def _muse_check(*args: str, cwd: Path) -> str:
62 r = _muse(*args, cwd=cwd)
63 if r.returncode != 0:
64 raise AssertionError(f"muse {' '.join(args)} failed:\n{r.stderr[:600]}")
65 return r.stdout
66
67
68 async def _wait_indexed(oid: str, timeout: float = 15.0) -> bool:
69 engine = create_async_engine(_PROD_DB_URL)
70 async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
71 try:
72 deadline = _time.monotonic() + timeout
73 while _time.monotonic() < deadline:
74 async with async_session() as session:
75 row = await session.scalar(
76 select(MusehubMPackIndex).where(MusehubMPackIndex.entity_id == oid)
77 )
78 if row is not None:
79 return True
80 await asyncio.sleep(0.5)
81 return False
82 finally:
83 await engine.dispose()
84
85
86 def _setup_fetch_scenario() -> tuple[str, Path, bytes, bytes]:
87 """Push XS repo, clone it, push a delta commit.
88
89 Returns (full_slug, clone_dir, original_content, delta_content).
90 Caller is responsible for cleaning up clone_dir.
91 """
92 original_content = os.urandom(4096)
93 delta_content = os.urandom(4096)
94
95 # --- push initial XS repo ---
96 push_dir = Path(tempfile.mkdtemp(prefix="muse_fxs_push_"))
97 try:
98 _muse_check("init", cwd=push_dir)
99 (push_dir / "original.txt").write_bytes(original_content)
100 _muse_check("code", "add", "original.txt", cwd=push_dir)
101 _muse_check(
102 "commit", "-m", "xs fetch test: initial commit",
103 "--agent-id", "bench", "--model-id", "bench",
104 cwd=push_dir,
105 )
106 name = f"bench-fetch-xs-{os.urandom(3).hex()}"
107 out = _muse_check(
108 "hub", "repo", "create", "--name", name,
109 "--visibility", "public", "--no-init", "--hub", LOCALHOST, "--json",
110 cwd=REPO_ROOT,
111 )
112 slug = json.loads(out)["slug"] # bare name
113 full_slug = f"gabriel/{slug}"
114 _muse_check("remote", "add", "origin", f"{LOCALHOST}/{full_slug}", cwd=push_dir)
115 r = _muse("push", "origin", "main", cwd=push_dir)
116 assert r.returncode == 0, f"initial push failed:\n{r.stderr[:400]}"
117 finally:
118 shutil.rmtree(push_dir, ignore_errors=True)
119
120 # wait for mpack index so clone can resolve the object
121 orig_oid = blob_id(original_content)
122 indexed = asyncio.run(_wait_indexed(orig_oid))
123 assert indexed, f"mpack index row never appeared for initial object {orig_oid}"
124
125 # --- clone ---
126 clone_parent = Path(tempfile.mkdtemp(prefix="muse_fxs_clone_"))
127 _muse_check("clone", f"{LOCALHOST}/{full_slug}", cwd=clone_parent)
128 repo_name = slug.split("/")[-1] if "/" in slug else slug
129 clone_dir = clone_parent / repo_name
130
131 # --- push delta commit from a fresh copy ---
132 delta_dir = Path(tempfile.mkdtemp(prefix="muse_fxs_delta_"))
133 try:
134 shutil.copytree(str(clone_dir), str(delta_dir / "repo"), symlinks=False)
135 delta_repo = delta_dir / "repo"
136 (delta_repo / "delta.txt").write_bytes(delta_content)
137 _muse_check("code", "add", "delta.txt", cwd=delta_repo)
138 _muse_check(
139 "commit", "-m", "xs fetch test: delta commit",
140 "--agent-id", "bench", "--model-id", "bench",
141 cwd=delta_repo,
142 )
143 r = _muse("push", "origin", "main", cwd=delta_repo)
144 assert r.returncode == 0, f"delta push failed:\n{r.stderr[:400]}"
145 finally:
146 shutil.rmtree(delta_dir, ignore_errors=True)
147
148 # wait for delta object to be indexed before fetching
149 delta_oid = blob_id(delta_content)
150 indexed = asyncio.run(_wait_indexed(delta_oid))
151 assert indexed, f"mpack index row never appeared for delta object {delta_oid}"
152
153 return full_slug, clone_dir, original_content, delta_content
154
155
156 # ---------------------------------------------------------------------------
157 # F1 — muse fetch exits 0
158 # ---------------------------------------------------------------------------
159
160 def test_f1_muse_fetch_xs_exits_zero() -> None:
161 """muse fetch of a 1-commit delta from a cloned XS repo must exit 0."""
162 _, clone_dir, _, _ = _setup_fetch_scenario()
163 try:
164 r = _muse("fetch", "origin", cwd=clone_dir)
165 assert r.returncode == 0, (
166 f"muse fetch failed (exit {r.returncode})\n"
167 f"stdout: {r.stdout[:400]}\n"
168 f"stderr: {r.stderr[:400]}"
169 )
170 finally:
171 shutil.rmtree(clone_dir.parent, ignore_errors=True)
172
173
174 # ---------------------------------------------------------------------------
175 # F2 — fetched object content is correct
176 # ---------------------------------------------------------------------------
177
178 def test_f2_fetched_object_content_is_correct() -> None:
179 """After fetch, the delta object must be readable locally with correct bytes.
180
181 Verified by merging the fetched remote branch and reading the file from disk.
182 """
183 _, clone_dir, original_content, delta_content = _setup_fetch_scenario()
184 try:
185 r = _muse("fetch", "origin", cwd=clone_dir)
186 assert r.returncode == 0, f"fetch failed:\n{r.stderr[:400]}"
187
188 # Merge the fetched remote tip into local main so the file lands on disk.
189 _muse_check("merge", "origin/main", cwd=clone_dir)
190
191 delta_file = clone_dir / "delta.txt"
192 assert delta_file.exists(), "delta.txt not present after fetch + merge"
193
194 actual_bytes = delta_file.read_bytes()
195 expected_oid = blob_id(delta_content)
196 actual_oid = blob_id(actual_bytes)
197 assert actual_oid == expected_oid, (
198 f"delta.txt content integrity failure\n"
199 f" expected oid: {expected_oid}\n"
200 f" actual oid: {actual_oid}\n"
201 f" file size: {len(actual_bytes)} bytes"
202 )
203 finally:
204 shutil.rmtree(clone_dir.parent, ignore_errors=True)
205
206
207 # ---------------------------------------------------------------------------
208 # F3 — no integrity errors in fetch output
209 # ---------------------------------------------------------------------------
210
211 def test_f3_fetch_output_has_no_integrity_errors() -> None:
212 """Fetch stdout/stderr must contain no integrity-failure strings."""
213 _, clone_dir, _, _ = _setup_fetch_scenario()
214 try:
215 r = _muse("fetch", "origin", cwd=clone_dir)
216 assert r.returncode == 0, f"fetch failed:\n{r.stderr[:400]}"
217
218 combined = (r.stdout + r.stderr).lower()
219 bad_phrases = ["integrity failure", "corrupted object", "skipping corrupted", "content integrity"]
220 for phrase in bad_phrases:
221 assert phrase not in combined, (
222 f"fetch exited 0 but output contains '{phrase}':\n"
223 f"stdout: {r.stdout[:400]}\n"
224 f"stderr: {r.stderr[:400]}"
225 )
226 finally:
227 shutil.rmtree(clone_dir.parent, ignore_errors=True)
228
229
230 # ---------------------------------------------------------------------------
231 # F4 — fetch is idempotent
232 # ---------------------------------------------------------------------------
233
234 def test_f4_fetch_is_idempotent() -> None:
235 """Running muse fetch origin twice must both exit 0 with identical state."""
236 _, clone_dir, _, delta_content = _setup_fetch_scenario()
237 try:
238 r1 = _muse("fetch", "origin", cwd=clone_dir)
239 assert r1.returncode == 0, f"first fetch failed:\n{r1.stderr[:400]}"
240
241 r2 = _muse("fetch", "origin", cwd=clone_dir)
242 assert r2.returncode == 0, f"second fetch failed:\n{r2.stderr[:400]}"
243
244 # After merging, the delta file must be present and correct.
245 _muse_check("merge", "origin/main", cwd=clone_dir)
246 delta_file = clone_dir / "delta.txt"
247 assert delta_file.exists(), "delta.txt missing after two fetches + merge"
248
249 actual_oid = blob_id(delta_file.read_bytes())
250 expected_oid = blob_id(delta_content)
251 assert actual_oid == expected_oid, (
252 f"delta.txt integrity failure after idempotent fetch\n"
253 f" expected: {expected_oid}\n"
254 f" actual: {actual_oid}"
255 )
256 finally:
257 shutil.rmtree(clone_dir.parent, ignore_errors=True)
258
259
260 # ---------------------------------------------------------------------------
261 # F5 — fetch does not clobber existing local content
262 # ---------------------------------------------------------------------------
263
264 def test_f5_fetch_does_not_clobber_local_content() -> None:
265 """original.txt must be byte-for-byte intact after fetching the delta commit."""
266 _, clone_dir, original_content, _ = _setup_fetch_scenario()
267 try:
268 r = _muse("fetch", "origin", cwd=clone_dir)
269 assert r.returncode == 0, f"fetch failed:\n{r.stderr[:400]}"
270
271 original_file = clone_dir / "original.txt"
272 assert original_file.exists(), "original.txt missing after fetch"
273
274 actual_bytes = original_file.read_bytes()
275 expected_oid = blob_id(original_content)
276 actual_oid = blob_id(actual_bytes)
277 assert actual_oid == expected_oid, (
278 f"original.txt was clobbered by fetch\n"
279 f" expected oid: {expected_oid}\n"
280 f" actual oid: {actual_oid}\n"
281 f" file size: {len(actual_bytes)} bytes"
282 )
283 finally:
284 shutil.rmtree(clone_dir.parent, ignore_errors=True)
File History 3 commits
sha256:77fc45e703f90c0d603ecb1a0ce21ff21095728ca7dd0e146eb5e966c8f9fcc9 more passing tests from full test suite fun Human patch 20 hours ago
sha256:4992098130166d191cefed0a2821d19cd3cdd3cf50867a4e715c2b30636826c7 fix: repair syntax errors from typing annotation cleanup Sonnet 4.6 14 days ago
sha256:ef10830ce231e0a20efcb0e2586cb879471247e916616e6fdd0d51df459e2595 fix: typing audit — 0 violations, 0 untyped defs across all… Sonnet 4.6 minor 14 days ago