"""Tests for marketing_intelligence.mcp.tools.guard.tool_guard.""" import asyncio from collections.abc import Coroutine from typing import Any from marketing_intelligence.mcp.tools.guard import tool_guard from marketing_intelligence.mcp.tools.responses import ( FindSoundIdResponse, PageToolResult, TagPageResponse, ToolResult, ) def _run(coro: Coroutine[Any, Any, Any]) -> Any: return asyncio.run(coro) # ── Helpers ─────────────────────────────────────────────────────────────────── async def _ok_tag(tag: str, session_id: str | None = None) -> TagPageResponse: return TagPageResponse(tag=tag) async def _ok_find(artist_handle: str, track_tag: str) -> FindSoundIdResponse: return FindSoundIdResponse( artist_handle=artist_handle, track_tag=track_tag, sound_id="42" ) async def _raise_os(tag: str) -> TagPageResponse: raise OSError("network down") async def _raise_timeout(tag: str) -> TagPageResponse: raise TimeoutError("timed out") async def _raise_not_impl(tag: str) -> TagPageResponse: raise NotImplementedError("engine not supported") async def _raise_generic(tag: str) -> TagPageResponse: raise ValueError("something unexpected") # ── Happy path ──────────────────────────────────────────────────────────────── class TestHappyPath: def test_returns_result_unchanged(self) -> None: guarded = tool_guard(TagPageResponse, echo=("tag",))(_ok_tag) result = _run(guarded(tag="hateou")) assert isinstance(result, TagPageResponse) assert result.tag == "hateou" assert result.success is True def test_non_page_tool_result_happy(self) -> None: guarded = tool_guard(FindSoundIdResponse, echo=("artist_handle", "track_tag"))( _ok_find ) result = _run(guarded(artist_handle="mylessmithuk", track_tag="hateou")) assert result.sound_id == "42" assert result.success is True # ── Expected errors (OSError / TimeoutError → scrape_failed) ───────────────── class TestExpectedErrors: def test_os_error_returns_error_envelope(self) -> None: guarded = tool_guard(TagPageResponse, echo=("tag",))(_raise_os) result = _run(guarded(tag="hateou")) assert result.success is False assert "scrape_failed" in (result.error or "") assert "network down" in (result.error or "") def test_timeout_error_returns_error_envelope(self) -> None: guarded = tool_guard(TagPageResponse, echo=("tag",))(_raise_timeout) result = _run(guarded(tag="hateou")) assert result.success is False assert "scrape_failed" in (result.error or "") def test_page_tool_result_scrape_failed_sets_block_unknown(self) -> None: guarded = tool_guard(TagPageResponse, echo=("tag",))(_raise_os) result = _run(guarded(tag="hateou")) assert isinstance(result, PageToolResult) assert result.signals is not None assert result.signals.block_signal == "unknown" def test_echo_params_copied_into_error(self) -> None: guarded = tool_guard(TagPageResponse, echo=("tag",))(_raise_os) result = _run(guarded(tag="hateou")) assert result.tag == "hateou" # ── NotImplementedError → unsupported ──────────────────────────────────────── class TestNotImplementedError: def test_returns_unsupported_envelope(self) -> None: guarded = tool_guard(TagPageResponse, echo=("tag",))(_raise_not_impl) result = _run(guarded(tag="hateou")) assert result.success is False assert "unsupported" in (result.error or "") def test_page_tool_result_unsupported_block_none(self) -> None: guarded = tool_guard(TagPageResponse, echo=("tag",))(_raise_not_impl) result = _run(guarded(tag="hateou")) assert result.signals is not None assert result.signals.block_signal == "none" # ── Generic exception → internal_error ─────────────────────────────────────── class TestGenericException: def test_returns_internal_error_envelope(self) -> None: guarded = tool_guard(TagPageResponse, echo=("tag",))(_raise_generic) result = _run(guarded(tag="hateou")) assert result.success is False assert "internal_error" in (result.error or "") def test_page_tool_result_internal_error_block_none(self) -> None: guarded = tool_guard(TagPageResponse, echo=("tag",))(_raise_generic) result = _run(guarded(tag="hateou")) assert result.signals is not None assert result.signals.block_signal == "none" # ── Non-PageToolResult: no signals field ───────────────────────────────────── class TestNonPageToolResult: def test_os_error_no_signals(self) -> None: async def _raise(artist_handle: str, track_tag: str) -> FindSoundIdResponse: raise OSError("fail") guarded = tool_guard(FindSoundIdResponse, echo=("artist_handle", "track_tag"))( _raise ) result = _run(guarded(artist_handle="x", track_tag="y")) assert result.success is False assert not hasattr(result, "signals") def test_echo_two_params(self) -> None: async def _raise(artist_handle: str, track_tag: str) -> FindSoundIdResponse: raise OSError("fail") guarded = tool_guard(FindSoundIdResponse, echo=("artist_handle", "track_tag"))( _raise ) result = _run(guarded(artist_handle="mylessmithuk", track_tag="hateou")) assert result.artist_handle == "mylessmithuk" assert result.track_tag == "hateou" # ── Default parameter handling ──────────────────────────────────────────────── class TestDefaultParams: def test_optional_param_not_required_in_call(self) -> None: guarded = tool_guard(TagPageResponse, echo=("tag",))(_ok_tag) result = _run(guarded(tag="hateou")) assert result.success is True def test_optional_param_passed_explicitly(self) -> None: guarded = tool_guard(TagPageResponse, echo=("tag",))(_ok_tag) result = _run(guarded(tag="hateou", session_id="sess-1")) assert result.success is True # ── Empty echo ──────────────────────────────────────────────────────────────── class TestEmptyEcho: def test_no_echo_fields_in_error(self) -> None: async def _raise_batch(urls: list[str]) -> ToolResult: raise OSError("fail") guarded = tool_guard(ToolResult, echo=())(_raise_batch) result = _run(guarded(urls=["https://tiktok.com/@a/video/1"])) assert result.success is False assert "scrape_failed" in (result.error or "")