gabriel / musehub public
test_mist_phase8_smoke.py python
329 lines 11.8 KB
Raw
sha256:bb2baaabdd19320bde50cb69d447fd1c1e571467df729be1a23b5e93064b046a feat(intel): standardize headers, gauge icon, velocity card… Sonnet 4.6 minor ⚠ breaking 143 days ago
1 """Phase 8 TDD: End-to-end Mist domain smoke test.
2
3 Exercises the full path for a mist-domain repo in one integration test:
4
5 seed repo + artifacts
6 → job_types_for_push dispatches intel.mist
7 → MistProvider.compute runs build_mist_anchor_index
8 → symbol anchors persisted to musehub_symbol_history_entries + musehub_symbol_intel
9 → persist_intel_results writes mist.anchor_index to musehub_intel_results
10 → profile activity canvas includes a "mist" domain grid with total >= 1
11 → GET /api/mists/explore returns 200
12 → GET /api/{owner}/mists returns 200
13 → GET /muse/mists returns 200
14 → GET /api/openapi.json lists /api/mists paths
15
16 No mocks — all assertions run against the real PostgreSQL test DB and the
17 live FastAPI app instance (same fixtures as phases 1–7).
18 """
19 from __future__ import annotations
20
21 import uuid
22 from datetime import datetime, timezone
23
24 import msgpack
25 import pytest
26 from httpx import AsyncClient
27 from muse.core.types import blob_id
28 from sqlalchemy import func, select
29 from sqlalchemy.ext.asyncio import AsyncSession
30
31 from musehub.core.genesis import compute_identity_id, compute_repo_id
32 from musehub.db import musehub_models as db
33
34
35 # ---------------------------------------------------------------------------
36 # Seed helper (inline — no cross-test import)
37 # ---------------------------------------------------------------------------
38
39 def _now() -> datetime:
40 return datetime.now(tz=timezone.utc)
41
42
43 def _oid(content: bytes) -> str:
44 return blob_id(content)
45
46
47 def _manifest_blob(manifest: dict[str, str]) -> bytes:
48 return msgpack.packb(manifest, use_bin_type=True)
49
50
51 def _commit_id() -> str:
52 return blob_id(uuid.uuid4().bytes)
53
54
55 def _snap_id(manifest: dict[str, str]) -> str:
56 return blob_id(msgpack.packb(sorted(manifest.items()), use_bin_type=True))
57
58
59 async def _seed_mist_repo(
60 session: AsyncSession,
61 owner: str,
62 artifacts: dict[str, bytes],
63 ) -> tuple[db.MusehubRepo, db.MusehubCommit]:
64 """Create a mist-domain repo with a commit pointing at a snapshot."""
65 owner_id = compute_identity_id(owner.encode())
66 slug = f"smoke-{uuid.uuid4().hex[:8]}"
67 created_at = _now()
68 repo_id = compute_repo_id(owner_id, slug, "mist", created_at.isoformat())
69
70 repo = db.MusehubRepo(
71 repo_id=repo_id,
72 name=slug,
73 owner=owner,
74 slug=slug,
75 visibility="public",
76 owner_user_id=owner_id,
77 domain_id="mist",
78 description="smoke-test mist repo",
79 tags=[],
80 created_at=created_at,
81 )
82 session.add(repo)
83 await session.flush()
84
85 manifest: dict[str, str] = {}
86 for filename, raw in artifacts.items():
87 oid = _oid(raw)
88 manifest[filename] = oid
89 if await session.get(db.MusehubObject, oid) is None:
90 session.add(db.MusehubObject(
91 object_id=oid,
92 path=filename,
93 size_bytes=len(raw),
94 disk_path="",
95 content_cache=raw,
96 ))
97 await session.flush()
98
99 snap_id = _snap_id(manifest)
100 if await session.get(db.MusehubSnapshot, snap_id) is None:
101 session.add(db.MusehubSnapshot(
102 snapshot_id=snap_id,
103 repo_id=repo_id,
104 entry_count=len(manifest),
105 manifest_blob=_manifest_blob(manifest),
106 ))
107 await session.flush()
108
109 commit = db.MusehubCommit(
110 commit_id=_commit_id(),
111 repo_id=repo_id,
112 message="smoke: initial mist",
113 author=owner,
114 branch="main",
115 parent_ids=[],
116 snapshot_id=snap_id,
117 timestamp=_now(),
118 )
119 session.add(commit)
120 await session.flush()
121 return repo, commit
122
123
124 # ---------------------------------------------------------------------------
125 # Fixtures
126 # ---------------------------------------------------------------------------
127
128 _ARTIFACTS: dict[str, bytes] = {
129 "utils.py": b"def helper_one(): pass\ndef helper_two(): pass\n",
130 "schema.json": b'{"type": "object", "properties": {"id": {"type": "string"}}}',
131 "README.md": b"# Smoke test mist\nContent-addressed artifact share.\n",
132 }
133
134
135 # ---------------------------------------------------------------------------
136 # Phase 8 — smoke test
137 # ---------------------------------------------------------------------------
138
139 class TestMistDomainEndToEnd:
140 @pytest.mark.asyncio
141 async def test_intel_mist_dispatched_for_mist_domain(self) -> None:
142 """job_types_for_push('mist') must include 'intel.mist'."""
143 from musehub.services.musehub_intel_providers import job_types_for_push
144 types = job_types_for_push("mist")
145 assert "intel.mist" in types, (
146 f"'intel.mist' must be dispatched for mist repos; got {types}"
147 )
148
149 @pytest.mark.asyncio
150 async def test_anchors_persisted_after_indexing(
151 self, db_session: AsyncSession
152 ) -> None:
153 """After build_mist_anchor_index runs, symbol history entries must exist."""
154 from musehub.services.musehub_mist_indexer import build_mist_anchor_index
155 from sqlalchemy import select
156
157 owner = f"smoke_{uuid.uuid4().hex[:8]}"
158 repo, commit = await _seed_mist_repo(db_session, owner, _ARTIFACTS)
159
160 results = await build_mist_anchor_index(
161 db_session, repo.repo_id, commit.commit_id
162 )
163
164 history_count = (await db_session.execute(
165 select(func.count()).where(
166 db.MusehubSymbolHistoryEntry.repo_id == repo.repo_id
167 )
168 )).scalar_one()
169 assert history_count >= 1, (
170 f"Expected at least 1 symbol history entry after indexing; got {history_count}"
171 )
172
173 @pytest.mark.asyncio
174 async def test_intel_results_written_by_mist_provider(
175 self, db_session: AsyncSession
176 ) -> None:
177 """MistProvider.compute + persist_intel_results must write mist.anchor_index."""
178 from musehub.services.musehub_intel_providers import MistProvider, persist_intel_results
179
180 owner = f"smoke_{uuid.uuid4().hex[:8]}"
181 repo, commit = await _seed_mist_repo(db_session, owner, _ARTIFACTS)
182
183 provider = MistProvider()
184 results = await provider.compute(
185 db_session, repo.repo_id, commit.commit_id, {}
186 )
187
188 # Must return at least the anchor_index result.
189 result_types = [r[0] for r in results]
190 assert "mist.anchor_index" in result_types, (
191 f"MistProvider.compute must return 'mist.anchor_index'; got {result_types}"
192 )
193
194 await persist_intel_results(
195 db_session, repo.repo_id, commit.commit_id, results
196 )
197 await db_session.flush()
198
199 row = (await db_session.execute(
200 select(db.MusehubIntelResult).where(
201 db.MusehubIntelResult.repo_id == repo.repo_id,
202 db.MusehubIntelResult.intel_type == "mist.anchor_index",
203 )
204 )).scalar_one_or_none()
205 assert row is not None, (
206 "persist_intel_results must write a 'mist.anchor_index' row to musehub_intel_results"
207 )
208
209 @pytest.mark.asyncio
210 async def test_symbol_intel_rows_written(
211 self, db_session: AsyncSession
212 ) -> None:
213 """Symbol intel rows must be upserted for each anchor extracted."""
214 from musehub.services.musehub_mist_indexer import build_mist_anchor_index
215
216 owner = f"smoke_{uuid.uuid4().hex[:8]}"
217 repo, commit = await _seed_mist_repo(db_session, owner, _ARTIFACTS)
218
219 await build_mist_anchor_index(db_session, repo.repo_id, commit.commit_id)
220
221 intel_count = (await db_session.execute(
222 select(func.count()).where(
223 db.MusehubSymbolIntel.repo_id == repo.repo_id
224 )
225 )).scalar_one()
226 assert intel_count >= 1, (
227 f"Expected at least 1 symbol intel row after indexing; got {intel_count}"
228 )
229
230 @pytest.mark.asyncio
231 async def test_profile_canvas_has_mist_grid(
232 self, db_session: AsyncSession
233 ) -> None:
234 """After seeding a mist repo with commits, profile canvas includes 'mist' domain."""
235 from musehub.services.musehub_profile import build_activity_canvas
236
237 owner = f"smoke_{uuid.uuid4().hex[:8]}"
238 await _seed_mist_repo(db_session, owner, _ARTIFACTS)
239
240 domains = await build_activity_canvas(db_session, owner)
241 domain_names = [d.domain for d in domains]
242 assert "mist" in domain_names, (
243 f"Profile canvas must include 'mist' domain; got {domain_names}"
244 )
245
246 mist = next(d for d in domains if d.domain == "mist")
247 assert mist.total >= 0 # zero is fine for snapshot-only push; non-crash matters
248
249 @pytest.mark.asyncio
250 async def test_push_validator_rejects_path_traversal(self) -> None:
251 """validate_mist_manifest must reject path traversal filenames."""
252 from musehub.services.musehub_mist_push_validator import validate_mist_manifest
253 result = validate_mist_manifest({"../evil.py": "sha256:abc"})
254 assert not result.valid
255 assert len(result.errors) >= 1
256
257 @pytest.mark.asyncio
258 async def test_explore_endpoint_returns_200(self, client: AsyncClient) -> None:
259 """GET /api/mists/explore must return 200."""
260 r = await client.get("/api/mists/explore")
261 assert r.status_code == 200, (
262 f"GET /api/mists/explore returned {r.status_code}"
263 )
264
265 @pytest.mark.asyncio
266 async def test_owner_mists_endpoint_returns_200(
267 self, client: AsyncClient
268 ) -> None:
269 """GET /api/{owner}/mists must return 200 (empty list for unknown owner is fine)."""
270 r = await client.get("/api/gabriel/mists")
271 assert r.status_code == 200, (
272 f"GET /api/gabriel/mists returned {r.status_code}"
273 )
274
275 @pytest.mark.asyncio
276 async def test_docs_mists_page_returns_200(self, client: AsyncClient) -> None:
277 """GET /muse/mists must return 200 with HTML content."""
278 r = await client.get("/muse/mists")
279 assert r.status_code == 200
280 assert "text/html" in r.headers.get("content-type", "")
281
282 @pytest.mark.asyncio
283 async def test_openapi_schema_lists_mists_paths(
284 self, client: AsyncClient
285 ) -> None:
286 """GET /api/openapi.json must list /api/mists paths."""
287 r = await client.get("/api/openapi.json")
288 assert r.status_code == 200
289 paths = r.json().get("paths", {})
290 mist_paths = [p for p in paths if "/mists" in p]
291 assert len(mist_paths) > 0, (
292 f"No /mists paths in OpenAPI schema; sample: {list(paths.keys())[:20]}"
293 )
294
295 @pytest.mark.asyncio
296 async def test_full_pipeline_anchor_count_positive(
297 self, db_session: AsyncSession
298 ) -> None:
299 """Full pipeline: index → intel → confirm anchor_count > 0 in result data."""
300 import json
301 from musehub.services.musehub_intel_providers import MistProvider, persist_intel_results
302
303 owner = f"smoke_{uuid.uuid4().hex[:8]}"
304 # utils.py has two functions → at least 2 anchors
305 repo, commit = await _seed_mist_repo(
306 db_session, owner,
307 {"utils.py": b"def alpha(): pass\ndef beta(): pass\n"}
308 )
309
310 provider = MistProvider()
311 results = await provider.compute(
312 db_session, repo.repo_id, commit.commit_id, {}
313 )
314 await persist_intel_results(
315 db_session, repo.repo_id, commit.commit_id, results
316 )
317 await db_session.flush()
318
319 row = (await db_session.execute(
320 select(db.MusehubIntelResult).where(
321 db.MusehubIntelResult.repo_id == repo.repo_id,
322 db.MusehubIntelResult.intel_type == "mist.anchor_index",
323 )
324 )).scalar_one_or_none()
325 assert row is not None
326 data = json.loads(row.data_json)
327 assert data.get("anchor_count", 0) >= 2, (
328 f"Expected anchor_count >= 2 for utils.py with 2 functions; got {data}"
329 )
File History 1 commit
sha256:bb2baaabdd19320bde50cb69d447fd1c1e571467df729be1a23b5e93064b046a feat(intel): standardize headers, gauge icon, velocity card… Sonnet 4.6 minor 143 days ago