Files
mcp-nextcloud/tests/unit/test_readiness_cache.py
T
Chris CoutinhoandClaude Opus 4.8 6ef7786cec fix(health): non-gating readiness probe; shared-task-group lifespan; settings migration
Fixes MCP reconnect timeouts on tenant servers (Deck #302). Three changes:

- /health/ready now gates only on local config. Nextcloud/Qdrant health is
  refreshed by a background loop, cached, and reported but NON-gating, so a
  single-replica tenant Pod is no longer pulled from its Service on a transient
  dependency blip (which dropped every MCP streamable-HTTP session and caused
  reconnect timeouts). The probe path performs no external I/O.
- Refactor starlette_lifespan: collapse the four near-identical per-mode
  task-group + session + yield + teardown skeletons into one shared task group
  that also runs the readiness refresh loop; each mode contributes a
  (start, teardown) pair. eviction_task_group is now always present.
- Migrate app.py off os.getenv: all config is read through dynaconf Settings
  (adds health_ready_refresh_interval, oidc_token_type, oidc_scopes, port).
  Inline/dynamic defaults preserved at each call site.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 21:28:19 +02:00

65 lines
2.0 KiB
Python

"""Unit tests for the readiness dependency-health cache (Deck #302).
The readiness probe reads this snapshot without performing any I/O; these tests
pin the small amount of logic it relies on (update/snapshot/staleness).
"""
import pytest
from nextcloud_mcp_server.observability.readiness import (
DependencyStatus,
ReadinessCache,
)
pytestmark = pytest.mark.unit
def test_dependency_status_defaults():
status = DependencyStatus(name="nextcloud")
assert status.healthy is None # not yet checked
assert status.detail == "pending"
assert status.checked_at == 0.0
def test_update_and_snapshot_round_trip():
cache = ReadinessCache()
cache.update("nextcloud_reachable", True, "ok", now=100.0)
cache.update("qdrant", False, "error: status 503", now=100.0)
snap = cache.snapshot()
assert snap["nextcloud_reachable"].healthy is True
assert snap["nextcloud_reachable"].detail == "ok"
assert snap["qdrant"].healthy is False
assert snap["qdrant"].detail == "error: status 503"
def test_snapshot_is_a_copy():
cache = ReadinessCache()
cache.update("nextcloud_reachable", True, "ok", now=1.0)
snap = cache.snapshot()
# Mutating the returned mapping must not affect the cache.
snap.clear()
assert "nextcloud_reachable" in cache.snapshot()
def test_is_stale_when_empty():
assert ReadinessCache(ttl_seconds=30.0).is_stale(now=0.0) is True
def test_is_stale_respects_ttl():
cache = ReadinessCache(ttl_seconds=30.0)
cache.update("nextcloud_reachable", True, "ok", now=100.0)
# Within the TTL window the snapshot is fresh.
assert cache.is_stale(now=120.0) is False
# At/after the TTL boundary it is stale.
assert cache.is_stale(now=130.0) is True
def test_is_stale_if_any_entry_is_old():
cache = ReadinessCache(ttl_seconds=30.0)
cache.update("nextcloud_reachable", True, "ok", now=100.0)
cache.update("qdrant", True, "ok", now=200.0)
# nextcloud_reachable is well past the TTL even though qdrant is fresh.
assert cache.is_stale(now=205.0) is True