#!/usr/bin/env python3 """Focused offline tests for the model-oriented Sonarr overlay.""" from __future__ import annotations import importlib.util import sys import types import unittest from pathlib import Path from unittest.mock import patch SOURCE = Path(__file__).parents[1] / "platform/mcp/patches/mcp_sonarr.py" def load_module(): async def run_blocking(function, *args, **kwargs): kwargs.pop("service", None) return function(*args, **kwargs) def dispatch(client, action, kwargs, **_options): return getattr(client, action)(**kwargs) utilities = types.ModuleType("agent_utilities.mcp_utilities") utilities.dispatch = dispatch utilities.run_blocking = run_blocking sys.modules["agent_utilities"] = types.ModuleType("agent_utilities") sys.modules["agent_utilities.mcp_utilities"] = utilities fastmcp = types.ModuleType("fastmcp") fastmcp.FastMCP = object sys.modules["fastmcp"] = fastmcp pydantic = types.ModuleType("pydantic") pydantic.Field = lambda *args, **kwargs: kwargs.get("default") sys.modules["pydantic"] = pydantic auth = types.ModuleType("arr_mcp.auth") auth.get_sonarr_client = lambda: None sys.modules["arr_mcp"] = types.ModuleType("arr_mcp") sys.modules["arr_mcp.auth"] = auth spec = importlib.util.spec_from_file_location("sonarr_patch", SOURCE) module = importlib.util.module_from_spec(spec) assert spec.loader spec.loader.exec_module(module) return module class FakeSonarrClient: def __init__(self, *, rejected: bool = False) -> None: self.posted = [] self.release = { "guid": "exact-guid", "title": "Murder.She.Wrote.S07.German.AC3D.DL.1080p.WebHD.x265-FuN", "indexer": "Test Indexer", "indexerId": 7, "size": 27_600_000_000, "protocol": "usenet", "downloadAllowed": not rejected, "releaseGroup": "FuN", "seasonNumber": 7, "fullSeason": True, "rejections": ["Existing file has equal or better quality"] if rejected else [], } def get_release(self, **_kwargs): return [dict(self.release)] def get_episode(self, **_kwargs): return [ {"id": 1, "seasonNumber": 7, "episodeNumber": 1, "hasFile": True}, {"id": 2, "seasonNumber": 7, "episodeNumber": 2, "hasFile": False}, ] def post_release(self, data=None, **kwargs): payload = data if data is not None else kwargs self.posted.append(payload) return payload class ReleaseGrabTests(unittest.IsolatedAsyncioTestCase): def setUp(self) -> None: self.module = load_module() self.module._APPROVALS.clear() async def test_exact_release_requires_preview_and_ticket(self) -> None: client = FakeSonarrClient() scope = {"series_id": 42, "season_number": 7, "guid": "exact-guid"} preview = await self.module._preview_release_grab(client, scope) self.assertFalse(preview["download_started"]) self.assertEqual(preview["existing_episode_files_in_season"], 1) self.assertTrue(preview["approval_ticket"]) result = await self.module._grab_release( client, {**scope, "confirm": True, "approval_ticket": preview["approval_ticket"]}, ) self.assertTrue(result["download_started"]) self.assertFalse(result["replacement_guaranteed"]) self.assertEqual(len(client.posted), 1) self.assertEqual(client.posted[0]["guid"], "exact-guid") async def test_rejected_release_needs_force_in_preview(self) -> None: client = FakeSonarrClient(rejected=True) scope = {"series_id": 42, "season_number": 7, "guid": "exact-guid"} blocked = await self.module._preview_release_grab(client, scope) self.assertTrue(blocked["force_required"]) self.assertIsNone(blocked["approval_ticket"]) approved = await self.module._preview_release_grab(client, {**scope, "force": True}) self.assertFalse(approved["force_required"]) self.assertTrue(approved["approval_ticket"]) async def test_guid_must_still_match_current_sonarr_results(self) -> None: client = FakeSonarrClient() with self.assertRaisesRegex(ValueError, "no longer present"): await self.module._preview_release_grab( client, {"series_id": 42, "season_number": 7, "guid": "different-guid"}, ) async def test_search_can_limit_results_to_group_and_season_pack(self) -> None: client = FakeSonarrClient() client.get_release = lambda **_kwargs: [ dict(client.release), { **client.release, "guid": "episode-guid", "title": "Mord.ist.ihr.Hobby.S07E02.German.1080p-FuN", "fullSeason": False, }, { **client.release, "guid": "other-group", "title": "Mord.ist.ihr.Hobby.S07.German.1080p-HQC", "releaseGroup": "HQC", }, ] result = await self.module._search_releases( client, { "series_id": 42, "season_number": 7, "release_group": "FuN", "season_pack_only": True, }, ) self.assertEqual(result["results"]["total"], 1) self.assertEqual(result["results"]["items"][0]["guid"], "exact-guid") def test_read_only_surface_is_explicit_and_has_no_generic_action(self) -> None: class FakeMcp: def __init__(self): self.names = [] def tool(self, **_kwargs): def decorate(function): self.names.append(function.__name__) return function return decorate with patch.dict("os.environ", {"ARR_MCP_WRITE": "0"}): mcp = FakeMcp() self.module.register_sonarr_tools(mcp) self.assertEqual( mcp.names, [ "sonarr_find_series", "sonarr_get_season_summary", "sonarr_search_releases", "sonarr_system_status", ], ) self.assertNotIn("sonarr_action", mcp.names) def test_write_tools_are_registered_only_when_enabled(self) -> None: class FakeMcp: def __init__(self): self.names = [] def tool(self, **_kwargs): def decorate(function): self.names.append(function.__name__) return function return decorate with patch.dict("os.environ", {"ARR_MCP_WRITE": "1"}): mcp = FakeMcp() self.module.register_sonarr_tools(mcp) self.assertIn("sonarr_preview_release_grab", mcp.names) self.assertIn("sonarr_grab_release", mcp.names) self.assertIn("sonarr_preview_episode_search", mcp.names) self.assertIn("sonarr_start_episode_search", mcp.names) if __name__ == "__main__": unittest.main()