"""Minimal tests for marketing_intelligence.agent.mcp_client.""" import asyncio import json from collections.abc import Coroutine from typing import Any from unittest.mock import AsyncMock, MagicMock, patch from marketing_intelligence.agent.mcp_client import MCPToolClient from marketing_intelligence.agent.mcp_client import _make_client as _real_make_client def _run(coro: Coroutine[Any, Any, Any]) -> Any: return asyncio.run(coro) def _make_client() -> tuple[MCPToolClient, MagicMock]: """Create MCPToolClient with a mocked inner fastmcp Client.""" mock_inner = MagicMock() mock_inner.__aenter__ = AsyncMock(return_value=mock_inner) mock_inner.__aexit__ = AsyncMock(return_value=False) with patch( "marketing_intelligence.agent.mcp_client._make_client", return_value=mock_inner, ): client = MCPToolClient() return client, mock_inner def _fake_tool( name: str, description: str = "", schema: dict | None = None ) -> MagicMock: t = MagicMock() t.name = name t.description = description t.inputSchema = schema or {} return t class TestListToolsAnthropic: def test_returns_anthropic_format(self) -> None: client, inner = _make_client() inner.list_tools = AsyncMock( return_value=[ _fake_tool("scrape_video", "Scrape a video", {"type": "object"}) ] ) result = _run(client.list_tools_anthropic()) assert len(result) == 1 assert result[0]["name"] == "scrape_video" assert result[0]["description"] == "Scrape a video" assert result[0]["input_schema"] == {"type": "object"} def test_filters_by_allowed_set(self) -> None: client, inner = _make_client() inner.list_tools = AsyncMock( return_value=[ _fake_tool("scrape_video"), _fake_tool("write_run_record"), ] ) result = _run(client.list_tools_anthropic(allowed={"scrape_video"})) assert len(result) == 1 assert result[0]["name"] == "scrape_video" def test_none_allowed_returns_all(self) -> None: client, inner = _make_client() inner.list_tools = AsyncMock( return_value=[_fake_tool("a"), _fake_tool("b"), _fake_tool("c")] ) result = _run(client.list_tools_anthropic(allowed=None)) assert len(result) == 3 def test_empty_description_becomes_empty_string(self) -> None: client, inner = _make_client() t = MagicMock() t.name = "tool" t.description = None t.inputSchema = {} inner.list_tools = AsyncMock(return_value=[t]) result = _run(client.list_tools_anthropic()) assert result[0]["description"] == "" class TestListToolsBedrock: def test_returns_bedrock_format(self) -> None: client, inner = _make_client() inner.list_tools = AsyncMock( return_value=[ _fake_tool("scrape_tag_page", "Scrape tag", {"type": "object"}) ] ) result = _run(client.list_tools_bedrock()) assert len(result) == 1 spec = result[0]["toolSpec"] assert spec["name"] == "scrape_tag_page" assert spec["inputSchema"] == {"json": {"type": "object"}} def test_filters_by_allowed_set(self) -> None: client, inner = _make_client() inner.list_tools = AsyncMock(return_value=[_fake_tool("a"), _fake_tool("b")]) result = _run(client.list_tools_bedrock(allowed={"a"})) assert len(result) == 1 assert result[0]["toolSpec"]["name"] == "a" class TestGetPrompt: def test_extracts_text_from_messages(self) -> None: client, inner = _make_client() msg = MagicMock() msg.content = MagicMock() msg.content.text = "hello from prompt" prompt_result = MagicMock() prompt_result.messages = [msg] inner.get_prompt = AsyncMock(return_value=prompt_result) result = _run(client.get_prompt("my_prompt")) assert "hello from prompt" in result def test_extracts_text_from_list_content(self) -> None: client, inner = _make_client() part = MagicMock() part.text = "list part text" msg = MagicMock() msg.content = [part] prompt_result = MagicMock() prompt_result.messages = [msg] inner.get_prompt = AsyncMock(return_value=prompt_result) result = _run(client.get_prompt("my_prompt")) assert "list part text" in result def test_skips_message_with_no_content(self) -> None: client, inner = _make_client() msg = MagicMock() msg.content = None prompt_result = MagicMock() prompt_result.messages = [msg] inner.get_prompt = AsyncMock(return_value=prompt_result) result = _run(client.get_prompt("p")) assert result == "" def test_joins_multiple_messages(self) -> None: client, inner = _make_client() m1, m2 = MagicMock(), MagicMock() m1.content = MagicMock() m1.content.text = "first" m2.content = MagicMock() m2.content.text = "second" prompt_result = MagicMock() prompt_result.messages = [m1, m2] inner.get_prompt = AsyncMock(return_value=prompt_result) result = _run(client.get_prompt("p")) assert "first" in result assert "second" in result class TestCallTool: def test_returns_structured_content_when_present(self) -> None: client, inner = _make_client() call_result = MagicMock() call_result.structured_content = {"views": 1000} inner.call_tool = AsyncMock(return_value=call_result) result = _run(client.call_tool("scrape_video", {"url": "https://t.co/v"})) assert result == {"views": 1000} def test_parses_json_from_text_content(self) -> None: client, inner = _make_client() first = MagicMock() first.text = json.dumps({"ok": True, "views": 500}) call_result = MagicMock() call_result.structured_content = None call_result.content = [first] inner.call_tool = AsyncMock(return_value=call_result) result = _run(client.call_tool("scrape_video", {})) assert result == {"ok": True, "views": 500} def test_returns_raw_string_when_not_json(self) -> None: client, inner = _make_client() first = MagicMock() first.text = "not json" call_result = MagicMock() call_result.structured_content = None call_result.content = [first] inner.call_tool = AsyncMock(return_value=call_result) result = _run(client.call_tool("tool", {})) assert result == "not json" def test_returns_empty_dict_when_no_content(self) -> None: client, inner = _make_client() call_result = MagicMock() call_result.structured_content = None call_result.content = [] inner.call_tool = AsyncMock(return_value=call_result) result = _run(client.call_tool("tool", {})) assert result == {} class TestMakeClient: def test_http_transport_when_url_set(self) -> None: with ( patch("marketing_intelligence.agent.mcp_client.settings") as ms, patch("marketing_intelligence.agent.mcp_client.Client") as MockClient, ): ms.mcp_server_url = "http://localhost:8000" _real_make_client() MockClient.assert_called_once_with("http://localhost:8000") def test_inprocess_transport_when_no_url(self) -> None: mock_server_module = MagicMock() mock_mcp_obj = MagicMock() mock_server_module.mcp = mock_mcp_obj with ( patch("marketing_intelligence.agent.mcp_client.settings") as ms, patch("marketing_intelligence.agent.mcp_client.Client") as MockClient, patch.dict( "sys.modules", {"marketing_intelligence.mcp.server": mock_server_module} ), ): ms.mcp_server_url = None _real_make_client() MockClient.assert_called_once_with(mock_mcp_obj) class TestMCPToolClientContextManager: def test_aenter_returns_self(self) -> None: client, inner = _make_client() result = _run(client.__aenter__()) assert result is client inner.__aenter__.assert_awaited_once() def test_aexit_delegates_to_inner(self) -> None: client, inner = _make_client() _run(client.__aexit__(None, None, None)) inner.__aexit__.assert_awaited_once()