gabriel / musehub public
test_mists.py python
360 lines 13.8 KB
Raw
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ breaking 156 days ago
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 time
26 import uuid
27
28 import pytest
29 from httpx import AsyncClient
30 from sqlalchemy.ext.asyncio import AsyncSession
31
32 from musehub.types.json_types import JSONObject
33
34 _OWNER = "testuser" # matches conftest._TEST_HANDLE
35
36
37 def _payload(**overrides: object) -> JSONObject:
38 base: JSONObject = {
39 "filename": f"stress_{uuid.uuid4().hex[:8]}.py",
40 "content": f"# stress test\nvalue = {uuid.uuid4().hex!r}\n",
41 "visibility": "public",
42 }
43 base.update(overrides)
44 return base
45
46
47 async def _create(client: AsyncClient, headers: dict, **overrides: object) -> JSONObject:
48 r = await client.post("/api/mists", json=_payload(**overrides), headers=headers)
49 assert r.status_code == 201, r.text
50 return dict(r.json())
51
52
53 # ═══════════════════════════════════════════════════════════════════════════════
54 # Concurrent creates
55 # ═══════════════════════════════════════════════════════════════════════════════
56
57 class TestConcurrentCreates:
58 """15 mists created concurrently — all must succeed, all IDs unique."""
59
60 @pytest.mark.anyio
61 async def test_concurrent_creates_all_succeed(
62 self, client: AsyncClient, auth_headers: dict
63 ) -> None:
64 n = 15
65
66 async def _one() -> JSONObject:
67 return await _create(client, auth_headers)
68
69 results = await asyncio.gather(*[_one() for _ in range(n)])
70
71 assert len(results) == n, f"Expected {n} results, got {len(results)}"
72 ids = [r["mistId"] for r in results]
73 assert len(set(ids)) == n, (
74 f"Expected {n} unique mist IDs, got {len(set(ids))} — "
75 "duplicate content across concurrent requests"
76 )
77
78 @pytest.mark.anyio
79 async def test_concurrent_creates_all_visible_in_list(
80 self, client: AsyncClient, auth_headers: dict
81 ) -> None:
82 """Created mists must all appear in the owner list."""
83 unique_tag = uuid.uuid4().hex[:12]
84 n = 15
85
86 async def _one() -> str:
87 r = await _create(client, auth_headers, tags=[unique_tag])
88 return r["mistId"]
89
90 created_ids = set(await asyncio.gather(*[_one() for _ in range(n)]))
91
92 # Paginate the owner list and collect all IDs with our tag.
93 found: set[str] = set()
94 cursor: str | None = None
95 while True:
96 params: dict[str, object] = {"limit": 100}
97 if cursor:
98 params["cursor"] = cursor
99 r = await client.get(f"/api/{_OWNER}/mists", params=params)
100 assert r.status_code == 200
101 body = r.json()
102 for m in body["mists"]:
103 if unique_tag in (m.get("tags") or []):
104 found.add(m["mistId"])
105 cursor = body.get("nextCursor")
106 if not cursor:
107 break
108
109 assert created_ids == found, (
110 f"Created {len(created_ids)} mists but found {len(found)} with tag"
111 )
112
113
114 # ═══════════════════════════════════════════════════════════════════════════════
115 # Large explore — 200 mist seed + full pagination
116 # ═══════════════════════════════════════════════════════════════════════════════
117
118 class TestLargeExplore:
119 """Seed 200 mists directly into the DB then paginate explore to collect all."""
120
121 @pytest.mark.anyio
122 async def test_200_mist_explore_no_duplicates(
123 self, client: AsyncClient, db_session: AsyncSession
124 ) -> None:
125 from muse.plugins.mist.plugin import compute_mist_id
126 from musehub.db.musehub_models import MusehubRepo
127 from musehub.services.musehub_mists import create_mist as _svc_create
128
129 # Use a unique artifact_type so this test's rows are isolated.
130 unique_type = f"stress_{uuid.uuid4().hex[:8]}"
131 n = 200
132
133 for i in range(n):
134 content = f"# stress explore {i}\nvalue = {uuid.uuid4().hex!r}\n"
135 mid = compute_mist_id(content.encode())
136 repo = MusehubRepo(
137 name=f"repo_{mid}", owner="stressuser", slug=f"mist_{mid}",
138 visibility="public", owner_user_id="stressuser",
139 )
140 db_session.add(repo)
141 await db_session.flush()
142 await _svc_create(
143 db_session,
144 mist_id=mid,
145 filename=f"stress_{i:04d}.py",
146 content=content,
147 owner="stressuser",
148 repo_id=str(repo.repo_id),
149 artifact_type=unique_type,
150 )
151 await db_session.commit()
152
153 # Paginate through all pages and collect IDs.
154 collected: list[str] = []
155 cursor: str | None = None
156 pages = 0
157
158 while True:
159 params: dict[str, object] = {
160 "artifact_type": unique_type,
161 "limit": 20,
162 }
163 if cursor:
164 params["cursor"] = cursor
165 r = await client.get("/api/mists/explore", params=params)
166 assert r.status_code == 200, r.text
167 body = r.json()
168 collected.extend(m["mistId"] for m in body["mists"])
169 cursor = body.get("nextCursor")
170 pages += 1
171 if not cursor:
172 break
173
174 assert len(collected) == n, (
175 f"Expected {n} mists, collected {len(collected)} across {pages} page(s)"
176 )
177 assert len(set(collected)) == n, (
178 f"Duplicate IDs in paginated explore output ({len(collected) - len(set(collected))} dups)"
179 )
180
181 @pytest.mark.anyio
182 async def test_large_explore_total_count_accurate(
183 self, client: AsyncClient, db_session: AsyncSession
184 ) -> None:
185 """The total field on the first page must reflect the full seed count."""
186 from muse.plugins.mist.plugin import compute_mist_id
187 from musehub.db.musehub_models import MusehubRepo
188 from musehub.services.musehub_mists import create_mist as _svc_create
189
190 unique_type = f"cnt_{uuid.uuid4().hex[:8]}"
191 n = 50
192
193 for i in range(n):
194 content = f"# count check {i} {uuid.uuid4().hex}"
195 mid = compute_mist_id(content.encode())
196 repo = MusehubRepo(
197 name=f"rc_{mid}", owner="countuser", slug=f"rcm_{mid}",
198 visibility="public", owner_user_id="countuser",
199 )
200 db_session.add(repo)
201 await db_session.flush()
202 await _svc_create(
203 db_session,
204 mist_id=mid,
205 filename=f"count_{i}.py",
206 content=content,
207 owner="countuser",
208 repo_id=str(repo.repo_id),
209 artifact_type=unique_type,
210 )
211 await db_session.commit()
212
213 r = await client.get(
214 "/api/mists/explore",
215 params={"artifact_type": unique_type, "limit": 10},
216 )
217 assert r.status_code == 200
218 body = r.json()
219 assert body["total"] == n, (
220 f"total={body['total']} but seeded {n} mists with type={unique_type!r}"
221 )
222
223 @pytest.mark.anyio
224 async def test_large_explore_under_2s(
225 self, client: AsyncClient, db_session: AsyncSession
226 ) -> None:
227 """First-page explore of 200+ mists must respond in under 2 seconds."""
228 from muse.plugins.mist.plugin import compute_mist_id
229 from musehub.db.musehub_models import MusehubRepo
230 from musehub.services.musehub_mists import create_mist as _svc_create
231
232 unique_type = f"perf_{uuid.uuid4().hex[:8]}"
233 n = 100
234
235 for i in range(n):
236 content = f"# perf {i} {uuid.uuid4().hex}"
237 mid = compute_mist_id(content.encode())
238 repo = MusehubRepo(
239 name=f"p_{mid}", owner="perfuser", slug=f"pm_{mid}",
240 visibility="public", owner_user_id="perfuser",
241 )
242 db_session.add(repo)
243 await db_session.flush()
244 await _svc_create(
245 db_session,
246 mist_id=mid,
247 filename=f"perf_{i}.py",
248 content=content,
249 owner="perfuser",
250 repo_id=str(repo.repo_id),
251 artifact_type=unique_type,
252 )
253 await db_session.commit()
254
255 start = time.monotonic()
256 r = await client.get(
257 "/api/mists/explore",
258 params={"artifact_type": unique_type, "limit": 20},
259 )
260 elapsed = time.monotonic() - start
261
262 assert r.status_code == 200
263 assert elapsed < 2.0, f"Explore took {elapsed:.3f}s — expected < 2s"
264
265
266 # ═══════════════════════════════════════════════════════════════════════════════
267 # Fork chains
268 # ═══════════════════════════════════════════════════════════════════════════════
269
270 class TestForkChains:
271 """Full-depth fork chain built via HTTP; structural invariants verified."""
272
273 @pytest.mark.anyio
274 async def test_fork_chain_depth_and_parent_ids(
275 self, client: AsyncClient, auth_headers: dict
276 ) -> None:
277 """Each fork has the correct fork_parent_id and fork_depth."""
278 root = await _create(client, auth_headers)
279 root_id = root["mistId"]
280
281 chain = [root_id]
282 for depth in range(1, 6):
283 r = await client.post(
284 f"/api/mists/{chain[-1]}/fork", headers=auth_headers
285 )
286 assert r.status_code == 201, (
287 f"Fork at depth {depth} failed: {r.status_code} {r.text}"
288 )
289 fork_id = r.json()["mistId"]
290 chain.append(fork_id)
291
292 # Verify each fork's metadata.
293 for depth in range(1, 6):
294 r = await client.get(f"/api/mists/{chain[depth]}")
295 assert r.status_code == 200
296 body = r.json()
297 assert body["forkParentId"] == chain[depth - 1], (
298 f"Depth {depth}: forkParentId={body['forkParentId']!r}, "
299 f"expected {chain[depth - 1]!r}"
300 )
301 assert body["forkDepth"] == depth, (
302 f"Depth {depth}: forkDepth={body['forkDepth']!r}, expected {depth}"
303 )
304
305 @pytest.mark.anyio
306 async def test_fork_inherits_content_and_filename(
307 self, client: AsyncClient, auth_headers: dict
308 ) -> None:
309 root = await _create(
310 client, auth_headers,
311 content="def root_fn(): return 'root'\n" + uuid.uuid4().hex,
312 filename="root.py",
313 )
314 r = await client.post(
315 f"/api/mists/{root['mistId']}/fork", headers=auth_headers
316 )
317 assert r.status_code == 201
318 fork_id = r.json()["mistId"]
319
320 r2 = await client.get(f"/api/mists/{fork_id}")
321 assert r2.status_code == 200
322 body = r2.json()
323 assert body["content"] == root["content"]
324 assert body["filename"] == root["filename"]
325
326 @pytest.mark.anyio
327 async def test_fork_count_increments_on_parent(
328 self, client: AsyncClient, auth_headers: dict
329 ) -> None:
330 root = await _create(client, auth_headers)
331 initial_forks = root.get("forkCount", 0)
332
333 for _ in range(3):
334 r = await client.post(
335 f"/api/mists/{root['mistId']}/fork", headers=auth_headers
336 )
337 assert r.status_code == 201
338
339 r = await client.get(f"/api/mists/{root['mistId']}")
340 assert r.status_code == 200
341 assert r.json()["forkCount"] == initial_forks + 3
342
343 @pytest.mark.anyio
344 async def test_fork_chain_completes_under_5s(
345 self, client: AsyncClient, auth_headers: dict
346 ) -> None:
347 """Building a full 5-fork chain must complete in under 5 seconds."""
348 root = await _create(client, auth_headers)
349 current_id = root["mistId"]
350
351 start = time.monotonic()
352 for _ in range(5):
353 r = await client.post(
354 f"/api/mists/{current_id}/fork", headers=auth_headers
355 )
356 assert r.status_code == 201
357 current_id = r.json()["mistId"]
358 elapsed = time.monotonic() - start
359
360 assert elapsed < 5.0, f"Fork chain took {elapsed:.3f}s — expected < 5s"
File History 1 commit
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ 156 days ago