mirror of
https://github.com/jmagar/unraid-mcp.git
synced 2026-03-23 04:29:17 -07:00
settings.py: drop update_temperature, update_time, update_api, connect_sign_in, connect_sign_out, setup_remote_access, enable_dynamic_remote_access, update_ssh — all 8 reference mutations confirmed absent from Unraid API v4.29.2. Keep update + configure_ups. info.py: drop update_server (updateServerIdentity not in Mutation type) and update_ssh (duplicate of removed settings action). MUTATIONS is now empty; DESTRUCTIVE_ACTIONS is now an empty set. notifications.py: drop create_unique (notifyIfUnique not in Mutation type). Tests: remove corresponding test classes, add parametrized regression tests asserting removed actions are not in each tool's Literal type, update KNOWN_DESTRUCTIVE and _DESTRUCTIVE_TEST_CASES in safety audit, update schema coverage assertions. 858 tests passing, 0 failures.
118 lines
4.0 KiB
Python
118 lines
4.0 KiB
Python
"""Tests for the unraid_settings tool."""
|
|
|
|
from collections.abc import Generator
|
|
from typing import get_args
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import pytest
|
|
from fastmcp import FastMCP
|
|
|
|
from unraid_mcp.core.exceptions import ToolError
|
|
from unraid_mcp.tools.settings import SETTINGS_ACTIONS, register_settings_tool
|
|
|
|
|
|
@pytest.fixture
|
|
def _mock_graphql() -> Generator[AsyncMock, None, None]:
|
|
with patch("unraid_mcp.tools.settings.make_graphql_request", new_callable=AsyncMock) as mock:
|
|
yield mock
|
|
|
|
|
|
def _make_tool() -> AsyncMock:
|
|
test_mcp = FastMCP("test")
|
|
register_settings_tool(test_mcp)
|
|
# FastMCP 3.x stores tools in providers[0]._components keyed as "tool:{name}@"
|
|
local_provider = test_mcp.providers[0]
|
|
tool = local_provider._components["tool:unraid_settings@"]
|
|
return tool.fn # type: ignore[union-attr]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Regression: removed actions must not appear in SETTINGS_ACTIONS
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"action",
|
|
[
|
|
"update_temperature",
|
|
"update_time",
|
|
"update_api",
|
|
"connect_sign_in",
|
|
"connect_sign_out",
|
|
"setup_remote_access",
|
|
"enable_dynamic_remote_access",
|
|
"update_ssh",
|
|
],
|
|
)
|
|
def test_removed_settings_actions_are_gone(action: str) -> None:
|
|
assert action not in get_args(SETTINGS_ACTIONS), (
|
|
f"{action} references a non-existent mutation and must not be in SETTINGS_ACTIONS"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Validation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSettingsValidation:
|
|
"""Tests for action validation and destructive guard."""
|
|
|
|
async def test_invalid_action(self, _mock_graphql: AsyncMock) -> None:
|
|
tool_fn = _make_tool()
|
|
with pytest.raises(ToolError, match="Invalid action"):
|
|
await tool_fn(action="nonexistent_action")
|
|
|
|
async def test_destructive_configure_ups_requires_confirm(
|
|
self, _mock_graphql: AsyncMock
|
|
) -> None:
|
|
tool_fn = _make_tool()
|
|
with pytest.raises(ToolError, match="confirm=True"):
|
|
await tool_fn(action="configure_ups", ups_config={"mode": "slave"})
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# update
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSettingsUpdate:
|
|
"""Tests for update action."""
|
|
|
|
async def test_update_requires_settings_input(self, _mock_graphql: AsyncMock) -> None:
|
|
tool_fn = _make_tool()
|
|
with pytest.raises(ToolError, match="settings_input is required"):
|
|
await tool_fn(action="update")
|
|
|
|
async def test_update_success(self, _mock_graphql: AsyncMock) -> None:
|
|
_mock_graphql.return_value = {
|
|
"updateSettings": {"restartRequired": False, "values": {}, "warnings": []}
|
|
}
|
|
tool_fn = _make_tool()
|
|
result = await tool_fn(action="update", settings_input={"shareCount": 5})
|
|
assert result["success"] is True
|
|
assert result["action"] == "update"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# configure_ups
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestUpsConfig:
|
|
"""Tests for configure_ups action."""
|
|
|
|
async def test_configure_ups_requires_ups_config(self, _mock_graphql: AsyncMock) -> None:
|
|
tool_fn = _make_tool()
|
|
with pytest.raises(ToolError, match="ups_config is required"):
|
|
await tool_fn(action="configure_ups", confirm=True)
|
|
|
|
async def test_configure_ups_success(self, _mock_graphql: AsyncMock) -> None:
|
|
_mock_graphql.return_value = {"configureUps": True}
|
|
tool_fn = _make_tool()
|
|
result = await tool_fn(
|
|
action="configure_ups", confirm=True, ups_config={"mode": "master", "cable": "usb"}
|
|
)
|
|
assert result["success"] is True
|
|
assert result["action"] == "configure_ups"
|