fix: address 18 PR review comments (threads 1-18)

Threads 1, 2, 3 — test hygiene:
- Move elicit_and_configure/elicit_reset_confirmation to module-level imports
  in unraid.py so tests can patch at unraid_mcp.tools.unraid.* (thread 2)
- Add return type annotations to _make_tool() in test_customization.py (thread 1)
- Replace unused _mock_ensure_started fixture params with @usefixtures (thread 3)

Thread 4 — remove dead 'connect' subaction from _SYSTEM_QUERIES; the subaction
was always rejected with a ToolError, creating an inconsistent contract.

Thread 5 — centralize two inline "query { online }" strings by reusing
_SYSTEM_QUERIES["online"]; add _DOCKER_QUERIES["_resolve"] for container-name
resolution instead of an inline query literal.

Threads 14, 15, 16, 17, 18 — test improvements:
- test-tools.sh: reword header to "broad non-destructive smoke coverage" (t14)
- test-tools.sh: add _json_payload() helper using jq --arg for safe JSON
  construction; replace all printf-based payloads (thread 15)
- test_input_validation.py: add return type annotations to _make_tool and all
  nested _run_test coroutines (thread 16)
- test_query_validation.py: extract _all_domain_dicts() shared helper to
  eliminate the duplicate 22-item registry (thread 17)
- test_query_validation.py: tighten regression threshold from 50 → 90 (thread 18)
This commit is contained in:
Jacob Magar
2026-03-16 10:01:12 -04:00
parent 884319ab11
commit cf9449a15d
10 changed files with 252 additions and 177 deletions

View File

