Add standalone Unraid ARR MCP package

This commit is contained in:
Mikei386
2026-08-27 21:49:01 +02:00
parent 588f24894d
commit d570a06911
10 changed files with 472 additions and 12 deletions
Regular → Executable
+54 -12
View File
@@ -1,6 +1,46 @@
import unittest
#!/usr/bin/env python3
"""Focused offline tests for the model-oriented Sonarr overlay."""
from arr_mcp.mcp import mcp_sonarr
from __future__ import annotations
import importlib.util
import sys
import types
import unittest
from pathlib import Path
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:
@@ -29,24 +69,26 @@ class FakeSonarrClient:
{"id": 2, "seasonNumber": 7, "episodeNumber": 2, "hasFile": False},
]
def post_release(self, data):
self.posted.append(data)
return data
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:
mcp_sonarr._APPROVALS.clear()
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 mcp_sonarr._preview_release_grab(client, scope)
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 mcp_sonarr._grab_release(
result = await self.module._grab_release(
client,
{**scope, "confirm": True, "approval_ticket": preview["approval_ticket"]},
)
@@ -58,18 +100,18 @@ class ReleaseGrabTests(unittest.IsolatedAsyncioTestCase):
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 mcp_sonarr._preview_release_grab(client, scope)
blocked = await self.module._preview_release_grab(client, scope)
self.assertTrue(blocked["force_required"])
self.assertIsNone(blocked["approval_ticket"])
approved = await mcp_sonarr._preview_release_grab(client, {**scope, "force": True})
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 mcp_sonarr._preview_release_grab(
await self.module._preview_release_grab(
client,
{"series_id": 42, "season_number": 7, "guid": "different-guid"},
)
@@ -91,7 +133,7 @@ class ReleaseGrabTests(unittest.IsolatedAsyncioTestCase):
"releaseGroup": "HQC",
},
]
result = await mcp_sonarr._search_releases(
result = await self.module._search_releases(
client,
{
"series_id": 42,