chore: update gitignore, bump to 0.2.1, apply CodeRabbit fixes

- Add .windsurf/, *.bak*, .1code/, .emdash.json to .gitignore
- Sync standard gitignore entries per project conventions
- Apply final test/tool fixes from CodeRabbit review threads
- Update GraphQL schema to latest introspection snapshot
- Bump version 0.2.0 → 0.2.1

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Jacob Magar
2026-03-13 00:53:51 -04:00
parent 2a5b19c42f
commit 06f18f32fc
16 changed files with 3294 additions and 1138 deletions

View File

@@ -659,9 +659,10 @@ class TestArrayToolRequests:
return_value=_graphql_response({"parityCheck": {"start": True}})
)
tool = self._get_tool()
result = await tool(action="parity_start")
result = await tool(action="parity_start", correct=False)
body = _extract_request_body(route.calls.last.request)
assert "StartParityCheck" in body["query"]
assert body["variables"] == {"correct": False}
assert result["success"] is True
@respx.mock
@@ -858,9 +859,9 @@ class TestNotificationsToolRequests:
async def test_create_sends_input_variables(self) -> None:
route = respx.post(API_URL).mock(
return_value=_graphql_response(
{"notifications": {"createNotification": {
{"createNotification": {
"id": "n1", "title": "Test", "importance": "INFO",
}}}
}}
)
)
tool = self._get_tool()
@@ -882,7 +883,7 @@ class TestNotificationsToolRequests:
async def test_archive_sends_id_variable(self) -> None:
route = respx.post(API_URL).mock(
return_value=_graphql_response(
{"notifications": {"archiveNotification": True}}
{"archiveNotification": {"id": "notif-1"}}
)
)
tool = self._get_tool()
@@ -901,7 +902,7 @@ class TestNotificationsToolRequests:
async def test_delete_sends_id_and_type(self) -> None:
route = respx.post(API_URL).mock(
return_value=_graphql_response(
{"notifications": {"deleteNotification": True}}
{"deleteNotification": {"unread": {"total": 0}}}
)
)
tool = self._get_tool()
@@ -920,7 +921,7 @@ class TestNotificationsToolRequests:
async def test_archive_all_sends_importance_when_provided(self) -> None:
route = respx.post(API_URL).mock(
return_value=_graphql_response(
{"notifications": {"archiveAll": True}}
{"archiveAll": {"archive": {"total": 1}}}
)
)
tool = self._get_tool()
@@ -1087,10 +1088,10 @@ class TestKeysToolRequests:
async def test_create_sends_input_variables(self) -> None:
route = respx.post(API_URL).mock(
return_value=_graphql_response(
{"createApiKey": {
{"apiKey": {"create": {
"id": "k2", "name": "new-key",
"key": "secret", "roles": ["read"],
}}
}}}
)
)
tool = self._get_tool()
@@ -1106,7 +1107,7 @@ class TestKeysToolRequests:
async def test_update_sends_input_variables(self) -> None:
route = respx.post(API_URL).mock(
return_value=_graphql_response(
{"updateApiKey": {"id": "k1", "name": "renamed", "roles": ["admin"]}}
{"apiKey": {"update": {"id": "k1", "name": "renamed", "roles": ["admin"]}}}
)
)
tool = self._get_tool()
@@ -1126,12 +1127,12 @@ class TestKeysToolRequests:
@respx.mock
async def test_delete_sends_ids_when_confirmed(self) -> None:
route = respx.post(API_URL).mock(
return_value=_graphql_response({"deleteApiKeys": True})
return_value=_graphql_response({"apiKey": {"delete": True}})
)
tool = self._get_tool()
result = await tool(action="delete", key_id="k1", confirm=True)
body = _extract_request_body(route.calls.last.request)
assert "DeleteApiKeys" in body["query"]
assert "DeleteApiKey" in body["query"]
assert body["variables"]["input"]["ids"] == ["k1"]
assert result["success"] is True

View File

