gabriel / musehub public

test_mists.py file-level

at sha256:3 · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 πŸ’₯ blast risk
sha256:0 fix: fall back to any indexed mpack in read_object_bytes when push mpac… · gabriel · Jun 17, 2026
1 """Section 18 β€” Mists Stress Suite (Tier 4).
2
3 Covers the three stress scenarios from the Phase 8 spec:
4
5 Concurrent creates 50 mists created concurrently via asyncio.gather β€”
6 all must succeed with unique mist_ids; no DB-level
7 serialisation errors or integrity violations.
8
9 Large explore 200 mists seeded directly into the DB; the explore
10 endpoint must return all of them across cursor-paginated
11 requests with correct total, no duplicates, and no
12 dropped rows.
13
14 Fork chains Deep fork chain built to the platform maximum (depth 5)
15 via repeated POST /api/mists/{id}/fork calls; the chain
16 must complete without error and the depth-5 node must
17 carry fork_depth=5 and a populated fork_parent_id.
18
19 All tests target the HTTP layer (AsyncClient) to exercise the full stack:
20 auth, validation, service, and ORM.
21 """
22 from __future__ import annotations
23
24 import asyncio
25 import secrets
26 import time
27 from datetime import datetime, timezone
28
29 import pytest
30 from httpx import AsyncClient
31 from sqlalchemy.ext.asyncio import AsyncSession
32
33 from musehub.core.genesis import compute_identity_id, compute_repo_id
34 from musehub.db.musehub_repo_models import MusehubRepo
35 from musehub.types.json_types import JSONObject, JSONValue, StrDict
36
37 _OWNER = "testuser" # matches conftest._TEST_HANDLE
38
39
40 def _payload(**overrides: JSONValue) -> JSONObject:
41 base: JSONObject = {
42 "filename": f"stress_{secrets.token_hex(4)}.py",
43 "content": f"# stress test\nvalue = {secrets.token_hex(16)!r}\n",
44 "visibility": "public",
45 }
46 base.update(overrides)
47 return base
48
49
50 async def _create(client: AsyncClient, headers: StrDict, **overrides: JSONValue) -> JSONObject:
51 r = await client.post("/api/mists", json=_payload(**overrides), headers=headers)
52 assert r.status_code == 201, r.text
53 return dict(r.json())
54
55
56 # ═══════════════════════════════════════════════════════════════════════════════
57 # Concurrent creates
58 # ═══════════════════════════════════════════════════════════════════════════════
59
60 class TestConcurrentCreates:
61 """15 mists created concurrently β€” all must succeed, all IDs unique."""
62
63 @pytest.mark.anyio
64 async def test_concurrent_creates_all_succeed(
65 self, client: AsyncClient, auth_headers: StrDict
66 ) -> None:
67 n = 15
68
69 async def _one() -> JSONObject:
70 return await _create(client, auth_headers)
71
72 results = await asyncio.gather(*[_one() for _ in range(n)])
73
74 assert len(results) == n, f"Expected {n} results, got {len(results)}"
75 ids = [r["mistId"] for r in results]
76 assert len(set(ids)) == n, (
77 f"Expected {n} unique mist IDs, got {len(set(ids))} β€” "
78 "duplicate content across concurrent requests"
79 )
80
81 @pytest.mark.anyio
82 async def test_concurrent_creates_all_visible_in_list(
83 self, client: AsyncClient, auth_headers: StrDict
84 ) -> None:
85 """Created mists must all appear in the owner list."""
86 unique_tag = secrets.token_hex(6)
87 n = 15
88
89 async def _one() -> str:
90 r = await _create(client, auth_headers, tags=[unique_tag])
91 return r["mistId"]
92
93 created_ids = set(await asyncio.gather(*[_one() for _ in range(n)]))
94
95 # Paginate the owner list and collect all IDs with our tag.
96 found: set[str] = set()
97 cursor: str | None = None
98 while True:
99 params: JSONObject = {"limit": 100}
100 if cursor:
101 params["cursor"] = cursor
102 r = await client.get(f"/api/{_OWNER}/mists", params=params)
103 assert r.status_code == 200
104 body = r.json()
105 for m in body["mists"]:
106 if unique_tag in (m.get("tags") or []):
107 found.add(m["mistId"])
108 cursor = body.get("nextCursor")
109 if not cursor:
110 break
111
112 assert created_ids == found, (
113 f"Created {len(created_ids)} mists but found {len(found)} with tag"
114 )
115
116
117 # ═══════════════════════════════════════════════════════════════════════════════
118 # Large explore β€” 200 mist seed + full pagination
119 # ═══════════════════════════════════════════════════════════════════════════════
120
121 class TestLargeExplore:
122 """Seed 200 mists directly into the DB then paginate explore to collect all."""
123
124 @pytest.mark.anyio
125 async def test_200_mist_explore_no_duplicates(
126 self, client: AsyncClient, db_session: AsyncSession
127 ) -> None:
128 from muse.plugins.mist.plugin import compute_mist_id
129 from musehub.services.musehub_mists import create_mist as _svc_create
130
131 # Use a unique artifact_type so this test's rows are isolated.
132 unique_type = f"stress_{secrets.token_hex(4)}"
133 n = 200
134
135 _stress_owner_id = compute_identity_id(b"stressuser")
136 for i in range(n):
137 content = f"# stress explore {i}\nvalue = {secrets.token_hex(16)!r}\n"
138 mid = compute_mist_id(content.encode())
139 _slug = f"mist_{mid}"
140 _created_at = datetime.now(tz=timezone.utc)
141 repo = MusehubRepo(
142 repo_id=compute_repo_id(_stress_owner_id, _slug, "code", _created_at.isoformat()),
143 name=f"repo_{mid}", owner="stressuser", slug=_slug,
144 visibility="public", owner_user_id=_stress_owner_id,
145 created_at=_created_at, updated_at=_created_at,
146 )
147 db_session.add(repo)
148 await db_session.flush()
149 await _svc_create(
150 db_session,
151 mist_id=mid,
152 filename=f"stress_{i:04d}.py",
153 content=content,
154 owner="stressuser",
155 repo_id=str(repo.repo_id),
156 artifact_type=unique_type,
157 )
158 await db_session.commit()
159
160 # Paginate through all pages and collect IDs.
161 collected: list[str] = []
162 cursor: str | None = None
163 pages = 0
164
165 while True:
166 params: JSONObject = {
167 "artifact_type": unique_type,
168 "limit": 20,
169 }
170 if cursor:
171 params["cursor"] = cursor
172 r = await client.get("/api/mists/explore", params=params)
173 assert r.status_code == 200, r.text
174 body = r.json()
175 collected.extend(m["mistId"] for m in body["mists"])
176 cursor = body.get("nextCursor")
177 pages += 1
178 if not cursor:
179 break
180
181 assert len(collected) == n, (
182 f"Expected {n} mists, collected {len(collected)} across {pages} page(s)"
183 )
184 assert len(set(collected)) == n, (
185 f"Duplicate IDs in paginated explore output ({len(collected) - len(set(collected))} dups)"
186 )
187
188 @pytest.mark.anyio
189 async def test_large_explore_total_count_accurate(
190 self, client: AsyncClient, db_session: AsyncSession
191 ) -> None:
192 """The total field on the first page must reflect the full seed count."""
193 from muse.plugins.mist.plugin import compute_mist_id
194 from musehub.services.musehub_mists import create_mist as _svc_create
195
196 unique_type = f"cnt_{secrets.token_hex(4)}"
197 n = 50
198
199 _count_owner_id = compute_identity_id(b"countuser")
200 for i in range(n):
201 content = f"# count check {i} {secrets.token_hex(16)}"
202 mid = compute_mist_id(content.encode())
203 _slug = f"rcm_{mid}"
204 _created_at = datetime.now(tz=timezone.utc)
205 repo = MusehubRepo(
206 repo_id=compute_repo_id(_count_owner_id, _slug, "code", _created_at.isoformat()),
207 name=f"rc_{mid}", owner="countuser", slug=_slug,
208 visibility="public", owner_user_id=_count_owner_id,
209 created_at=_created_at, updated_at=_created_at,
210 )
211 db_session.add(repo)
212 await db_session.flush()
213 await _svc_create(
214 db_session,
215 mist_id=mid,
216 filename=f"count_{i}.py",
217 content=content,
218 owner="countuser",
219 repo_id=str(repo.repo_id),
220 artifact_type=unique_type,
221 )
222 await db_session.commit()
223
224 r = await client.get(
225 "/api/mists/explore",
226 params={"artifact_type": unique_type, "limit": 10},
227 )
228 assert r.status_code == 200
229 body = r.json()
230 assert body["total"] == n, (
231 f"total={body['total']} but seeded {n} mists with type={unique_type!r}"
232 )
233
234 @pytest.mark.anyio
235 async def test_large_explore_under_2s(
236 self, client: AsyncClient, db_session: AsyncSession
237 ) -> None:
238 """First-page explore of 200+ mists must respond in under 2 seconds."""
239 from muse.plugins.mist.plugin import compute_mist_id
240 from musehub.services.musehub_mists import create_mist as _svc_create
241
242 unique_type = f"perf_{secrets.token_hex(4)}"
243 n = 100
244
245 _perf_owner_id = compute_identity_id(b"perfuser")
246 for i in range(n):
247 content = f"# perf {i} {secrets.token_hex(16)}"
248 mid = compute_mist_id(content.encode())
249 _slug = f"pm_{mid}"
250 _created_at = datetime.now(tz=timezone.utc)
251 repo = MusehubRepo(
252 repo_id=compute_repo_id(_perf_owner_id, _slug, "code", _created_at.isoformat()),
253 name=f"p_{mid}", owner="perfuser", slug=_slug,
254 visibility="public", owner_user_id=_perf_owner_id,
255 created_at=_created_at, updated_at=_created_at,
256 )
257 db_session.add(repo)
258 await db_session.flush()
259 await _svc_create(
260 db_session,
261 mist_id=mid,
262 filename=f"perf_{i}.py",
263 content=content,
264 owner="perfuser",
265 repo_id=str(repo.repo_id),
266 artifact_type=unique_type,
267 )
268 await db_session.commit()
269
270 start = time.monotonic()
271 r = await client.get(
272 "/api/mists/explore",
273 params={"artifact_type": unique_type, "limit": 20},
274 )
275 elapsed = time.monotonic() - start
276
277 assert r.status_code == 200
278 assert elapsed < 2.0, f"Explore took {elapsed:.3f}s β€” expected < 2s"
279
280
281 # ═══════════════════════════════════════════════════════════════════════════════
282 # Fork chains
283 # ═══════════════════════════════════════════════════════════════════════════════
284
285 class TestForkChains:
286 """Full-depth fork chain built via HTTP; structural invariants verified."""
287
288 @pytest.mark.anyio
289 async def test_fork_chain_depth_and_parent_ids(
290 self, client: AsyncClient, auth_headers: StrDict
291 ) -> None:
292 """Each fork has the correct fork_parent_id and fork_depth."""
293 root = await _create(client, auth_headers)
294 root_id = root["mistId"]
295
296 chain = [root_id]
297 for depth in range(1, 6):
298 r = await client.post(
299 f"/api/mists/{chain[-1]}/fork", headers=auth_headers
300 )
301 assert r.status_code == 201, (
302 f"Fork at depth {depth} failed: {r.status_code} {r.text}"
303 )
304 fork_id = r.json()["mistId"]
305 chain.append(fork_id)
306
307 # Verify each fork's metadata.
308 for depth in range(1, 6):
309 r = await client.get(f"/api/mists/{chain[depth]}")
310 assert r.status_code == 200
311 body = r.json()
312 assert body["forkParentId"] == chain[depth - 1], (
313 f"Depth {depth}: forkParentId={body['forkParentId']!r}, "
314 f"expected {chain[depth - 1]!r}"
315 )
316 assert body["forkDepth"] == depth, (
317 f"Depth {depth}: forkDepth={body['forkDepth']!r}, expected {depth}"
318 )
319
320 @pytest.mark.anyio
321 async def test_fork_inherits_content_and_filename(
322 self, client: AsyncClient, auth_headers: StrDict
323 ) -> None:
324 root = await _create(
325 client, auth_headers,
326 content=f"def root_fn(): return 'root'\n{secrets.token_hex(16)}",
327 filename="root.py",
328 )
329 r = await client.post(
330 f"/api/mists/{root['mistId']}/fork", headers=auth_headers
331 )
332 assert r.status_code == 201
333 fork_id = r.json()["mistId"]
334
335 r2 = await client.get(f"/api/mists/{fork_id}")
336 assert r2.status_code == 200
337 body = r2.json()
338 assert body["content"] == root["content"]
339 assert body["filename"] == root["filename"]
340
341 @pytest.mark.anyio
342 async def test_fork_count_increments_on_parent(
343 self, client: AsyncClient, auth_headers: StrDict
344 ) -> None:
345 root = await _create(client, auth_headers)
346 initial_forks = root.get("forkCount", 0)
347
348 for _ in range(3):
349 r = await client.post(
350 f"/api/mists/{root['mistId']}/fork", headers=auth_headers
351 )
352 assert r.status_code == 201
353
354 r = await client.get(f"/api/mists/{root['mistId']}")
355 assert r.status_code == 200
356 assert r.json()["forkCount"] == initial_forks + 3
357
358 @pytest.mark.anyio
359 async def test_fork_chain_completes_under_5s(
360 self, client: AsyncClient, auth_headers: StrDict
361 ) -> None:
362 """Building a full 5-fork chain must complete in under 5 seconds."""
363 root = await _create(client, auth_headers)
364 current_id = root["mistId"]
365
366 start = time.monotonic()
367 for _ in range(5):
368 r = await client.post(
369 f"/api/mists/{current_id}/fork", headers=auth_headers
370 )
371 assert r.status_code == 201
372 current_id = r.json()["mistId"]
373 elapsed = time.monotonic() - start
374
375 assert elapsed < 5.0, f"Fork chain took {elapsed:.3f}s β€” expected < 5s"