mirror of
https://github.com/jmagar/unraid-mcp.git
synced 2026-03-23 12:39:24 -07:00
feat: add 28 GraphQL mutations across storage, info, docker, and new settings tool
- storage: flash_backup mutation (initiates rclone flash backup, destructive) - info: update_server and update_ssh mutations - docker: 11 organizer mutations (create_folder, set_folder_children, delete_entries, move_to_folder, move_to_position, rename_folder, create_folder_with_items, update_view_prefs, sync_templates, reset_template_mappings, refresh_digests); delete_entries and reset_template_mappings added to DESTRUCTIVE_ACTIONS - settings: new unraid_settings tool with 9 mutations (update, update_temperature, update_time, configure_ups, update_api, connect_sign_in, connect_sign_out, setup_remote_access, enable_dynamic_remote_access); registered in server.py - tests: 82 new tests (28 settings, 23 docker organizer, 7 info, 6 storage + 18 existing fixes for notification regex and safety audit list) - bump version 0.3.0 → 0.4.0 (11 tools, ~104 actions) 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -342,3 +342,111 @@ class TestDockerNetworkErrors:
|
||||
tool_fn = _make_tool()
|
||||
with pytest.raises(ToolError, match="Invalid JSON"):
|
||||
await tool_fn(action="list")
|
||||
|
||||
|
||||
_ORGANIZER_RESPONSE = {"version": 1.0, "views": [{"id": "default", "name": "Default", "rootId": "root", "flatEntries": []}]}
|
||||
|
||||
|
||||
class TestDockerOrganizerMutations:
|
||||
async def test_create_folder_success(self, _mock_graphql: AsyncMock) -> None:
|
||||
_mock_graphql.return_value = {"createDockerFolder": _ORGANIZER_RESPONSE}
|
||||
result = await _make_tool()(action="create_folder", folder_name="Media")
|
||||
assert result["success"] is True
|
||||
|
||||
async def test_create_folder_requires_name(self, _mock_graphql: AsyncMock) -> None:
|
||||
with pytest.raises(ToolError, match="folder_name"):
|
||||
await _make_tool()(action="create_folder")
|
||||
|
||||
async def test_set_folder_children_success(self, _mock_graphql: AsyncMock) -> None:
|
||||
_mock_graphql.return_value = {"setDockerFolderChildren": _ORGANIZER_RESPONSE}
|
||||
result = await _make_tool()(action="set_folder_children", children_ids=["c1"])
|
||||
assert result["success"] is True
|
||||
|
||||
async def test_set_folder_children_requires_children(self, _mock_graphql: AsyncMock) -> None:
|
||||
with pytest.raises(ToolError, match="children_ids"):
|
||||
await _make_tool()(action="set_folder_children")
|
||||
|
||||
async def test_delete_entries_requires_confirm(self, _mock_graphql: AsyncMock) -> None:
|
||||
with pytest.raises(ToolError, match="destructive"):
|
||||
await _make_tool()(action="delete_entries", entry_ids=["e1"])
|
||||
|
||||
async def test_delete_entries_requires_ids(self, _mock_graphql: AsyncMock) -> None:
|
||||
with pytest.raises(ToolError, match="entry_ids"):
|
||||
await _make_tool()(action="delete_entries", confirm=True)
|
||||
|
||||
async def test_delete_entries_success(self, _mock_graphql: AsyncMock) -> None:
|
||||
_mock_graphql.return_value = {"deleteDockerEntries": _ORGANIZER_RESPONSE}
|
||||
result = await _make_tool()(action="delete_entries", entry_ids=["e1"], confirm=True)
|
||||
assert result["success"] is True
|
||||
|
||||
async def test_move_to_folder_success(self, _mock_graphql: AsyncMock) -> None:
|
||||
_mock_graphql.return_value = {"moveDockerEntriesToFolder": _ORGANIZER_RESPONSE}
|
||||
result = await _make_tool()(action="move_to_folder", source_entry_ids=["e1"], destination_folder_id="f1")
|
||||
assert result["success"] is True
|
||||
|
||||
async def test_move_to_folder_requires_source_ids(self, _mock_graphql: AsyncMock) -> None:
|
||||
with pytest.raises(ToolError, match="source_entry_ids"):
|
||||
await _make_tool()(action="move_to_folder", destination_folder_id="f1")
|
||||
|
||||
async def test_move_to_folder_requires_destination(self, _mock_graphql: AsyncMock) -> None:
|
||||
with pytest.raises(ToolError, match="destination_folder_id"):
|
||||
await _make_tool()(action="move_to_folder", source_entry_ids=["e1"])
|
||||
|
||||
async def test_move_to_position_success(self, _mock_graphql: AsyncMock) -> None:
|
||||
_mock_graphql.return_value = {"moveDockerItemsToPosition": _ORGANIZER_RESPONSE}
|
||||
result = await _make_tool()(action="move_to_position", source_entry_ids=["e1"], destination_folder_id="f1", position=2.0)
|
||||
assert result["success"] is True
|
||||
|
||||
async def test_move_to_position_requires_position(self, _mock_graphql: AsyncMock) -> None:
|
||||
with pytest.raises(ToolError, match="position"):
|
||||
await _make_tool()(action="move_to_position", source_entry_ids=["e1"], destination_folder_id="f1")
|
||||
|
||||
async def test_rename_folder_success(self, _mock_graphql: AsyncMock) -> None:
|
||||
_mock_graphql.return_value = {"renameDockerFolder": _ORGANIZER_RESPONSE}
|
||||
result = await _make_tool()(action="rename_folder", folder_id="f1", new_folder_name="New")
|
||||
assert result["success"] is True
|
||||
|
||||
async def test_rename_folder_requires_folder_id(self, _mock_graphql: AsyncMock) -> None:
|
||||
with pytest.raises(ToolError, match="folder_id"):
|
||||
await _make_tool()(action="rename_folder", new_folder_name="New")
|
||||
|
||||
async def test_rename_folder_requires_new_name(self, _mock_graphql: AsyncMock) -> None:
|
||||
with pytest.raises(ToolError, match="new_folder_name"):
|
||||
await _make_tool()(action="rename_folder", folder_id="f1")
|
||||
|
||||
async def test_create_folder_with_items_success(self, _mock_graphql: AsyncMock) -> None:
|
||||
_mock_graphql.return_value = {"createDockerFolderWithItems": _ORGANIZER_RESPONSE}
|
||||
result = await _make_tool()(action="create_folder_with_items", folder_name="New")
|
||||
assert result["success"] is True
|
||||
|
||||
async def test_create_folder_with_items_requires_name(self, _mock_graphql: AsyncMock) -> None:
|
||||
with pytest.raises(ToolError, match="folder_name"):
|
||||
await _make_tool()(action="create_folder_with_items")
|
||||
|
||||
async def test_update_view_prefs_success(self, _mock_graphql: AsyncMock) -> None:
|
||||
_mock_graphql.return_value = {"updateDockerViewPreferences": _ORGANIZER_RESPONSE}
|
||||
result = await _make_tool()(action="update_view_prefs", view_prefs={"sort": "name"})
|
||||
assert result["success"] is True
|
||||
|
||||
async def test_update_view_prefs_requires_prefs(self, _mock_graphql: AsyncMock) -> None:
|
||||
with pytest.raises(ToolError, match="view_prefs"):
|
||||
await _make_tool()(action="update_view_prefs")
|
||||
|
||||
async def test_sync_templates_success(self, _mock_graphql: AsyncMock) -> None:
|
||||
_mock_graphql.return_value = {"syncDockerTemplatePaths": {"scanned": 5, "matched": 4, "skipped": 1, "errors": []}}
|
||||
result = await _make_tool()(action="sync_templates")
|
||||
assert result["success"] is True
|
||||
|
||||
async def test_reset_template_mappings_requires_confirm(self, _mock_graphql: AsyncMock) -> None:
|
||||
with pytest.raises(ToolError, match="destructive"):
|
||||
await _make_tool()(action="reset_template_mappings")
|
||||
|
||||
async def test_reset_template_mappings_success(self, _mock_graphql: AsyncMock) -> None:
|
||||
_mock_graphql.return_value = {"resetDockerTemplateMappings": True}
|
||||
result = await _make_tool()(action="reset_template_mappings", confirm=True)
|
||||
assert result["success"] is True
|
||||
|
||||
async def test_refresh_digests_success(self, _mock_graphql: AsyncMock) -> None:
|
||||
_mock_graphql.return_value = {"refreshDockerDigests": True}
|
||||
result = await _make_tool()(action="refresh_digests")
|
||||
assert result["success"] is True
|
||||
|
||||
Reference in New Issue
Block a user