@@ -305,7 +305,7 @@ class TestConfirmAllowsExecution:
assert result["success"] is True
async def test_notifications_delete_with_confirm(self, _mock_notif_graphql: AsyncMock) -> None:
_mock_notif_graphql.return_value = {"notifications": {"deleteNotification": True}}
_mock_notif_graphql.return_value = {"deleteNotification": {"unread": {"total": 0}}}
tool_fn = make_tool_fn(
"unraid_mcp.tools.notifications", "register_notifications_tool", "unraid_notifications"
)
@@ -318,7 +318,7 @@ class TestConfirmAllowsExecution:
assert result["success"] is True
async def test_notifications_delete_archived_with_confirm(self, _mock_notif_graphql: AsyncMock) -> None:
_mock_notif_graphql.return_value = {"notifications": {"deleteArchivedNotifications": True}}
_mock_notif_graphql.return_value = {"deleteArchivedNotifications": {"archive": {"total": 0}}}
tool_fn = make_tool_fn(
"unraid_mcp.tools.notifications", "register_notifications_tool", "unraid_notifications"
)
@@ -332,7 +332,7 @@ class TestConfirmAllowsExecution:
assert result["success"] is True
async def test_keys_delete_with_confirm(self, _mock_keys_graphql: AsyncMock) -> None:
_mock_keys_graphql.return_value = {"deleteApiKeys": True}
_mock_keys_graphql.return_value = {"apiKey": {"delete": True}}
tool_fn = make_tool_fn("unraid_mcp.tools.keys", "register_keys_tool", "unraid_keys")
result = await tool_fn(action="delete", key_id="key-123", confirm=True)
assert result["success"] is True

View File

