Files
teng-lin--notebooklm-py/tests/unit/mcp/conftest.py
T
wehub-resource-sync 09e9f3545f
Test / Code Quality (push) Has been cancelled
Test / Test (macos-latest, Python 3.10) (push) Has been cancelled
Test / Test (macos-latest, Python 3.11) (push) Has been cancelled
Test / Test (macos-latest, Python 3.12) (push) Has been cancelled
Test / Test (macos-latest, Python 3.13) (push) Has been cancelled
Test / Test (macos-latest, Python 3.14) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.10) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.11) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.12) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.13) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.14) (push) Has been cancelled
Test / Test (windows-latest, Python 3.10) (push) Has been cancelled
Test / Test (windows-latest, Python 3.11) (push) Has been cancelled
Test / Test (windows-latest, Python 3.12) (push) Has been cancelled
Test / Test (windows-latest, Python 3.13) (push) Has been cancelled
Test / Test (windows-latest, Python 3.14) (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled
dependency-audit / pip-audit (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:30:13 +08:00

125 lines
4.4 KiB
Python

"""Fixtures for MCP tool/server unit tests.
The MCP server is exercised through FastMCP's in-memory :class:`fastmcp.Client`
against a server whose lifespan yields a mocked ``NotebookLMClient`` (injected
via the ``client_factory`` seam). Tests configure the mock's namespace methods
(``mock_client.notebooks.list = AsyncMock(return_value=...)``) and assert on the
serialized ``structured_content`` plus that the right API method was called.
Return values can be plain local ``@dataclass`` fakes — ``to_jsonable`` converts
any dataclass — so tests need not construct real core types.
"""
from __future__ import annotations
import contextlib
import importlib.util
from collections.abc import AsyncIterator, Callable
from typing import Any
from unittest.mock import AsyncMock, MagicMock
import pytest
# The canonical contributor install (`uv sync --frozen --extra browser --extra dev
# --extra markdown`) omits the `mcp` extra, so `fastmcp` may be absent. A bare
# top-level ``pytest.importorskip`` in a *conftest* raises during conftest import,
# which pytest reports as a collection ERROR (not a clean skip) — so instead we
# tell pytest to ignore this whole directory when fastmcp is missing. The default
# `uv run pytest` then stays green without the extra; with the extra these tests
# RUN. Each test/guardrail module that imports fastmcp also carries its own
# ``pytest.importorskip("fastmcp")`` as a belt-and-suspenders guard.
def _fastmcp_available() -> bool:
"""Whether the optional ``mcp`` extra (fastmcp) is importable.
``find_spec`` returns ``None`` for an absent leaf but can *raise*
``ModuleNotFoundError`` if an ancestor is missing — treat either as absent.
"""
try:
return importlib.util.find_spec("fastmcp") is not None
except ModuleNotFoundError:
return False
collect_ignore_glob: list[str] = []
if not _fastmcp_available():
collect_ignore_glob = ["*"]
else:
from fastmcp import Client, FastMCP
from notebooklm.mcp.server import create_server
# Public client namespaces the tools reach through. Each is a MagicMock whose
# async methods tests override with AsyncMock.
_NAMESPACES = (
"notebooks",
"sources",
"chat",
"artifacts",
"research",
"notes",
"sharing",
"labels",
"settings",
"mind_maps",
)
@pytest.fixture
def mock_client() -> MagicMock:
"""A ``MagicMock`` standing in for ``NotebookLMClient`` with namespace attrs."""
client = MagicMock()
for namespace in _NAMESPACES:
setattr(client, namespace, MagicMock())
# `_app.download.execute_download` probes `client.artifacts._list_for_download`
# (the #1488 raw-rows fast path). A bare MagicMock auto-vivifies it as a
# truthy, non-awaitable attr; pin it to None so download tests exercise the
# public `.list` fallback they mock (the fast path is covered by the _app
# download tests, which use the real client).
client.artifacts._list_for_download = None
# Identity accessors used by ``server_info(include_account=True)`` — both are
# top-level client methods (not namespace attrs), so pin them explicitly:
# ``get_account_email`` is awaited, ``get_account_authuser`` is sync.
client.get_account_email = AsyncMock(return_value=None)
client.get_account_authuser = MagicMock(return_value=0)
return client
def _server_for(mock_client: MagicMock) -> FastMCP:
@contextlib.asynccontextmanager
async def factory() -> AsyncIterator[MagicMock]:
yield mock_client
return create_server(client_factory=factory)
@pytest.fixture
def server_factory(mock_client: MagicMock) -> Callable[[], FastMCP]:
"""Return a zero-arg builder for a server bound to ``mock_client``."""
return lambda: _server_for(mock_client)
@pytest.fixture
def mcp_call(mock_client: MagicMock) -> Callable[..., Any]:
"""Return ``async (tool_name, args=None) -> ToolResult`` against the mock."""
async def _call(tool_name: str, args: dict[str, Any] | None = None) -> Any:
async with Client(_server_for(mock_client)) as client:
return await client.call_tool(tool_name, args or {})
return _call
@pytest.fixture
def mcp_list_tools(mock_client: MagicMock) -> Callable[[], Any]:
"""Return ``async () -> list[Tool]`` (the registered tool manifest)."""
async def _list() -> Any:
async with Client(_server_for(mock_client)) as client:
return await client.list_tools()
return _list
__all__ = ["AsyncMock"] # re-exported for convenience in tool tests