Files
lfnovo--open-notebook/tests/test_connection_test_classification.py
2026-07-13 12:10:23 +08:00

138 lines
5.1 KiB
Python

"""
Tests for connection-test error classification (open_notebook/ai/connection_tester.py).
Two semantics share one auth/network classifier:
- The *provider* test asks only "do these credentials reach a working
provider?" — so a missing/retired/unsupported test model is SUCCESS (the
#970 durability fix): a hard-coded test model going away must never be
misreported as a broken connection.
- The *individual model* test validates one specific registered model, so a
missing model there IS a failure.
"""
import pytest
from open_notebook.ai.connection_tester import (
_connection_failure_reason,
_is_rate_limit,
_normalize_error_message,
classify_provider_test_error,
)
# Realistic provider error strings.
GOOGLE_RETIRED_MODEL_404 = (
"404 models/gemini-2.0-flash is not found for API version v1beta, "
"or is not supported for generateContent."
)
GOOGLE_DEPRECATED = "400 Model gemini-1.5-pro has been deprecated."
GOOGLE_BAD_KEY_401 = "401 API key not valid. Please pass a valid API key."
GOOGLE_PERM_403 = "403 Permission denied on resource project."
GOOGLE_QUOTA_429 = "429 Resource has been exhausted (e.g. check quota)."
DNS_FAILURE = "Connection error: [Errno -2] Name or service not known (getaddrinfo failed)"
TIMEOUT = "Request timed out after 10s"
class TestConnectionFailureReason:
"""Only auth/permission/network are true connection failures."""
@pytest.mark.parametrize(
"msg,expected_fragment",
[
(GOOGLE_BAD_KEY_401, "Invalid API key"),
(GOOGLE_PERM_403, "lacks required permissions"),
(DNS_FAILURE, "Connection error"),
(TIMEOUT, "timed out"),
],
)
def test_real_failures_return_a_reason(self, msg, expected_fragment):
reason = _connection_failure_reason(msg)
assert reason is not None
assert expected_fragment in reason
@pytest.mark.parametrize(
"msg",
[GOOGLE_RETIRED_MODEL_404, GOOGLE_DEPRECATED, GOOGLE_QUOTA_429],
)
def test_provider_reached_returns_none(self, msg):
# A model/quota problem came back FROM the provider — not a failure.
assert _connection_failure_reason(msg) is None
class TestRateLimitDetection:
@pytest.mark.parametrize(
"msg",
[
"429 Too Many Requests",
"Resource has been exhausted (e.g. check quota).",
"You have hit your rate limit",
"quota exceeded for this project",
],
)
def test_detects_throttling_phrasings(self, msg):
assert _is_rate_limit(msg) is True
def test_plain_model_error_is_not_rate_limit(self):
assert _is_rate_limit(GOOGLE_RETIRED_MODEL_404) is False
class TestClassifyProviderTestError:
"""Provider test: only bad creds / unreachable endpoint fail."""
def test_retired_test_model_is_success(self):
# The core #970 regression: a shut-down test model must report the
# key as valid, not the connection as broken.
success, message = classify_provider_test_error(GOOGLE_RETIRED_MODEL_404)
assert success is True
assert "test model unavailable" in message
def test_deprecated_model_is_success(self):
success, _ = classify_provider_test_error(GOOGLE_DEPRECATED)
assert success is True
def test_rate_limit_is_success(self):
success, message = classify_provider_test_error(GOOGLE_QUOTA_429)
assert success is True
assert "connection works" in message
@pytest.mark.parametrize(
"msg,expected_fragment",
[
(GOOGLE_BAD_KEY_401, "Invalid API key"),
(GOOGLE_PERM_403, "lacks required permissions"),
(DNS_FAILURE, "Connection error"),
(TIMEOUT, "timed out"),
],
)
def test_auth_and_network_still_fail(self, msg, expected_fragment):
success, message = classify_provider_test_error(msg)
assert success is False
assert expected_fragment in message
def test_unrecognized_error_stays_a_failure(self):
# A construction/config error we can't attribute to the provider is
# surfaced, not silently reported as success.
success, message = classify_provider_test_error(
"TypeError: create_language() missing required config 'base_url'"
)
assert success is False
assert "Error:" in message
class TestIndividualModelSemanticsDiffer:
"""The SAME model-not-found string is failure for the individual-model
test but success for the provider test — the intended difference."""
def test_model_not_found_is_failure_for_individual_test(self):
success, message = _normalize_error_message(GOOGLE_RETIRED_MODEL_404)
assert success is False
assert "Model not found" in message
def test_same_string_is_success_for_provider_test(self):
success, _ = classify_provider_test_error(GOOGLE_RETIRED_MODEL_404)
assert success is True
def test_individual_test_shares_auth_classification(self):
success, message = _normalize_error_message(GOOGLE_BAD_KEY_401)
assert success is False
assert "Invalid API key" in message