fix: apply all PR review agent findings (silent failures, type safety, test gaps)

Addresses issues found by 4 parallel review agents (code-reviewer,
silent-failure-hunter, type-design-analyzer, pr-test-analyzer).

Source fixes:
- core/utils.py: add public safe_display_url() (moved from tools/health.py)
- core/client.py: rename _redact_sensitive → redact_sensitive (public API)
- core/types.py: add SubscriptionData.__post_init__ for tz-aware datetime
  enforcement; remove 6 unused type aliases (SystemHealth, APIResponse, etc.)
- subscriptions/manager.py: add exc_info=True to both except-Exception blocks;
  add except ValueError break-on-config-error before retry loop; import
  redact_sensitive by new public name
- subscriptions/resources.py: re-raise in autostart_subscriptions() so
  ensure_subscriptions_started() doesn't permanently set _subscriptions_started
- subscriptions/diagnostics.py: except ToolError: raise before broad except;
  use safe_display_url() instead of raw URL slice
- tools/health.py: move _safe_display_url to core/utils; add exc_info=True;
  raise ToolError (not return dict) on ImportError
- tools/info.py: use get_args(INFO_ACTIONS) instead of INFO_ACTIONS.__args__
- tools/{array,docker,keys,notifications,rclone,storage,virtualization}.py:
  add Literal-vs-ALL_ACTIONS sync check at import time

Test fixes:
- test_health.py: import safe_display_url from core.utils; update
  test_diagnose_import_error_internal to expect ToolError (not error dict)
- test_storage.py: add 3 safe_get tests for zero/False/empty-string values
- test_subscription_manager.py: add TestCapLogContentSingleMassiveLine (2 tests)
- test_client.py: rename _redact_sensitive → redact_sensitive; add tests for
  new sensitive keys and is_cacheable explicit-keyword form
This commit is contained in:
Jacob Magar
2026-02-19 02:23:04 -05:00
parent 348f4149a5
commit 1751bc2984
28 changed files with 354 additions and 187 deletions

View File

@@ -7,6 +7,7 @@ to the Unraid API with proper timeout handling and error management.
import asyncio
import hashlib
import json
import re
import time
from typing import Any, Final
@@ -47,14 +48,14 @@ def _is_sensitive_key(key: str) -> bool:
return any(s in key_lower for s in _SENSITIVE_KEYS)
def _redact_sensitive(obj: Any) -> Any:
def redact_sensitive(obj: Any) -> Any:
"""Recursively redact sensitive values from nested dicts/lists."""
if isinstance(obj, dict):
return {
k: ("***" if _is_sensitive_key(k) else _redact_sensitive(v)) for k, v in obj.items()
k: ("***" if _is_sensitive_key(k) else redact_sensitive(v)) for k, v in obj.items()
}
if isinstance(obj, list):
return [_redact_sensitive(item) for item in obj]
return [redact_sensitive(item) for item in obj]
return obj
@@ -139,6 +140,7 @@ _CACHEABLE_QUERY_PREFIXES = frozenset(
)
_CACHE_TTL_SECONDS = 60.0
_OPERATION_NAME_PATTERN = re.compile(r"^(?:query\s+)?([_A-Za-z][_0-9A-Za-z]*)\b")
class _QueryCache:
@@ -160,9 +162,13 @@ class _QueryCache:
@staticmethod
def is_cacheable(query: str) -> bool:
"""Check if a query is eligible for caching based on its operation name."""
if query.lstrip().startswith("mutation"):
normalized = query.lstrip()
if normalized.startswith("mutation"):
return False
return any(prefix in query for prefix in _CACHEABLE_QUERY_PREFIXES)
match = _OPERATION_NAME_PATTERN.match(normalized)
if not match:
return False
return match.group(1) in _CACHEABLE_QUERY_PREFIXES
def get(self, query: str, variables: dict[str, Any] | None) -> dict[str, Any] | None:
"""Return cached result if present and not expired, else None."""
@@ -324,7 +330,7 @@ async def make_graphql_request(
logger.debug(f"Making GraphQL request to {UNRAID_API_URL}:")
logger.debug(f"Query: {query[:200]}{'...' if len(query) > 200 else ''}") # Log truncated query
if variables:
logger.debug(f"Variables: {_redact_sensitive(variables)}")
logger.debug(f"Variables: {redact_sensitive(variables)}")
try:
# Rate limit: consume a token before making the request