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