"""Tests for checklist section 6.2 — API performance. Covers: - Pagination infrastructure exists (PaginationParams, build_cursor_link_header) - Commit log endpoint is DB-level paginated (cursor-based keyset, not unbounded SELECT) - Symbol list endpoint is paginated (page/per_page, bounded result set) - Blame endpoint has a hard limit - StorageBackend.stream() exists on BlobBackend - Raw file download (ui_tree) uses StreamingResponse, not Response(content=...) - Symbol intel is pre-computed at push time (fire-and-forget indexer triggered) - Intel served from intel_full_json column, not computed per-request """ from __future__ import annotations import inspect import pathlib import tempfile from muse.core.types import blob_id, fake_id import pytest _REPO_ROOT = pathlib.Path(__file__).parent.parent # --------------------------------------------------------------------------- # Pagination infrastructure # --------------------------------------------------------------------------- def test_pagination_params_exist() -> None: """PaginationParams provides cursor and limit for cursor-based pagination.""" from musehub.api.routes.musehub.pagination import PaginationParams p = PaginationParams(cursor="abc", limit=25) assert p.cursor == "abc" assert p.limit == 25 def test_pagination_params_defaults() -> None: """PaginationParams accepts explicit cursor=None and limit=20.""" from musehub.api.routes.musehub.pagination import PaginationParams p = PaginationParams(cursor=None, limit=20) assert p.cursor is None assert p.limit == 20 def test_build_cursor_link_header_emits_rel_next() -> None: """build_cursor_link_header emits a rel='next' RFC 8288 Link header.""" from starlette.requests import Request as StarletteRequest from musehub.api.routes.musehub.pagination import build_cursor_link_header scope = { "type": "http", "method": "GET", "path": "/repos/alice/midi/commits", "query_string": b"limit=20", "headers": [], } req = StarletteRequest(scope) header = build_cursor_link_header(req, next_cursor="tok123", limit=20) assert 'rel="next"' in header assert "cursor=tok123" in header assert "limit=20" in header def test_limit_is_bounded() -> None: """PaginationParams __init__ must declare limit with le=200 via Query.""" import inspect from musehub.api.routes.musehub.pagination import PaginationParams src = inspect.getsource(PaginationParams.__init__) assert "le=200" in src or "le = 200" in src, ( "PaginationParams.limit must declare le=200 in its Query() to bound result sets." ) # --------------------------------------------------------------------------- # Commit log is DB-level paginated (not SELECT *) # --------------------------------------------------------------------------- def test_ui_commits_uses_db_level_limit() -> None: """ui_commits must call .limit() for DB-level pagination.""" src = (_REPO_ROOT / "musehub" / "api" / "routes" / "musehub" / "ui_commits.py").read_text() assert ".limit(" in src, "ui_commits must use .limit() for DB-level pagination" # --------------------------------------------------------------------------- # Symbol list pagination # --------------------------------------------------------------------------- def test_ui_symbols_applies_pagination_window() -> None: """ui_symbols must apply a page/per_page window before returning results.""" src = (_REPO_ROOT / "musehub" / "api" / "routes" / "musehub" / "ui_symbols.py").read_text() assert "per_page" in src, "ui_symbols must accept a per_page parameter" assert "offset" in src or "page_symbols" in src, ( "ui_symbols must slice results to the requested page window" ) # --------------------------------------------------------------------------- # Blame limit # --------------------------------------------------------------------------- def test_blame_has_hard_limit() -> None: """blame endpoint must have a .limit() call to prevent unbounded result sets.""" src = (_REPO_ROOT / "musehub" / "api" / "routes" / "musehub" / "blame.py").read_text() assert ".limit(" in src, "blame.py must call .limit() on its DB query" # --------------------------------------------------------------------------- # StorageBackend.stream() — protocol and implementations # --------------------------------------------------------------------------- def test_storage_backend_protocol_has_stream() -> None: """StorageBackend Protocol must declare a stream() method.""" from musehub.storage.backends import StorageBackend assert hasattr(StorageBackend, "stream"), ( "StorageBackend Protocol must declare stream() for chunked downloads" ) def test_blob_backend_has_stream() -> None: from musehub.storage.backends import BlobBackend assert hasattr(BlobBackend, "stream"), "BlobBackend must implement stream()" # --------------------------------------------------------------------------- # Raw file download uses StreamingResponse, not full-buffer Response # --------------------------------------------------------------------------- def test_raw_download_uses_streaming_response() -> None: """ui_tree raw_file_semantic must use StreamingResponse for chunked download. The old implementation used Response(content=data) which loaded the full file into RAM. The fix uses StreamingResponse with backend.stream(). """ src = (_REPO_ROOT / "musehub" / "api" / "routes" / "musehub" / "ui_tree.py").read_text() assert "StreamingResponse" in src, ( "ui_tree.py must use StreamingResponse for raw file download, " "not Response(content=data) which buffers the full file in RAM." ) assert "backend.stream(" in src or "storage.stream(" in src, ( "ui_tree.py raw download must call backend.stream() for chunked iteration." ) # The old pattern should be gone assert "Response(content=data)" not in src, ( "Response(content=data) must be removed — it buffers the entire file in RAM." ) # --------------------------------------------------------------------------- # Symbol intel pre-computed at push time # --------------------------------------------------------------------------- def test_wire_push_triggers_intel_indexer() -> None: """wire.py push route must enqueue intel jobs after a successful push.""" src = (_REPO_ROOT / "musehub" / "api" / "routes" / "wire.py").read_text() assert "enqueue_push_intel" in src, ( "wire.py must call enqueue_push_intel after push to pre-compute intelligence." ) def test_symbol_intel_served_from_precomputed_column() -> None: """ui_symbols must read intel from intel_full_json column, not recompute it.""" src = (_REPO_ROOT / "musehub" / "api" / "routes" / "musehub" / "ui_symbols.py").read_text() assert "load_intel_snapshot" in src, ( "ui_symbols must call load_intel_snapshot() to read pre-computed intel." ) assert "intel_full_json" in src or "load_intel_snapshot" in src, ( "Symbol intel must be loaded from the pre-computed DB column, not recomputed." ) # Must NOT call the compute functions directly assert "compute_intel(" not in src, ( "ui_symbols must not call compute_intel() — intel must be pre-computed at push." )