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