@@ -39,12 +39,17 @@ class TestArrayValidation:
with pytest.raises(ToolError, match="Invalid action"):
await tool_fn(action=action)
async def test_parity_start_requires_correct(self, _mock_graphql: AsyncMock) -> None:
tool_fn = _make_tool()
with pytest.raises(ToolError, match="correct is required"):
await tool_fn(action="parity_start")
class TestArrayActions:
async def test_parity_start(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {"parityCheck": {"start": True}}
tool_fn = _make_tool()
result = await tool_fn(action="parity_start")
result = await tool_fn(action="parity_start", correct=False)
assert result["success"] is True
assert result["action"] == "parity_start"
_mock_graphql.assert_called_once()
@@ -94,14 +99,14 @@ class TestArrayMutationFailures:
async def test_parity_start_mutation_returns_false(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {"parityCheck": {"start": False}}
tool_fn = _make_tool()
result = await tool_fn(action="parity_start")
result = await tool_fn(action="parity_start", correct=False)
assert result["success"] is True
assert result["data"] == {"parityCheck": {"start": False}}
async def test_parity_start_mutation_returns_null(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {"parityCheck": {"start": None}}
tool_fn = _make_tool()
result = await tool_fn(action="parity_start")
result = await tool_fn(action="parity_start", correct=False)
assert result["success"] is True
assert result["data"] == {"parityCheck": {"start": None}}
@@ -110,7 +115,7 @@ class TestArrayMutationFailures:
) -> None:
_mock_graphql.return_value = {"parityCheck": {"start": {}}}
tool_fn = _make_tool()
result = await tool_fn(action="parity_start")
result = await tool_fn(action="parity_start", correct=False)
assert result["success"] is True
assert result["data"] == {"parityCheck": {"start": {}}}
@@ -128,7 +133,7 @@ class TestArrayNetworkErrors:
_mock_graphql.side_effect = ToolError("HTTP error 500: Internal Server Error")
tool_fn = _make_tool()
with pytest.raises(ToolError, match="HTTP error 500"):
await tool_fn(action="parity_start")
await tool_fn(action="parity_start", correct=False)
async def test_connection_refused(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.side_effect = ToolError("Network connection error: Connection refused")

View File

@@ -65,7 +65,9 @@ class TestKeysActions:
async def test_create(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {
"createApiKey": {"id": "k:new", "name": "new-key", "key": "secret123", "roles": []}
"apiKey": {
"create": {"id": "k:new", "name": "new-key", "key": "secret123", "roles": []}
}
}
tool_fn = _make_tool()
result = await tool_fn(action="create", name="new-key")
@@ -74,11 +76,13 @@ class TestKeysActions:
async def test_create_with_roles(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {
"createApiKey": {
"id": "k:new",
"name": "admin-key",
"key": "secret",
"roles": ["admin"],
"apiKey": {
"create": {
"id": "k:new",
"name": "admin-key",
"key": "secret",
"roles": ["admin"],
}
}
}
tool_fn = _make_tool()
@@ -86,13 +90,15 @@ class TestKeysActions:
assert result["success"] is True
async def test_update(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {"updateApiKey": {"id": "k:1", "name": "renamed", "roles": []}}
_mock_graphql.return_value = {
"apiKey": {"update": {"id": "k:1", "name": "renamed", "roles": []}}
}
tool_fn = _make_tool()
result = await tool_fn(action="update", key_id="k:1", name="renamed")
assert result["success"] is True
async def test_delete(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {"deleteApiKeys": True}
_mock_graphql.return_value = {"apiKey": {"delete": True}}
tool_fn = _make_tool()
result = await tool_fn(action="delete", key_id="k:1", confirm=True)
assert result["success"] is True

View File

@@ -82,9 +82,7 @@ class TestNotificationsActions:
async def test_create(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {
"notifications": {
"createNotification": {"id": "n:new", "title": "Test", "importance": "INFO"}
}
"createNotification": {"id": "n:new", "title": "Test", "importance": "INFO"}
}
tool_fn = _make_tool()
result = await tool_fn(
@@ -97,13 +95,13 @@ class TestNotificationsActions:
assert result["success"] is True
async def test_archive_notification(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {"notifications": {"archiveNotification": True}}
_mock_graphql.return_value = {"archiveNotification": {"id": "n:1"}}
tool_fn = _make_tool()
result = await tool_fn(action="archive", notification_id="n:1")
assert result["success"] is True
async def test_delete_with_confirm(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {"notifications": {"deleteNotification": True}}
_mock_graphql.return_value = {"deleteNotification": {"unread": {"total": 0}}}
tool_fn = _make_tool()
result = await tool_fn(
action="delete",
@@ -114,13 +112,13 @@ class TestNotificationsActions:
assert result["success"] is True
async def test_archive_all(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {"notifications": {"archiveAll": True}}
_mock_graphql.return_value = {"archiveAll": {"archive": {"total": 1}}}
tool_fn = _make_tool()
result = await tool_fn(action="archive_all")
assert result["success"] is True
async def test_unread_notification(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {"notifications": {"unreadNotification": True}}
_mock_graphql.return_value = {"unreadNotification": {"id": "n:1"}}
tool_fn = _make_tool()
result = await tool_fn(action="unread", notification_id="n:1")
assert result["success"] is True
@@ -140,7 +138,7 @@ class TestNotificationsActions:
assert filter_var["offset"] == 5
async def test_delete_archived(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {"notifications": {"deleteArchivedNotifications": True}}
_mock_graphql.return_value = {"deleteArchivedNotifications": {"archive": {"total": 0}}}
tool_fn = _make_tool()
result = await tool_fn(action="delete_archived", confirm=True)
assert result["success"] is True
@@ -180,9 +178,7 @@ class TestNotificationsCreateValidation:
)
async def test_alert_importance_accepted(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {
"notifications": {"createNotification": {"id": "n:1", "importance": "ALERT"}}
}
_mock_graphql.return_value = {"createNotification": {"id": "n:1", "importance": "ALERT"}}
tool_fn = _make_tool()
result = await tool_fn(
action="create", title="T", subject="S", description="D", importance="alert"
@@ -223,9 +219,7 @@ class TestNotificationsCreateValidation:
)
async def test_title_at_max_accepted(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {
"notifications": {"createNotification": {"id": "n:1", "importance": "NORMAL"}}
}
_mock_graphql.return_value = {"createNotification": {"id": "n:1", "importance": "NORMAL"}}
tool_fn = _make_tool()
result = await tool_fn(
action="create",