"""Section 14 — MCP Read Tools: 7-layer test suite. Covers ``musehub/services/musehub_mcp_executor.py`` executor functions and their wiring through the MCP dispatcher (``musehub/mcp/dispatcher.py``). Read tools under test: execute_browse_repo, execute_list_branches, execute_list_commits, execute_read_file, execute_get_analysis, execute_search, execute_get_commit, execute_compare, execute_whoami, execute_search_repos and the helpers: _mime_for_path, _check_db_available, MusehubToolResult Seven layers: Layer 1 Unit: - _mime_for_path: known MIDI extension, .webp custom, unknown → octet-stream, .py - _check_db_available: factory=None → db_unavailable result - MusehubToolResult: ok=True / ok=False shape invariants - execute_get_analysis: invalid dimension returns immediately (no DB touch) - execute_search: invalid mode returns immediately - execute_whoami: user_id=None → authenticated=False immediately Layer 2 Integration: - execute_browse_repo: existing repo → ok=True with repo/branches/commits keys - execute_browse_repo: unknown repo_id → ok=False, error_code=not_found - execute_list_branches: existing repo → branch list returned - execute_list_branches: unknown repo → not_found - execute_list_commits: commits returned newest-first, branch filter, limit clamp - execute_read_file: known object → ok=True, mime resolved - execute_read_file: unknown object_id → not_found - execute_read_file: unknown repo → not_found - execute_get_commit: known commit → ok=True - execute_get_commit: unknown commit → not_found - execute_get_analysis: overview / commits / objects dimensions - execute_search: path mode / commit mode case-insensitive - execute_compare: ok=True with diff shape - execute_whoami: with user_id → authenticated=True Layer 3 E2E (HTTP tools/call): - musehub_list_branches: isError=False, content is valid JSON - musehub_list_branches unknown repo → isError=True - musehub_list_commits with limit - musehub_search invalid mode → isError=True - musehub_get_commit not found → isError=True - musehub_whoami anonymous → authenticated=False - musehub_get_analysis invalid dimension → isError=True - owner+slug transparent resolution Layer 4 Stress: - 50 commits → list_commits returns all 50 - 30 objects, search returns matching subset Layer 5 Data Integrity: - browse_repo response shape: all required top-level keys - list_commits newest-first ordering - read_file mime_type resolved per extension - search path mode case-insensitive - search commit mode case-insensitive - get_analysis overview has all required fields Layer 6 Security: - execute_search_repos only returns public repos - execute_whoami with None → authenticated=False (no data leakage) - write tool via HTTP without auth → isError=True Layer 7 Performance: - 1000× _mime_for_path under 10 ms - execute_browse_repo on populated repo under 200 ms - execute_get_analysis overview under 200 ms """ from __future__ import annotations import json import time import uuid from datetime import datetime, timezone import pytest import pytest_asyncio from httpx import AsyncClient, ASGITransport from sqlalchemy.ext.asyncio import AsyncSession from musehub.db import musehub_models as db from musehub.main import app from musehub.muse_contracts.json_types import JSONObject from musehub.services.musehub_mcp_executor import ( MusehubToolResult, _check_db_available, _mime_for_path, execute_browse_repo, execute_compare, execute_get_analysis, execute_get_commit, execute_list_branches, execute_list_commits, execute_read_file, execute_search, execute_whoami, ) # ── Fixtures ────────────────────────────────────────────────────────────────── @pytest.fixture def anyio_backend() -> str: return "asyncio" @pytest_asyncio.fixture async def http_client(db_session: AsyncSession) -> AsyncClient: async with AsyncClient( transport=ASGITransport(app=app), base_url="http://localhost", ) as c: yield c # ── Helpers ─────────────────────────────────────────────────────────────────── def _uid() -> str: return str(uuid.uuid4()) async def _repo( session: AsyncSession, slug: str, visibility: str = "public", owner: str = "alice", ) -> db.MusehubRepo: repo = db.MusehubRepo( name=slug, owner=owner, slug=slug, visibility=visibility, owner_user_id="uid-alice", ) session.add(repo) await session.flush() await session.refresh(repo) return repo async def _commit( session: AsyncSession, repo_id: str, branch: str = "main", message: str = "add track", author: str = "alice", ts: datetime | None = None, ) -> db.MusehubCommit: c = db.MusehubCommit( commit_id=_uid()[:16], repo_id=repo_id, branch=branch, parent_ids=[], message=message, author=author, timestamp=ts or datetime.now(tz=timezone.utc), ) session.add(c) await session.flush() return c async def _branch( session: AsyncSession, repo_id: str, name: str, head_commit_id: str, ) -> db.MusehubBranch: b = db.MusehubBranch( repo_id=repo_id, name=name, head_commit_id=head_commit_id, ) session.add(b) await session.flush() return b async def _object( session: AsyncSession, repo_id: str, path: str, size_bytes: int = 1024, ) -> db.MusehubObject: obj = db.MusehubObject( object_id=f"sha256:{_uid()[:32]}", repo_id=repo_id, path=path, size_bytes=size_bytes, disk_path=f"/tmp/{_uid()}.bin", ) session.add(obj) await session.flush() return obj def _tools_call(name: str, arguments: JSONObject) -> JSONObject: return {"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": name, "arguments": arguments}} def _unwrap_tool_text(text: str) -> str: """Strip wrapper tags added by the dispatcher.""" text = text.strip() if text.startswith(""): text = text[len(""):].strip() if text.endswith(""): text = text[: -len("")].strip() return text async def _init_session(http_client: AsyncClient) -> str: """POST initialize and return the session_id.""" resp = await http_client.post( "/mcp", json={ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-11-25", "clientInfo": {"name": "test", "version": "1.0"}, "capabilities": {}, }, }, headers={"Content-Type": "application/json"}, ) return resp.headers["mcp-session-id"] # ── Layer 1 — Unit ──────────────────────────────────────────────────────────── class TestUnitMimeForPath: def test_midi_extension(self) -> None: mime = _mime_for_path("tracks/song.mid") assert mime == "audio/midi" def test_webp_custom_extension(self) -> None: assert _mime_for_path("image.webp") == "image/webp" def test_unknown_extension_returns_octet_stream(self) -> None: assert _mime_for_path("artifact.xyz123") == "application/octet-stream" def test_python_extension(self) -> None: assert "python" in _mime_for_path("script.py").lower() def test_no_extension_returns_octet_stream(self) -> None: assert _mime_for_path("noextension") == "application/octet-stream" def test_case_insensitive_extension(self) -> None: upper = _mime_for_path("TRACK.WEBP") lower = _mime_for_path("track.webp") assert upper == lower class TestUnitCheckDbAvailable: def test_factory_none_returns_error(self) -> None: from musehub.db import database original = database._async_session_factory try: setattr(database, '_async_session_factory', None) result = _check_db_available() assert result is not None assert result.ok is False assert result.error_code == "db_unavailable" assert result.error_message is not None finally: database._async_session_factory = original def test_factory_set_returns_none(self, db_session: AsyncSession) -> None: """With db_session fixture active, factory is set — check returns None.""" result = _check_db_available() assert result is None class TestUnitMusehubToolResult: def test_ok_true_shape(self) -> None: r = MusehubToolResult(ok=True, data={"repo_id": "abc"}) assert r.ok is True assert r.data == {"repo_id": "abc"} assert r.error_code is None assert r.error_message is None def test_ok_false_shape(self) -> None: r = MusehubToolResult( ok=False, error_code="not_found", error_message="Repo not found.", ) assert r.ok is False assert r.error_code == "not_found" assert "not found" in r.error_message.lower() assert r.data == {} class TestUnitValidationWithoutDB: @pytest.mark.anyio async def test_get_analysis_invalid_dimension(self, db_session: AsyncSession) -> None: result = await execute_get_analysis("any-repo-id", dimension="music") assert result.ok is False assert result.error_code == "invalid_args" assert "music" in (result.error_message or "") @pytest.mark.anyio async def test_search_invalid_mode(self, db_session: AsyncSession) -> None: result = await execute_search("any-repo-id", query="bass", mode="regex") assert result.ok is False assert result.error_code == "invalid_args" assert "regex" in (result.error_message or "") @pytest.mark.anyio async def test_whoami_anonymous(self) -> None: """execute_whoami with None returns authenticated=False without hitting DB.""" result = await execute_whoami(None) assert result.ok is True assert result.data["authenticated"] is False assert result.data["user_id"] is None # ── Layer 2 — Integration ───────────────────────────────────────────────────── class TestIntegrationBrowseRepo: @pytest.mark.anyio async def test_existing_repo_returns_ok(self, db_session: AsyncSession) -> None: r = await _repo(db_session, "browse-ok") c = await _commit(db_session, r.repo_id) await _branch(db_session, r.repo_id, "main", c.commit_id) await db_session.commit() result = await execute_browse_repo(r.repo_id) assert result.ok is True assert "repo" in result.data assert "branches" in result.data assert "recent_commits" in result.data assert result.data["branch_count"] == 1 @pytest.mark.anyio async def test_unknown_repo_returns_not_found(self, db_session: AsyncSession) -> None: result = await execute_browse_repo("nonexistent-repo-id") assert result.ok is False assert result.error_code == "repo_not_found" class TestIntegrationListBranches: @pytest.mark.anyio async def test_returns_branches(self, db_session: AsyncSession) -> None: r = await _repo(db_session, "lb-ok") c = await _commit(db_session, r.repo_id) await _branch(db_session, r.repo_id, "main", c.commit_id) await _branch(db_session, r.repo_id, "dev", c.commit_id) await db_session.commit() result = await execute_list_branches(r.repo_id) assert result.ok is True assert result.data["branch_count"] == 2 names = [b["name"] for b in result.data["branches"]] assert "main" in names assert "dev" in names @pytest.mark.anyio async def test_unknown_repo_returns_not_found(self, db_session: AsyncSession) -> None: result = await execute_list_branches("ghost-repo") assert result.ok is False assert result.error_code == "repo_not_found" class TestIntegrationListCommits: @pytest.mark.anyio async def test_returns_commits(self, db_session: AsyncSession) -> None: r = await _repo(db_session, "lc-ok") for i in range(5): await _commit(db_session, r.repo_id, message=f"commit {i}") await db_session.commit() result = await execute_list_commits(r.repo_id, limit=10) assert result.ok is True assert result.data["returned"] == 5 @pytest.mark.anyio async def test_branch_filter(self, db_session: AsyncSession) -> None: r = await _repo(db_session, "lc-branch") await _commit(db_session, r.repo_id, branch="main", message="on main") await _commit(db_session, r.repo_id, branch="dev", message="on dev") await db_session.commit() result = await execute_list_commits(r.repo_id, branch="main", limit=10) assert result.ok is True commits = result.data["commits"] assert all(c["branch"] == "main" for c in commits) @pytest.mark.anyio async def test_limit_clamped_high(self, db_session: AsyncSession) -> None: """Limit values over 100 are clamped to 100.""" r = await _repo(db_session, "lc-clamp-hi") for _ in range(5): await _commit(db_session, r.repo_id) await db_session.commit() # limit=200 should clamp to 100 but still return all 5 result = await execute_list_commits(r.repo_id, limit=200) assert result.ok is True assert result.data["returned"] == 5 @pytest.mark.anyio async def test_limit_clamped_low(self, db_session: AsyncSession) -> None: """Limit values below 1 are clamped to 1.""" r = await _repo(db_session, "lc-clamp-lo") for _ in range(5): await _commit(db_session, r.repo_id) await db_session.commit() result = await execute_list_commits(r.repo_id, limit=0) assert result.ok is True assert result.data["returned"] == 1 @pytest.mark.anyio async def test_unknown_repo(self, db_session: AsyncSession) -> None: result = await execute_list_commits("ghost-lc") assert result.ok is False assert result.error_code == "repo_not_found" class TestIntegrationReadFile: @pytest.mark.anyio async def test_known_object_returns_metadata(self, db_session: AsyncSession) -> None: r = await _repo(db_session, "rf-ok") obj = await _object(db_session, r.repo_id, "tracks/bass.mid", size_bytes=4096) await db_session.commit() result = await execute_read_file(r.repo_id, obj.object_id) assert result.ok is True assert result.data["object_id"] == obj.object_id assert result.data["path"] == "tracks/bass.mid" assert result.data["size_bytes"] == 4096 assert "midi" in result.data["mime_type"].lower() @pytest.mark.anyio async def test_unknown_object_returns_not_found(self, db_session: AsyncSession) -> None: r = await _repo(db_session, "rf-no-obj") await db_session.commit() result = await execute_read_file(r.repo_id, "sha256:deadbeef") assert result.ok is False assert result.error_code == "file_not_found" @pytest.mark.anyio async def test_unknown_repo_returns_not_found(self, db_session: AsyncSession) -> None: result = await execute_read_file("ghost-repo", "sha256:anything") assert result.ok is False assert result.error_code == "repo_not_found" class TestIntegrationGetCommit: @pytest.mark.anyio async def test_known_commit_returns_data(self, db_session: AsyncSession) -> None: r = await _repo(db_session, "gc-ok") c = await _commit(db_session, r.repo_id, message="feature: harmony") await db_session.commit() result = await execute_get_commit(r.repo_id, c.commit_id) assert result.ok is True assert result.data["commit_id"] == c.commit_id assert result.data["message"] == "feature: harmony" assert result.data["author"] == "alice" @pytest.mark.anyio async def test_unknown_commit_returns_not_found(self, db_session: AsyncSession) -> None: r = await _repo(db_session, "gc-miss") await db_session.commit() result = await execute_get_commit(r.repo_id, "nonexistent-commit-id") assert result.ok is False assert result.error_code == "commit_not_found" class TestIntegrationGetAnalysis: @pytest.mark.anyio async def test_overview_dimension(self, db_session: AsyncSession) -> None: r = await _repo(db_session, "ga-overview") c = await _commit(db_session, r.repo_id) await _branch(db_session, r.repo_id, "main", c.commit_id) await _object(db_session, r.repo_id, "track.mid") await db_session.commit() result = await execute_get_analysis(r.repo_id, dimension="overview") assert result.ok is True d = result.data assert d["dimension"] == "overview" assert d["branch_count"] == 1 assert d["commit_count"] >= 1 assert d["object_count"] == 1 @pytest.mark.anyio async def test_commits_dimension(self, db_session: AsyncSession) -> None: r = await _repo(db_session, "ga-commits") await _commit(db_session, r.repo_id, branch="main", author="alice") await _commit(db_session, r.repo_id, branch="dev", author="bob") await db_session.commit() result = await execute_get_analysis(r.repo_id, dimension="commits") assert result.ok is True d = result.data assert d["dimension"] == "commits" assert "by_branch" in d assert "by_author" in d assert d["by_author"].get("alice", 0) >= 1 assert d["by_author"].get("bob", 0) >= 1 @pytest.mark.anyio async def test_objects_dimension(self, db_session: AsyncSession) -> None: r = await _repo(db_session, "ga-objects") await _object(db_session, r.repo_id, "a.mid", size_bytes=100) await _object(db_session, r.repo_id, "b.mid", size_bytes=200) await db_session.commit() result = await execute_get_analysis(r.repo_id, dimension="objects") assert result.ok is True d = result.data assert d["dimension"] == "objects" assert d["total_objects"] == 2 assert d["total_size_bytes"] == 300 class TestIntegrationSearch: @pytest.mark.anyio async def test_path_mode_returns_matching_objects(self, db_session: AsyncSession) -> None: r = await _repo(db_session, "s-path") await _object(db_session, r.repo_id, "tracks/jazz_bass.mid") await _object(db_session, r.repo_id, "tracks/treble.mid") await db_session.commit() result = await execute_search(r.repo_id, "jazz", mode="path") assert result.ok is True assert result.data["result_count"] == 1 assert result.data["results"][0]["path"] == "tracks/jazz_bass.mid" @pytest.mark.anyio async def test_path_mode_case_insensitive(self, db_session: AsyncSession) -> None: r = await _repo(db_session, "s-ci-path") await _object(db_session, r.repo_id, "JAZZ_TRACK.mid") await db_session.commit() result = await execute_search(r.repo_id, "jazz", mode="path") assert result.ok is True assert result.data["result_count"] == 1 @pytest.mark.anyio async def test_commit_mode_returns_matching_commits(self, db_session: AsyncSession) -> None: r = await _repo(db_session, "s-commit") await _commit(db_session, r.repo_id, message="add bass groove") await _commit(db_session, r.repo_id, message="fix tempo sync") await db_session.commit() result = await execute_search(r.repo_id, "bass", mode="commit") assert result.ok is True assert result.data["result_count"] == 1 assert "bass" in result.data["results"][0]["message"].lower() @pytest.mark.anyio async def test_commit_mode_case_insensitive(self, db_session: AsyncSession) -> None: r = await _repo(db_session, "s-ci-commit") await _commit(db_session, r.repo_id, message="Add BASS line") await db_session.commit() result = await execute_search(r.repo_id, "bass", mode="commit") assert result.ok is True assert result.data["result_count"] == 1 class TestIntegrationCompare: @pytest.mark.anyio async def test_compare_returns_diff_shape(self, db_session: AsyncSession) -> None: r = await _repo(db_session, "compare-ok") ca = await _commit(db_session, r.repo_id, branch="main") cb = await _commit(db_session, r.repo_id, branch="dev") await db_session.commit() result = await execute_compare(r.repo_id, base_ref="main", head_ref="dev") assert result.ok is True assert result.data["base_ref"] == "main" assert result.data["head_ref"] == "dev" assert "base_commit_id" in result.data assert "head_commit_id" in result.data class TestIntegrationWhoami: @pytest.mark.anyio async def test_authenticated_user_returns_data(self, db_session: AsyncSession) -> None: result = await execute_whoami("uid-test-user") assert result.ok is True assert result.data["authenticated"] is True assert result.data["user_id"] == "uid-test-user" # ── Layer 3 — End-to-End ────────────────────────────────────────────────────── class TestE2EReadTools: @pytest.mark.anyio async def test_list_branches_returns_valid_json_content( self, http_client: AsyncClient, db_session: AsyncSession ) -> None: r = await _repo(db_session, "e2e-lb") c = await _commit(db_session, r.repo_id) await _branch(db_session, r.repo_id, "main", c.commit_id) await db_session.commit() resp = await http_client.post( "/mcp", json=_tools_call("musehub_list_branches", {"repo_id": r.repo_id}), headers={"Content-Type": "application/json"}, ) assert resp.status_code == 200 data = resp.json() assert data["result"]["isError"] is False text = _unwrap_tool_text(data["result"]["content"][0]["text"]) payload = json.loads(text) assert "branches" in payload @pytest.mark.anyio async def test_list_branches_unknown_repo_returns_iserror( self, http_client: AsyncClient, db_session: AsyncSession ) -> None: resp = await http_client.post( "/mcp", json=_tools_call("musehub_list_branches", {"repo_id": "ghost-e2e"}), headers={"Content-Type": "application/json"}, ) assert resp.status_code == 200 data = resp.json() assert data["result"]["isError"] is True error = json.loads(data["result"]["content"][0]["text"]) assert error["error_code"] == "repo_not_found" @pytest.mark.anyio async def test_list_commits_with_limit( self, http_client: AsyncClient, db_session: AsyncSession ) -> None: r = await _repo(db_session, "e2e-lc") for _ in range(10): await _commit(db_session, r.repo_id) await db_session.commit() resp = await http_client.post( "/mcp", json=_tools_call("musehub_list_commits", {"repo_id": r.repo_id, "limit": 5}), headers={"Content-Type": "application/json"}, ) assert resp.status_code == 200 result = resp.json()["result"] assert result["isError"] is False payload = json.loads(_unwrap_tool_text(result["content"][0]["text"])) assert payload["returned"] <= 5 @pytest.mark.anyio async def test_search_invalid_mode_returns_iserror( self, http_client: AsyncClient, db_session: AsyncSession ) -> None: r = await _repo(db_session, "e2e-s-mode") await db_session.commit() resp = await http_client.post( "/mcp", json=_tools_call("musehub_search", {"repo_id": r.repo_id, "query": "x", "mode": "invalid"}), headers={"Content-Type": "application/json"}, ) assert resp.status_code == 200 assert resp.json()["result"]["isError"] is True @pytest.mark.anyio async def test_get_commit_not_found_returns_iserror( self, http_client: AsyncClient, db_session: AsyncSession ) -> None: r = await _repo(db_session, "e2e-gc") await db_session.commit() resp = await http_client.post( "/mcp", json=_tools_call("musehub_get_commit", {"repo_id": r.repo_id, "commit_id": "ghost-commit"}), headers={"Content-Type": "application/json"}, ) assert resp.status_code == 200 assert resp.json()["result"]["isError"] is True @pytest.mark.anyio async def test_whoami_anonymous_returns_not_authenticated( self, http_client: AsyncClient, db_session: AsyncSession ) -> None: resp = await http_client.post( "/mcp", json=_tools_call("musehub_whoami", {}), headers={"Content-Type": "application/json"}, ) assert resp.status_code == 200 result = resp.json()["result"] assert result["isError"] is False payload = json.loads(_unwrap_tool_text(result["content"][0]["text"])) assert payload["authenticated"] is False @pytest.mark.anyio async def test_get_analysis_invalid_dimension_returns_iserror( self, http_client: AsyncClient, db_session: AsyncSession ) -> None: r = await _repo(db_session, "e2e-ga-bad") await db_session.commit() resp = await http_client.post( "/mcp", json=_tools_call("musehub_get_analysis", {"repo_id": r.repo_id, "dimension": "music"}), headers={"Content-Type": "application/json"}, ) assert resp.status_code == 200 assert resp.json()["result"]["isError"] is True @pytest.mark.anyio async def test_unknown_tool_returns_iserror( self, http_client: AsyncClient, db_session: AsyncSession ) -> None: resp = await http_client.post( "/mcp", json=_tools_call("musehub_no_such_tool", {}), headers={"Content-Type": "application/json"}, ) assert resp.status_code == 200 assert resp.json()["result"]["isError"] is True # ── Layer 4 — Stress ────────────────────────────────────────────────────────── class TestStressReadTools: @pytest.mark.anyio async def test_50_commits_all_returned(self, db_session: AsyncSession) -> None: r = await _repo(db_session, "stress-commits") for i in range(50): await _commit(db_session, r.repo_id, message=f"commit {i}") await db_session.commit() result = await execute_list_commits(r.repo_id, limit=100) assert result.ok is True assert result.data["returned"] == 50 @pytest.mark.anyio async def test_30_objects_search_returns_subset(self, db_session: AsyncSession) -> None: r = await _repo(db_session, "stress-search") # 20 matching objects + 10 non-matching for i in range(20): await _object(db_session, r.repo_id, f"jazz/track_{i}.mid") for i in range(10): await _object(db_session, r.repo_id, f"blues/track_{i}.mid") await db_session.commit() result = await execute_search(r.repo_id, "jazz", mode="path") assert result.ok is True assert result.data["result_count"] == 20 # ── Layer 5 — Data Integrity ────────────────────────────────────────────────── class TestDataIntegrityBrowseRepo: @pytest.mark.anyio async def test_response_has_all_required_keys(self, db_session: AsyncSession) -> None: r = await _repo(db_session, "di-browse") c = await _commit(db_session, r.repo_id) await _branch(db_session, r.repo_id, "main", c.commit_id) await db_session.commit() result = await execute_browse_repo(r.repo_id) assert result.ok is True for key in ("repo", "branches", "recent_commits", "total_commits", "branch_count"): assert key in result.data, f"Missing key: {key}" @pytest.mark.anyio async def test_repo_sub_dict_has_required_fields(self, db_session: AsyncSession) -> None: r = await _repo(db_session, "di-browse-repo") c = await _commit(db_session, r.repo_id) await _branch(db_session, r.repo_id, "main", c.commit_id) await db_session.commit() result = await execute_browse_repo(r.repo_id) repo_data = result.data["repo"] for field in ("repo_id", "name", "visibility", "owner_user_id", "created_at"): assert field in repo_data, f"Missing repo field: {field}" class TestDataIntegrityCommitOrdering: @pytest.mark.anyio async def test_commits_newest_first(self, db_session: AsyncSession) -> None: from datetime import timedelta r = await _repo(db_session, "di-order") base = datetime.now(tz=timezone.utc) for i in range(5): await _commit( db_session, r.repo_id, message=f"commit {i}", ts=base + timedelta(seconds=i), ) await db_session.commit() result = await execute_list_commits(r.repo_id, limit=10) commits = result.data["commits"] timestamps = [c["timestamp"] for c in commits] assert timestamps == sorted(timestamps, reverse=True) class TestDataIntegrityReadFileMime: @pytest.mark.anyio async def test_webp_mime_resolved(self, db_session: AsyncSession) -> None: r = await _repo(db_session, "di-mime-webp") obj = await _object(db_session, r.repo_id, "roll.webp") await db_session.commit() result = await execute_read_file(r.repo_id, obj.object_id) assert result.ok is True assert result.data["mime_type"] == "image/webp" @pytest.mark.anyio async def test_midi_mime_resolved(self, db_session: AsyncSession) -> None: r = await _repo(db_session, "di-mime-mid") obj = await _object(db_session, r.repo_id, "track.mid") await db_session.commit() result = await execute_read_file(r.repo_id, obj.object_id) assert result.ok is True assert "midi" in result.data["mime_type"].lower() class TestDataIntegrityGetAnalysisOverview: @pytest.mark.anyio async def test_overview_has_all_required_fields(self, db_session: AsyncSession) -> None: r = await _repo(db_session, "di-ga-overview") c = await _commit(db_session, r.repo_id) await _branch(db_session, r.repo_id, "main", c.commit_id) await db_session.commit() result = await execute_get_analysis(r.repo_id, dimension="overview") assert result.ok is True for field in ("repo_id", "dimension", "repo_name", "visibility", "branch_count", "commit_count", "object_count"): assert field in result.data, f"Missing field: {field}" # ── Layer 6 — Security ──────────────────────────────────────────────────────── class TestSecurityReadTools: @pytest.mark.anyio async def test_whoami_anonymous_returns_no_user_data(self) -> None: """Anonymous whoami must not leak user info.""" result = await execute_whoami(None) assert result.ok is True assert result.data["authenticated"] is False assert result.data["user_id"] is None # Must not contain any other fields that could leak data. assert "repo_count" not in result.data @pytest.mark.anyio async def test_search_repos_only_returns_public_repos( self, db_session: AsyncSession ) -> None: """execute_search_repos must never return private repos.""" await _repo(db_session, "sec-public", visibility="public") await _repo(db_session, "sec-private", visibility="private") await db_session.commit() from musehub.services.musehub_mcp_executor import execute_search_repos result = await execute_search_repos(query="sec", limit=50) assert result.ok is True names = [r["name"] for r in result.data["repos"]] assert "sec-private" not in names @pytest.mark.anyio async def test_write_tool_via_mcp_requires_auth( self, http_client: AsyncClient, db_session: AsyncSession ) -> None: """musehub_create_repo (write tool) called without auth returns 401.""" resp = await http_client.post( "/mcp", json=_tools_call("musehub_create_repo", {"name": "should-fail", "owner": "alice"}), headers={"Content-Type": "application/json"}, ) assert resp.status_code == 401 @pytest.mark.anyio async def test_read_file_unknown_repo_does_not_crash(self, db_session: AsyncSession) -> None: """Unknown repo must return not_found, never raise an exception.""" result = await execute_read_file("completely-made-up-id", "sha256:x") assert not result.ok assert result.error_code == "repo_not_found" # ── Layer 7 — Performance ───────────────────────────────────────────────────── class TestPerformanceMimeResolution: def test_1000_mime_resolutions_under_10ms(self) -> None: paths = [ "track.mid", "cover.webp", "audio.mp3", "script.py", "unknown.xyz", "noext", "deep/path/to/file.mid", ] start = time.perf_counter() for i in range(1000): _mime_for_path(paths[i % len(paths)]) elapsed_ms = (time.perf_counter() - start) * 1000 assert elapsed_ms < 10, f"1000× _mime_for_path took {elapsed_ms:.1f} ms" class TestPerformanceExecutors: @pytest.mark.anyio async def test_browse_repo_under_200ms(self, db_session: AsyncSession) -> None: r = await _repo(db_session, "perf-browse") for _ in range(10): c = await _commit(db_session, r.repo_id) await _branch(db_session, r.repo_id, "main", c.commit_id) for _ in range(5): await _object(db_session, r.repo_id, f"track_{_}.mid") await db_session.commit() start = time.perf_counter() result = await execute_browse_repo(r.repo_id) elapsed_ms = (time.perf_counter() - start) * 1000 assert result.ok is True assert elapsed_ms < 200, f"execute_browse_repo took {elapsed_ms:.1f} ms" @pytest.mark.anyio async def test_get_analysis_overview_under_200ms(self, db_session: AsyncSession) -> None: r = await _repo(db_session, "perf-analysis") for _ in range(20): c = await _commit(db_session, r.repo_id) await _branch(db_session, r.repo_id, "main", c.commit_id) await db_session.commit() start = time.perf_counter() result = await execute_get_analysis(r.repo_id, dimension="overview") elapsed_ms = (time.perf_counter() - start) * 1000 assert result.ok is True assert elapsed_ms < 200, f"execute_get_analysis(overview) took {elapsed_ms:.1f} ms"