"""Hamilton review MAJOR #5: connection recovery and config-vs-operational errors. Pre-fix: any connection failure set `_connection_error` and pinned it forever. A transient network blip required restarting the MCP server. Fix: distinguish *configuration* errors (missing env, bad WSDL) which are pinned, from *operational* errors (network, TLS, session timeout) which can be retried on the next call. """ from pathlib import Path import pytest from mcp_cucm_axl.cache import AxlCache from mcp_cucm_axl.client import AxlClient @pytest.fixture def cache(tmp_path: Path) -> AxlCache: return AxlCache(tmp_path / "test.sqlite", default_ttl=60, cluster_id="test") def test_config_error_is_pinned(cache: AxlCache, monkeypatch): """Missing AXL_URL is a config error — it doesn't get better on retry, and the next call should still raise the same clear message.""" monkeypatch.delenv("AXL_URL", raising=False) monkeypatch.delenv("AXL_USER", raising=False) monkeypatch.delenv("AXL_PASS", raising=False) client = AxlClient(cache) with pytest.raises(RuntimeError, match="AXL_URL"): client._ensure_connected() # Second call: same config error, pinned with pytest.raises(RuntimeError, match="AXL_URL"): client._ensure_connected() def test_operational_error_is_not_pinned(cache: AxlCache, monkeypatch): """A transient operational error (zeep Client construction failing, network blip, etc.) should NOT pin the client forever. The next call must be allowed to retry.""" monkeypatch.setenv("AXL_URL", "https://test.invalid:8443/axl") monkeypatch.setenv("AXL_USER", "test") monkeypatch.setenv("AXL_PASS", "test") monkeypatch.setenv("AXL_VERIFY_TLS", "false") # Force the zeep Client constructor inside _ensure_connected to raise. # This simulates "WSDL fetch failed", "TLS handshake error", etc. — # transient operational failures. from mcp_cucm_axl import client as client_mod def boom(*args, **kwargs): raise ConnectionError("simulated transient network failure") monkeypatch.setattr(client_mod, "Client", boom) client = AxlClient(cache) with pytest.raises(RuntimeError, match="simulated transient"): client._ensure_connected() # Hamilton review MAJOR #5: operational errors must NOT set _config_error. # _config_error is the permanent pin; only set on missing env vars / config # mistakes. A failed network connection is operational and the next call # must be allowed to retry. assert client._config_error is None, ( "operational errors must not set _config_error (the pin); " "only configuration errors (missing env vars, bad WSDL) should pin" ) # _last_error is set for diagnostics, but it does not block retries. assert client._last_error is not None, ( "_last_error should record the operational failure for diagnostics" ) assert "simulated transient" in client._last_error def test_health_diagnostic_includes_connection_state(cache: AxlCache): """The client should expose its connection age / last-attempt info so an operator can see what's going on without reading sys.stderr.""" client = AxlClient(cache) info = client.connection_status() assert "connected" in info assert info["connected"] is False # never tried yet assert "last_error" in info