@@ -60,7 +60,7 @@ def _assert_only_tool_error(exc: BaseException) -> None:
)
def _make_tool():
def _make_tool() -> Any:
return make_tool_fn("unraid_mcp.tools.unraid", "register_unraid_tool", "unraid")
@@ -82,7 +82,7 @@ class TestDockerContainerIdFuzzing:
def test_details_arbitrary_container_id(self, container_id: str) -> None:
"""Arbitrary container IDs for 'details' must not crash the tool."""
async def _run_test():
async def _run_test() -> None:
tool_fn = _make_tool()
with patch(
"unraid_mcp.tools.unraid.make_graphql_request", new_callable=AsyncMock
@@ -99,7 +99,7 @@ class TestDockerContainerIdFuzzing:
def test_start_arbitrary_container_id(self, container_id: str) -> None:
"""Arbitrary container IDs for 'start' must not crash the tool."""
async def _run_test():
async def _run_test() -> None:
tool_fn = _make_tool()
with patch(
"unraid_mcp.tools.unraid.make_graphql_request", new_callable=AsyncMock
@@ -115,7 +115,7 @@ class TestDockerContainerIdFuzzing:
def test_stop_arbitrary_container_id(self, container_id: str) -> None:
"""Arbitrary container IDs for 'stop' must not crash the tool."""
async def _run_test():
async def _run_test() -> None:
tool_fn = _make_tool()
with patch(
"unraid_mcp.tools.unraid.make_graphql_request", new_callable=AsyncMock
@@ -131,7 +131,7 @@ class TestDockerContainerIdFuzzing:
def test_restart_arbitrary_container_id(self, container_id: str) -> None:
"""Arbitrary container IDs for 'restart' must not crash the tool."""
async def _run_test():
async def _run_test() -> None:
tool_fn = _make_tool()
with patch(
"unraid_mcp.tools.unraid.make_graphql_request", new_callable=AsyncMock
@@ -172,7 +172,7 @@ class TestDockerInvalidActions:
if subaction in valid_subactions:
return # Skip valid subactions — they have different semantics
async def _run_test():
async def _run_test() -> None:
tool_fn = _make_tool()
with patch("unraid_mcp.tools.unraid.make_graphql_request", new_callable=AsyncMock):
try:
@@ -209,7 +209,7 @@ class TestNotificationsEnumFuzzing:
if importance.upper() in valid_importances:
return # Skip valid values
async def _run_test():
async def _run_test() -> None:
tool_fn = _make_tool()
with patch(
"unraid_mcp.tools.unraid.make_graphql_request",
@@ -242,7 +242,7 @@ class TestNotificationsEnumFuzzing:
if list_type.upper() in valid_list_types:
return # Skip valid values
async def _run_test():
async def _run_test() -> None:
tool_fn = _make_tool()
with patch(
"unraid_mcp.tools.unraid.make_graphql_request",
@@ -273,7 +273,7 @@ class TestNotificationsEnumFuzzing:
must raise ToolError for oversized values, never truncate silently or crash.
"""
async def _run_test():
async def _run_test() -> None:
tool_fn = _make_tool()
with patch(
"unraid_mcp.tools.unraid.make_graphql_request",
@@ -308,7 +308,7 @@ class TestNotificationsEnumFuzzing:
if notif_type.upper() in valid_types:
return
async def _run_test():
async def _run_test() -> None:
tool_fn = _make_tool()
with patch(
"unraid_mcp.tools.unraid.make_graphql_request",
@@ -353,7 +353,7 @@ class TestNotificationsEnumFuzzing:
if subaction in valid_subactions:
return
async def _run_test():
async def _run_test() -> None:
tool_fn = _make_tool()
with patch(
"unraid_mcp.tools.unraid.make_graphql_request",
@@ -389,7 +389,7 @@ class TestKeysInputFuzzing:
def test_get_arbitrary_key_id(self, key_id: str) -> None:
"""Arbitrary key_id for 'get' must not crash the tool."""
async def _run_test():
async def _run_test() -> None:
tool_fn = _make_tool()
with patch(
"unraid_mcp.tools.unraid.make_graphql_request", new_callable=AsyncMock
@@ -409,7 +409,7 @@ class TestKeysInputFuzzing:
def test_create_arbitrary_key_name(self, name: str) -> None:
"""Arbitrary name strings for 'create' must not crash the tool."""
async def _run_test():
async def _run_test() -> None:
tool_fn = _make_tool()
with patch(
"unraid_mcp.tools.unraid.make_graphql_request", new_callable=AsyncMock
@@ -431,7 +431,7 @@ class TestKeysInputFuzzing:
def test_add_role_arbitrary_roles(self, roles: list[str]) -> None:
"""Arbitrary role lists for 'add_role' must not crash the tool."""
async def _run_test():
async def _run_test() -> None:
tool_fn = _make_tool()
with patch(
"unraid_mcp.tools.unraid.make_graphql_request", new_callable=AsyncMock
@@ -456,7 +456,7 @@ class TestKeysInputFuzzing:
if subaction in valid_subactions:
return
async def _run_test():
async def _run_test() -> None:
tool_fn = _make_tool()
with patch("unraid_mcp.tools.unraid.make_graphql_request", new_callable=AsyncMock):
try:
@@ -489,7 +489,7 @@ class TestVMInputFuzzing:
def test_start_arbitrary_vm_id(self, vm_id: str) -> None:
"""Arbitrary vm_id for 'start' must not crash the tool."""
async def _run_test():
async def _run_test() -> None:
tool_fn = _make_tool()
with patch(
"unraid_mcp.tools.unraid.make_graphql_request",
@@ -510,7 +510,7 @@ class TestVMInputFuzzing:
def test_stop_arbitrary_vm_id(self, vm_id: str) -> None:
"""Arbitrary vm_id for 'stop' must not crash the tool."""
async def _run_test():
async def _run_test() -> None:
tool_fn = _make_tool()
with patch(
"unraid_mcp.tools.unraid.make_graphql_request",
@@ -531,7 +531,7 @@ class TestVMInputFuzzing:
def test_details_arbitrary_vm_id(self, vm_id: str) -> None:
"""Arbitrary vm_id for 'details' must not crash the tool."""
async def _run_test():
async def _run_test() -> None:
tool_fn = _make_tool()
with patch(
"unraid_mcp.tools.unraid.make_graphql_request",
@@ -566,7 +566,7 @@ class TestVMInputFuzzing:
if subaction in valid_subactions:
return
async def _run_test():
async def _run_test() -> None:
tool_fn = _make_tool()
with patch(
"unraid_mcp.tools.unraid.make_graphql_request",
@@ -616,7 +616,7 @@ class TestBoundaryValues:
def test_docker_details_adversarial_inputs(self, container_id: str) -> None:
"""Adversarial container_id values must not crash the Docker domain."""
async def _run_test():
async def _run_test() -> None:
tool_fn = _make_tool()
with patch(
"unraid_mcp.tools.unraid.make_graphql_request", new_callable=AsyncMock
@@ -649,7 +649,7 @@ class TestBoundaryValues:
def test_notifications_importance_adversarial(self, importance: str) -> None:
"""Adversarial importance values must raise ToolError, not crash."""
async def _run_test():
async def _run_test() -> None:
tool_fn = _make_tool()
with patch(
"unraid_mcp.tools.unraid.make_graphql_request",
@@ -687,7 +687,7 @@ class TestBoundaryValues:
def test_keys_get_adversarial_key_ids(self, key_id: str) -> None:
"""Adversarial key_id values must not crash the keys get action."""
async def _run_test():
async def _run_test() -> None:
tool_fn = _make_tool()
with patch(
"unraid_mcp.tools.unraid.make_graphql_request", new_callable=AsyncMock
@@ -739,7 +739,7 @@ class TestInfoActionGuard:
if action in valid_actions:
return
async def _run_test():
async def _run_test() -> None:
tool_fn = _make_tool()
with patch("unraid_mcp.tools.unraid.make_graphql_request", new_callable=AsyncMock):
try:

View File

@@ -28,6 +28,46 @@ def _validate_operation(schema: GraphQLSchema, query_str: str) -> list[str]:
return [str(e) for e in errors]
def _all_domain_dicts(unraid_mod: object) -> list[tuple[str, dict[str, str]]]:
"""Return all query/mutation dicts from the consolidated unraid module.
Single source of truth used by both test_all_tool_queries_validate and
test_total_operations_count so the two lists stay in sync automatically.
"""
import types
m = unraid_mod # type: ignore[assignment]
if not isinstance(m, types.ModuleType):
import importlib
m = importlib.import_module("unraid_mcp.tools.unraid")
return [
("system/QUERIES", m._SYSTEM_QUERIES),
("array/QUERIES", m._ARRAY_QUERIES),
("array/MUTATIONS", m._ARRAY_MUTATIONS),
("disk/QUERIES", m._DISK_QUERIES),
("disk/MUTATIONS", m._DISK_MUTATIONS),
("docker/QUERIES", m._DOCKER_QUERIES),
("docker/MUTATIONS", m._DOCKER_MUTATIONS),
("vm/QUERIES", m._VM_QUERIES),
("vm/MUTATIONS", m._VM_MUTATIONS),
("notification/QUERIES", m._NOTIFICATION_QUERIES),
("notification/MUTATIONS", m._NOTIFICATION_MUTATIONS),
("rclone/QUERIES", m._RCLONE_QUERIES),
("rclone/MUTATIONS", m._RCLONE_MUTATIONS),
("user/QUERIES", m._USER_QUERIES),
("key/QUERIES", m._KEY_QUERIES),
("key/MUTATIONS", m._KEY_MUTATIONS),
("setting/MUTATIONS", m._SETTING_MUTATIONS),
("customization/QUERIES", m._CUSTOMIZATION_QUERIES),
("customization/MUTATIONS", m._CUSTOMIZATION_MUTATIONS),
("plugin/QUERIES", m._PLUGIN_QUERIES),
("plugin/MUTATIONS", m._PLUGIN_MUTATIONS),
("oidc/QUERIES", m._OIDC_QUERIES),
]
# ============================================================================
# Info Tool (19 queries)
# ============================================================================
@@ -165,7 +205,6 @@ class TestInfoQueries:
"ups_devices",
"ups_device",
"ups_config",
"connect",
}
assert set(QUERIES.keys()) == expected_actions
@@ -378,6 +417,7 @@ class TestDockerQueries:
"details",
"networks",
"network_details",
"_resolve",
}
assert set(QUERIES.keys()) == expected
@@ -920,30 +960,7 @@ class TestSchemaCompleteness:
import unraid_mcp.tools.unraid as unraid_mod
# All query/mutation dicts in the consolidated module, keyed by domain/type label
all_operation_dicts: list[tuple[str, dict[str, str]]] = [
("system/QUERIES", unraid_mod._SYSTEM_QUERIES),
("array/QUERIES", unraid_mod._ARRAY_QUERIES),
("array/MUTATIONS", unraid_mod._ARRAY_MUTATIONS),
("disk/QUERIES", unraid_mod._DISK_QUERIES),
("disk/MUTATIONS", unraid_mod._DISK_MUTATIONS),
("docker/QUERIES", unraid_mod._DOCKER_QUERIES),
("docker/MUTATIONS", unraid_mod._DOCKER_MUTATIONS),
("vm/QUERIES", unraid_mod._VM_QUERIES),
("vm/MUTATIONS", unraid_mod._VM_MUTATIONS),
("notification/QUERIES", unraid_mod._NOTIFICATION_QUERIES),
("notification/MUTATIONS", unraid_mod._NOTIFICATION_MUTATIONS),
("rclone/QUERIES", unraid_mod._RCLONE_QUERIES),
("rclone/MUTATIONS", unraid_mod._RCLONE_MUTATIONS),
("user/QUERIES", unraid_mod._USER_QUERIES),
("key/QUERIES", unraid_mod._KEY_QUERIES),
("key/MUTATIONS", unraid_mod._KEY_MUTATIONS),
("setting/MUTATIONS", unraid_mod._SETTING_MUTATIONS),
("customization/QUERIES", unraid_mod._CUSTOMIZATION_QUERIES),
("customization/MUTATIONS", unraid_mod._CUSTOMIZATION_MUTATIONS),
("plugin/QUERIES", unraid_mod._PLUGIN_QUERIES),
("plugin/MUTATIONS", unraid_mod._PLUGIN_MUTATIONS),
("oidc/QUERIES", unraid_mod._OIDC_QUERIES),
]
all_operation_dicts = _all_domain_dicts(unraid_mod)
# Known schema mismatches — bugs in tool implementation, not in tests.
# Remove entries as they are fixed.
@@ -995,30 +1012,7 @@ class TestSchemaCompleteness:
"""Verify the expected number of tool operations exist."""
import unraid_mcp.tools.unraid as unraid_mod
all_dicts = [
unraid_mod._SYSTEM_QUERIES,
unraid_mod._ARRAY_QUERIES,
unraid_mod._ARRAY_MUTATIONS,
unraid_mod._DISK_QUERIES,
unraid_mod._DISK_MUTATIONS,
unraid_mod._DOCKER_QUERIES,
unraid_mod._DOCKER_MUTATIONS,
unraid_mod._VM_QUERIES,
unraid_mod._VM_MUTATIONS,
unraid_mod._NOTIFICATION_QUERIES,
unraid_mod._NOTIFICATION_MUTATIONS,
unraid_mod._RCLONE_QUERIES,
unraid_mod._RCLONE_MUTATIONS,
unraid_mod._USER_QUERIES,
unraid_mod._KEY_QUERIES,
unraid_mod._KEY_MUTATIONS,
unraid_mod._SETTING_MUTATIONS,
unraid_mod._CUSTOMIZATION_QUERIES,
unraid_mod._CUSTOMIZATION_MUTATIONS,
unraid_mod._PLUGIN_QUERIES,
unraid_mod._PLUGIN_MUTATIONS,
unraid_mod._OIDC_QUERIES,
]
all_dicts = [d for _, d in _all_domain_dicts(unraid_mod)]
total = sum(len(d) for d in all_dicts)
assert total >= 50, f"Expected at least 50 operations, found {total}"
assert total >= 90, f"Expected at least 90 operations, found {total}"

View File

@@ -15,7 +15,7 @@ def _mock_graphql():
yield m
def _make_tool():
def _make_tool() -> Any:
return make_tool_fn("unraid_mcp.tools.unraid", "register_unraid_tool", "unraid")

View File

@@ -129,6 +129,11 @@ class TestHealthActions:
mock_manager.active_subscriptions = {}
mock_manager.resource_data = {}
mock_cache = MagicMock()
mock_cache.statistics.return_value = MagicMock(call_tool=None)
mock_error = MagicMock()
mock_error.get_error_stats.return_value = {}
with (
patch("unraid_mcp.subscriptions.manager.subscription_manager", mock_manager),
patch("unraid_mcp.subscriptions.resources.ensure_subscriptions_started", AsyncMock()),
@@ -136,10 +141,14 @@ class TestHealthActions:
"unraid_mcp.subscriptions.utils._analyze_subscription_status",
return_value=(0, []),
),
patch("unraid_mcp.server.cache_middleware", mock_cache),
patch("unraid_mcp.server.error_middleware", mock_error),
):
result = await tool_fn(action="health", subaction="diagnose")
assert "subscriptions" in result
assert "summary" in result
assert "cache" in result
assert "errors" in result
async def test_diagnose_wraps_exception(self, _mock_graphql: AsyncMock) -> None:
"""When subscription manager raises, tool wraps in ToolError."""
@@ -223,7 +232,7 @@ async def test_health_setup_action_calls_elicitation() -> None:
with (
patch("unraid_mcp.config.settings.CREDENTIALS_ENV_PATH", mock_path),
patch(
"unraid_mcp.core.setup.elicit_and_configure", new=AsyncMock(return_value=True)
"unraid_mcp.tools.unraid.elicit_and_configure", new=AsyncMock(return_value=True)
) as mock_elicit,
):
result = await tool_fn(action="health", subaction="setup", ctx=MagicMock())
@@ -242,7 +251,7 @@ async def test_health_setup_action_returns_declined_message() -> None:
with (
patch("unraid_mcp.config.settings.CREDENTIALS_ENV_PATH", mock_path),
patch("unraid_mcp.core.setup.elicit_and_configure", new=AsyncMock(return_value=False)),
patch("unraid_mcp.tools.unraid.elicit_and_configure", new=AsyncMock(return_value=False)),
):
result = await tool_fn(action="health", subaction="setup", ctx=MagicMock())
@@ -268,10 +277,10 @@ async def test_health_setup_already_configured_and_working_no_reset() -> None:
new=AsyncMock(return_value={"online": True}),
),
patch(
"unraid_mcp.core.setup.elicit_reset_confirmation",
"unraid_mcp.tools.unraid.elicit_reset_confirmation",
new=AsyncMock(return_value=False),
),
patch("unraid_mcp.core.setup.elicit_and_configure") as mock_configure,
patch("unraid_mcp.tools.unraid.elicit_and_configure") as mock_configure,
):
result = await tool_fn(action="health", subaction="setup", ctx=MagicMock())
@@ -295,11 +304,11 @@ async def test_health_setup_already_configured_user_confirms_reset() -> None:
new=AsyncMock(return_value={"online": True}),
),
patch(
"unraid_mcp.core.setup.elicit_reset_confirmation",
"unraid_mcp.tools.unraid.elicit_reset_confirmation",
new=AsyncMock(return_value=True),
),
patch(
"unraid_mcp.core.setup.elicit_and_configure", new=AsyncMock(return_value=True)
"unraid_mcp.tools.unraid.elicit_and_configure", new=AsyncMock(return_value=True)
) as mock_configure,
):
result = await tool_fn(action="health", subaction="setup", ctx=MagicMock())
@@ -323,11 +332,11 @@ async def test_health_setup_credentials_exist_but_connection_fails_user_confirms
new=AsyncMock(side_effect=Exception("connection refused")),
),
patch(
"unraid_mcp.core.setup.elicit_reset_confirmation",
"unraid_mcp.tools.unraid.elicit_reset_confirmation",
new=AsyncMock(return_value=True),
),
patch(
"unraid_mcp.core.setup.elicit_and_configure", new=AsyncMock(return_value=True)
"unraid_mcp.tools.unraid.elicit_and_configure", new=AsyncMock(return_value=True)
) as mock_configure,
):
result = await tool_fn(action="health", subaction="setup", ctx=MagicMock())
@@ -351,10 +360,10 @@ async def test_health_setup_credentials_exist_connection_fails_user_declines() -
new=AsyncMock(side_effect=Exception("connection refused")),
),
patch(
"unraid_mcp.core.setup.elicit_reset_confirmation",
"unraid_mcp.tools.unraid.elicit_reset_confirmation",
new=AsyncMock(return_value=False),
),
patch("unraid_mcp.core.setup.elicit_and_configure") as mock_configure,
patch("unraid_mcp.tools.unraid.elicit_and_configure") as mock_configure,
):
result = await tool_fn(action="health", subaction="setup", ctx=MagicMock())
@@ -376,7 +385,7 @@ async def test_health_setup_ctx_none_already_configured_returns_no_changes() ->
"unraid_mcp.tools.unraid.make_graphql_request",
new=AsyncMock(return_value={"online": True}),
),
patch("unraid_mcp.core.setup.elicit_and_configure") as mock_configure,
patch("unraid_mcp.tools.unraid.elicit_and_configure") as mock_configure,
):
result = await tool_fn(action="health", subaction="setup", ctx=None)
@@ -399,7 +408,7 @@ async def test_health_setup_declined_message_includes_manual_path() -> None:
with (
patch("unraid_mcp.config.settings.CREDENTIALS_ENV_PATH", mock_path),
patch("unraid_mcp.core.setup.elicit_and_configure", new=AsyncMock(return_value=False)),
patch("unraid_mcp.tools.unraid.elicit_and_configure", new=AsyncMock(return_value=False)),
):
result = await tool_fn(action="health", subaction="setup", ctx=MagicMock())

View File

@@ -120,7 +120,7 @@ class TestUnraidInfoTool:
async def test_connect_action_raises_tool_error(self, _mock_graphql: AsyncMock) -> None:
tool_fn = _make_tool()
with pytest.raises(ToolError, match="connect.*not available"):
with pytest.raises(ToolError, match="Invalid subaction 'connect'"):
await tool_fn(action="system", subaction="connect")
async def test_generic_exception_wraps(self, _mock_graphql: AsyncMock) -> None:

View File

@@ -30,9 +30,8 @@ class TestLiveResourcesUseManagerCache:
"""All live resources must read from the persistent SubscriptionManager cache."""
@pytest.mark.parametrize("action", list(SNAPSHOT_ACTIONS.keys()))
async def test_resource_returns_cached_data(
self, action: str, _mock_ensure_started: AsyncMock
) -> None:
@pytest.mark.usefixtures("_mock_ensure_started")
async def test_resource_returns_cached_data(self, action: str) -> None:
cached = {"systemMetricsCpu": {"percentTotal": 12.5}}
with patch("unraid_mcp.subscriptions.resources.subscription_manager") as mock_mgr:
mock_mgr.get_resource_data = AsyncMock(return_value=cached)
@@ -42,8 +41,9 @@ class TestLiveResourcesUseManagerCache:
assert json.loads(result) == cached
@pytest.mark.parametrize("action", list(SNAPSHOT_ACTIONS.keys()))
@pytest.mark.usefixtures("_mock_ensure_started")
async def test_resource_returns_connecting_when_no_cache_and_no_error(
self, action: str, _mock_ensure_started: AsyncMock
self, action: str
) -> None:
with patch("unraid_mcp.subscriptions.resources.subscription_manager") as mock_mgr:
mock_mgr.get_resource_data = AsyncMock(return_value=None)
@@ -55,9 +55,8 @@ class TestLiveResourcesUseManagerCache:
assert parsed["status"] == "connecting"
@pytest.mark.parametrize("action", list(SNAPSHOT_ACTIONS.keys()))
async def test_resource_returns_error_status_on_permanent_failure(
self, action: str, _mock_ensure_started: AsyncMock
) -> None:
@pytest.mark.usefixtures("_mock_ensure_started")
async def test_resource_returns_error_status_on_permanent_failure(self, action: str) -> None:
with patch("unraid_mcp.subscriptions.resources.subscription_manager") as mock_mgr:
mock_mgr.get_resource_data = AsyncMock(return_value=None)
mock_mgr.last_error = {action: "WebSocket auth failed"}
@@ -91,7 +90,8 @@ class TestSnapshotSubscriptionsRegistered:
class TestLogsStreamResource:
async def test_logs_stream_no_data(self, _mock_ensure_started: AsyncMock) -> None:
@pytest.mark.usefixtures("_mock_ensure_started")
async def test_logs_stream_no_data(self) -> None:
with patch("unraid_mcp.subscriptions.resources.subscription_manager") as mock_mgr:
mock_mgr.get_resource_data = AsyncMock(return_value=None)
mcp = _make_resources()
@@ -101,9 +101,8 @@ class TestLogsStreamResource:
parsed = json.loads(result)
assert "status" in parsed
async def test_logs_stream_returns_data_with_empty_dict(
self, _mock_ensure_started: AsyncMock
) -> None:
@pytest.mark.usefixtures("_mock_ensure_started")
async def test_logs_stream_returns_data_with_empty_dict(self) -> None:
"""Empty dict cache hit must return data, not 'connecting' status."""
with patch("unraid_mcp.subscriptions.resources.subscription_manager") as mock_mgr:
mock_mgr.get_resource_data = AsyncMock(return_value={})
@@ -118,9 +117,8 @@ class TestAutoStartDisabledFallback:
"""When auto_start is disabled, resources fall back to on-demand subscribe_once."""
@pytest.mark.parametrize("action", list(SNAPSHOT_ACTIONS.keys()))
async def test_fallback_returns_subscribe_once_data(
self, action: str, _mock_ensure_started: AsyncMock
) -> None:
@pytest.mark.usefixtures("_mock_ensure_started")
async def test_fallback_returns_subscribe_once_data(self, action: str) -> None:
fallback_data = {"systemMetricsCpu": {"percentTotal": 42.0}}
with (
patch("unraid_mcp.subscriptions.resources.subscription_manager") as mock_mgr,
@@ -138,9 +136,8 @@ class TestAutoStartDisabledFallback:
assert json.loads(result) == fallback_data
@pytest.mark.parametrize("action", list(SNAPSHOT_ACTIONS.keys()))
async def test_fallback_failure_returns_connecting(
self, action: str, _mock_ensure_started: AsyncMock
) -> None:
@pytest.mark.usefixtures("_mock_ensure_started")
async def test_fallback_failure_returns_connecting(self, action: str) -> None:
"""When on-demand fallback itself fails, still return 'connecting' status."""
with (
patch("unraid_mcp.subscriptions.resources.subscription_manager") as mock_mgr,