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:
Jacob Magar
2026-03-13 03:03:37 -04:00
parent 4af1e74b4a
commit 9aee3a2448
11 changed files with 994 additions and 7 deletions

View File

@@ -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