chore: import upstream snapshot with attribution
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Has been cancelled
Backend release / release (push) Has been cancelled
Bandit Security Scan / bandit_scan (push) Has been cancelled
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Has been cancelled
Build and push multi-arch DocsGPT Docker image / manifest (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / manifest (push) Has been cancelled
Python linting / ruff (push) Has been cancelled
Run python tests with pytest / Run tests and count coverage (3.12) (push) Has been cancelled
React Widget Build / build (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:28:29 +08:00
commit fed8b2eed7
1531 changed files with 1107494 additions and 0 deletions
View File
File diff suppressed because it is too large Load Diff
+144
View File
@@ -0,0 +1,144 @@
"""Tests for ``application.core.db_uri``.
DocsGPT has two Postgres connection strings — ``POSTGRES_URI`` (consumed
by SQLAlchemy) and ``PGVECTOR_CONNECTION_STRING`` (consumed by
``psycopg.connect()`` directly). They need opposite normalization
because SQLAlchemy requires a ``postgresql+psycopg://`` dialect prefix
and libpq rejects it. Each field has its own normalizer so operators
can write whichever form feels natural and cross-pollination between
the two fields is forgiven.
The normalizers live in ``application.core.db_uri`` as plain functions
so these tests can exercise them directly without having to instantiate
``Settings`` (which would pull in ``.env`` file side effects).
"""
from __future__ import annotations
import pytest
from application.core.db_uri import (
normalize_pgvector_connection_string,
normalize_postgres_uri,
)
@pytest.mark.unit
class TestNormalizePostgresUri:
@pytest.mark.parametrize(
"input_value,expected",
[
# User-friendly forms get rewritten to the SQLAlchemy dialect.
(
"postgres://u:p@h:5432/d",
"postgresql+psycopg://u:p@h:5432/d",
),
(
"postgresql://u:p@h:5432/d",
"postgresql+psycopg://u:p@h:5432/d",
),
# Legacy psycopg2 dialect is silently upgraded — psycopg2 is
# no longer in requirements.txt, so there's no way it can work
# as-is, and rewriting is friendlier than failing.
(
"postgresql+psycopg2://u:p@h:5432/d",
"postgresql+psycopg://u:p@h:5432/d",
),
# Already-correct dialect passes through unchanged.
(
"postgresql+psycopg://u:p@h:5432/d",
"postgresql+psycopg://u:p@h:5432/d",
),
# Whitespace is trimmed before rewriting.
(
" postgres://u:p@h/d ",
"postgresql+psycopg://u:p@h/d",
),
# Query-string params (sslmode, options) are preserved verbatim.
(
"postgresql://u:p@h:5432/d?sslmode=require&application_name=docsgpt",
"postgresql+psycopg://u:p@h:5432/d?sslmode=require&application_name=docsgpt",
),
],
)
def test_rewrites_common_forms_to_psycopg_dialect(self, input_value, expected):
assert normalize_postgres_uri(input_value) == expected
@pytest.mark.parametrize(
"input_value",
[None, "", " ", "None", "none"],
)
def test_empty_or_none_like_returns_none(self, input_value):
assert normalize_postgres_uri(input_value) is None
def test_unknown_scheme_passes_through(self):
"""A dialect we don't recognise is left alone so SQLAlchemy can
produce its own error message when the engine tries to connect.
Better than silently eating the config."""
weird = "postgresql+asyncpg://u:p@h/d"
assert normalize_postgres_uri(weird) == weird
def test_non_string_input_passes_through(self):
"""Non-string inputs (e.g. if pydantic ever passes an int) shouldn't
crash the normalizer — let pydantic's own type validation handle it."""
assert normalize_postgres_uri(42) == 42 # type: ignore[arg-type]
@pytest.mark.unit
class TestNormalizePgvectorConnectionString:
"""Symmetric to the POSTGRES_URI normalizer but pulls in the OPPOSITE
direction: strips the SQLAlchemy dialect prefix so libpq accepts it.
"""
@pytest.mark.parametrize(
"input_value,expected",
[
# User-friendly forms pass through — libpq accepts them natively.
(
"postgres://u:p@h:5432/d",
"postgres://u:p@h:5432/d",
),
(
"postgresql://u:p@h:5432/d",
"postgresql://u:p@h:5432/d",
),
# SQLAlchemy dialect prefixes get stripped so libpq accepts them.
# Operators hit this when they copy POSTGRES_URI → PGVECTOR_CONNECTION_STRING.
(
"postgresql+psycopg://u:p@h:5432/d",
"postgresql://u:p@h:5432/d",
),
(
"postgresql+psycopg2://u:p@h:5432/d",
"postgresql://u:p@h:5432/d",
),
# Whitespace is trimmed before rewriting.
(
" postgresql+psycopg://u:p@h/d ",
"postgresql://u:p@h/d",
),
# Query-string params (sslmode, etc.) are preserved verbatim.
(
"postgresql+psycopg://u:p@h:5432/d?sslmode=require",
"postgresql://u:p@h:5432/d?sslmode=require",
),
],
)
def test_rewrites_dialect_forms_to_libpq_compatible(self, input_value, expected):
assert normalize_pgvector_connection_string(input_value) == expected
@pytest.mark.parametrize(
"input_value",
[None, "", " ", "None", "none"],
)
def test_empty_or_none_like_returns_none(self, input_value):
assert normalize_pgvector_connection_string(input_value) is None
def test_unknown_scheme_passes_through(self):
"""A scheme we don't recognise is left alone so libpq can produce
its own error message when the connection is attempted."""
weird = "mysql://u:p@h/d"
assert normalize_pgvector_connection_string(weird) == weird
def test_non_string_input_passes_through(self):
assert normalize_pgvector_connection_string(42) == 42 # type: ignore[arg-type]
+48
View File
@@ -0,0 +1,48 @@
"""Unit tests for the bounded-drain gunicorn worker."""
from unittest import mock
import pytest
from application.core import shutdown
from application.core.settings import settings
from application.gunicorn_worker import (
BoundedDrainUvicornWorker,
_ShutdownAwareServer,
)
@pytest.fixture(autouse=True)
def _reset_flag():
shutdown.reset_shutdown()
yield
shutdown.reset_shutdown()
@pytest.mark.unit
def test_worker_bounds_graceful_shutdown():
cfg = BoundedDrainUvicornWorker.CONFIG_KWARGS
assert cfg["timeout_graceful_shutdown"] == settings.GRACEFUL_SHUTDOWN_TIMEOUT_SECONDS
assert isinstance(cfg["timeout_graceful_shutdown"], int)
# Must sit under the gunicorn --timeout (180) or the watchdog fires first.
assert 0 < cfg["timeout_graceful_shutdown"] < 180
@pytest.mark.unit
@pytest.mark.asyncio
async def test_server_shutdown_raises_flag():
from uvicorn.config import Config
async def _dummy_asgi(scope, receive, send): # pragma: no cover - never called
pass
server = _ShutdownAwareServer(Config(app=_dummy_asgi))
assert shutdown.is_shutting_down() is False
# Stub the heavy uvicorn drain so the test only exercises our override.
with mock.patch("uvicorn.server.Server.shutdown", new=mock.AsyncMock()) as sup:
await server.shutdown()
# The flag is raised before delegating to uvicorn's real shutdown.
assert shutdown.is_shutting_down() is True
sup.assert_awaited_once()
+161
View File
@@ -0,0 +1,161 @@
"""Unit tests for ``log_context`` + ``_ContextFilter``."""
from __future__ import annotations
import logging
import pytest
from application.core import log_context
from application.core.logging_config import _ContextFilter
@pytest.fixture(autouse=True)
def _clean_log_ctx():
# The contextvar is module-scoped; snapshot at entry, restore at exit
# to keep tests from leaking state into each other.
token = log_context.bind()
try:
yield
finally:
log_context.reset(token)
@pytest.mark.unit
class TestBindAndSnapshot:
def test_bind_returns_token_and_snapshot_reflects_overlay(self):
token = log_context.bind(activity_id="a1", user_id="u1")
assert log_context.snapshot() == {"activity_id": "a1", "user_id": "u1"}
log_context.reset(token)
assert log_context.snapshot() == {}
def test_bind_drops_unknown_keys(self):
token = log_context.bind(activity_id="a1", not_a_real_key="boom")
try:
assert log_context.snapshot() == {"activity_id": "a1"}
finally:
log_context.reset(token)
def test_bind_drops_none_values(self):
token = log_context.bind(activity_id="a1", agent_id=None)
try:
assert "agent_id" not in log_context.snapshot()
finally:
log_context.reset(token)
def test_bind_coerces_values_to_str(self):
token = log_context.bind(activity_id=42)
try:
assert log_context.snapshot()["activity_id"] == "42"
finally:
log_context.reset(token)
def test_nested_bind_overlays_and_resets_lifo(self):
outer = log_context.bind(activity_id="outer", user_id="u1")
inner = log_context.bind(activity_id="inner", agent_id="agent-1")
# Inner overrides activity_id, keeps user_id from outer, adds agent_id.
assert log_context.snapshot() == {
"activity_id": "inner",
"user_id": "u1",
"agent_id": "agent-1",
}
log_context.reset(inner)
assert log_context.snapshot() == {"activity_id": "outer", "user_id": "u1"}
log_context.reset(outer)
assert log_context.snapshot() == {}
def test_parent_activity_id_pattern(self):
outer = log_context.bind(activity_id="parent-1")
parent = log_context.snapshot().get("activity_id")
inner = log_context.bind(activity_id="child-1", parent_activity_id=parent)
try:
snap = log_context.snapshot()
assert snap["activity_id"] == "child-1"
assert snap["parent_activity_id"] == "parent-1"
finally:
log_context.reset(inner)
log_context.reset(outer)
@pytest.mark.unit
class TestContextFilter:
def _make_record(self, **extra) -> logging.LogRecord:
record = logging.LogRecord(
name="test",
level=logging.INFO,
pathname=__file__,
lineno=1,
msg="hello",
args=(),
exc_info=None,
)
for k, v in extra.items():
setattr(record, k, v)
return record
def test_stamps_record_with_context(self):
token = log_context.bind(activity_id="a1", user_id="u1")
try:
record = self._make_record()
assert _ContextFilter().filter(record) is True
assert record.activity_id == "a1"
assert record.user_id == "u1"
finally:
log_context.reset(token)
def test_explicit_extra_wins_over_context(self):
# extra={} sets attributes on the record before the filter runs;
# the filter must not overwrite them.
token = log_context.bind(activity_id="from-ctx")
try:
record = self._make_record(activity_id="from-extra")
_ContextFilter().filter(record)
assert record.activity_id == "from-extra"
finally:
log_context.reset(token)
def test_no_op_when_context_empty(self):
record = self._make_record()
assert _ContextFilter().filter(record) is True
assert not hasattr(record, "activity_id")
@pytest.mark.unit
class TestFilterWiringEndToEnd:
"""Regression guard: the filter must be installed on handlers, not on
loggers — Python skips logger-level filters during propagation.
"""
def test_propagated_record_gets_stamped(self):
from application.core.logging_config import _install_context_filter
captured: list[logging.LogRecord] = []
class _Capture(logging.Handler):
def emit(self, record):
captured.append(record)
root = logging.getLogger()
saved_handlers = list(root.handlers)
saved_level = root.level
try:
root.handlers = [_Capture()]
root.setLevel(logging.DEBUG)
_install_context_filter()
child = logging.getLogger("test_log_context.propagation")
child.setLevel(logging.DEBUG)
token = log_context.bind(activity_id="propagated-id")
try:
child.info("from a child logger")
finally:
log_context.reset(token)
assert captured, "Capture handler should have received the record"
assert captured[0].activity_id == "propagated-id"
finally:
root.handlers = saved_handlers
root.setLevel(saved_level)
+104
View File
@@ -0,0 +1,104 @@
"""Tests for setup_logging — in particular the OTEL log-handler hand-off.
`opentelemetry-instrument` attaches an OTEL `LoggingHandler` to the root
logger before our module-level `setup_logging()` runs in `application/app.py`.
The default `dictConfig` call replaces `root.handlers`, which would silently
drop the OTEL handler. setup_logging snapshots and re-attaches OTEL handlers
when OTLP log export is enabled.
"""
from __future__ import annotations
import logging
import sys
import types
import pytest
from application.core.logging_config import setup_logging
@pytest.fixture(autouse=True)
def _reset_root_logger():
"""Snapshot/restore the root logger so tests don't leak handlers."""
root = logging.getLogger()
saved_handlers = list(root.handlers)
saved_level = root.level
yield
root.handlers = saved_handlers
root.setLevel(saved_level)
def _make_fake_otel_handler() -> logging.Handler:
"""Build a Handler whose class lives in a module starting with 'opentelemetry'.
Mirrors how the real `opentelemetry.sdk._logs.LoggingHandler` would be
detected without needing the OTEL SDK installed in the test env.
"""
fake_module = types.ModuleType("opentelemetry.fake_sdk._logs")
sys.modules.setdefault(fake_module.__name__, fake_module)
class FakeOtelHandler(logging.Handler):
pass
FakeOtelHandler.__module__ = fake_module.__name__
return FakeOtelHandler()
@pytest.mark.unit
class TestSetupLogging:
def test_default_keeps_only_console_handler(self, monkeypatch):
monkeypatch.delenv("OTEL_LOGS_EXPORTER", raising=False)
monkeypatch.delenv("OTEL_SDK_DISABLED", raising=False)
logging.getLogger().handlers = []
setup_logging()
handlers = logging.getLogger().handlers
assert len(handlers) == 1
assert isinstance(handlers[0], logging.StreamHandler)
def test_preserves_otel_handler_when_otlp_logs_enabled(self, monkeypatch):
monkeypatch.setenv("OTEL_LOGS_EXPORTER", "otlp")
monkeypatch.delenv("OTEL_SDK_DISABLED", raising=False)
otel_handler = _make_fake_otel_handler()
logging.getLogger().handlers = [otel_handler]
setup_logging()
handlers = logging.getLogger().handlers
assert otel_handler in handlers, (
"OTEL handler must survive setup_logging when OTLP log export is on"
)
assert any(
isinstance(h, logging.StreamHandler) and not isinstance(h, type(otel_handler))
for h in handlers
), "console handler should still be installed alongside the OTEL handler"
def test_does_not_preserve_when_sdk_disabled(self, monkeypatch):
monkeypatch.setenv("OTEL_LOGS_EXPORTER", "otlp")
monkeypatch.setenv("OTEL_SDK_DISABLED", "true")
otel_handler = _make_fake_otel_handler()
logging.getLogger().handlers = [otel_handler]
setup_logging()
handlers = logging.getLogger().handlers
assert otel_handler not in handlers, (
"When OTEL_SDK_DISABLED=true the handler should not be preserved"
)
def test_does_not_preserve_when_logs_exporter_unset(self, monkeypatch):
monkeypatch.delenv("OTEL_LOGS_EXPORTER", raising=False)
monkeypatch.delenv("OTEL_SDK_DISABLED", raising=False)
otel_handler = _make_fake_otel_handler()
logging.getLogger().handlers = [otel_handler]
setup_logging()
handlers = logging.getLogger().handlers
assert otel_handler not in handlers
+295
View File
@@ -0,0 +1,295 @@
"""Regression tests for the YAML-driven ModelRegistry.
These tests encode the contract that persisted agent / workflow /
conversation references depend on: every model id and core capability
that existed in the old ``model_configs.py`` lists must continue to be
produced by the new YAML-backed registry.
If a future YAML edit accidentally renames an id or changes a
capability, these tests fail at CI before merge — protecting agents and
workflows from silent fallback to the system default.
"""
from __future__ import annotations
from unittest.mock import MagicMock, patch
import pytest
from application.core.model_registry import ModelRegistry
from application.core.model_yaml import (
BUILTIN_MODELS_DIR,
load_model_yamls,
)
# ── Per-provider expected IDs ─────────────────────────────────────────────
# Snapshot of the current built-in catalog. If you intentionally change
# what models a provider's YAML lists, update this constant in the same
# commit. The test exists to catch *unintentional* renames (e.g. a typo
# in an upstream model id) that would silently break every agent that
# references the old id.
EXPECTED_IDS = {
"openai": {"gpt-5.5", "gpt-5.4-mini", "gpt-5.4-nano"},
"anthropic": {
"claude-opus-4-7",
"claude-sonnet-4-6",
"claude-haiku-4-5",
},
"google": {
"gemini-3.1-pro-preview",
"gemini-3.5-flash",
"gemini-3.1-flash-lite",
},
"groq": {
"openai/gpt-oss-120b",
"llama-3.3-70b-versatile",
"llama-3.1-8b-instant",
},
"openrouter": {
"qwen/qwen3-coder:free",
"deepseek/deepseek-v3.2",
"anthropic/claude-sonnet-4.6",
},
"novita": {
"deepseek/deepseek-v4-pro",
"moonshotai/kimi-k2.6",
"zai-org/glm-5",
},
"openai_compatible": {
"deepseek-v4-flash",
"deepseek-v4-pro",
},
"docsgpt": {"docsgpt-local"},
"huggingface": {"huggingface-local"},
}
def _make_settings(**overrides):
s = MagicMock()
# All credential / mode flags off by default so each test opts in.
s.OPENAI_BASE_URL = None
s.OPENAI_API_KEY = None
s.OPENAI_API_BASE = None
s.ANTHROPIC_API_KEY = None
s.GOOGLE_API_KEY = None
s.GROQ_API_KEY = None
s.OPEN_ROUTER_API_KEY = None
s.NOVITA_API_KEY = None
s.HUGGINGFACE_API_KEY = None
s.LLM_PROVIDER = ""
s.LLM_NAME = None
s.API_KEY = None
s.MODELS_CONFIG_DIR = None
for k, v in overrides.items():
setattr(s, k, v)
return s
@pytest.fixture(autouse=True)
def _reset_registry(monkeypatch):
ModelRegistry.reset()
# openai_compatible catalogs read their key directly from os.environ,
# so clear any host-set keys to keep these tests deterministic.
monkeypatch.delenv("DEEPSEEK_API_KEY", raising=False)
yield
ModelRegistry.reset()
# ── YAML schema / loader ─────────────────────────────────────────────────
def _by_provider(catalogs):
"""Group a list of catalogs by provider name. Mirrors the registry's
own grouping; useful for asserting per-provider model sets in tests."""
out = {}
for c in catalogs:
out.setdefault(c.provider, []).append(c)
return out
@pytest.mark.unit
class TestYAMLLoader:
def test_loader_produces_expected_provider_set(self):
catalogs = load_model_yamls([BUILTIN_MODELS_DIR])
providers = {c.provider for c in catalogs}
assert providers == set(EXPECTED_IDS.keys())
def test_each_provider_has_expected_ids(self):
grouped = _by_provider(load_model_yamls([BUILTIN_MODELS_DIR]))
for provider, expected in EXPECTED_IDS.items():
actual = {m.id for c in grouped[provider] for m in c.models}
assert actual == expected, f"{provider}: expected {expected}, got {actual}"
def test_attachment_alias_image_expands_to_five_mime_types(self):
grouped = _by_provider(load_model_yamls([BUILTIN_MODELS_DIR]))
# OpenAI uses `attachments: [image]` in its defaults block.
for c in grouped["openai"]:
for m in c.models:
assert "image/png" in m.capabilities.supported_attachment_types
assert "image/jpeg" in m.capabilities.supported_attachment_types
assert "image/webp" in m.capabilities.supported_attachment_types
assert len(m.capabilities.supported_attachment_types) == 5
def test_attachment_alias_pdf_plus_image_for_google(self):
grouped = _by_provider(load_model_yamls([BUILTIN_MODELS_DIR]))
for c in grouped["google"]:
for m in c.models:
assert "application/pdf" in m.capabilities.supported_attachment_types
assert "image/png" in m.capabilities.supported_attachment_types
assert len(m.capabilities.supported_attachment_types) == 6
def test_per_model_context_window_overrides_provider_default(self):
grouped = _by_provider(load_model_yamls([BUILTIN_MODELS_DIR]))
openai = {m.id: m for c in grouped["openai"] for m in c.models}
# Provider default is 400_000; gpt-5.5 overrides to 1_050_000.
assert openai["gpt-5.4-mini"].capabilities.context_window == 400_000
assert openai["gpt-5.5"].capabilities.context_window == 1_050_000
# ── Registry × settings: every documented .env permutation ───────────────
@pytest.mark.unit
class TestRegistryPermutations:
def test_openai_only(self):
s = _make_settings(OPENAI_API_KEY="sk-test", LLM_PROVIDER="openai")
with patch("application.core.settings.settings", s):
reg = ModelRegistry()
ids = {m.id for m in reg.get_all_models()}
assert ids == EXPECTED_IDS["openai"] | EXPECTED_IDS["docsgpt"]
def test_openai_base_url_replaces_catalog_with_dynamic(self):
s = _make_settings(
OPENAI_BASE_URL="http://localhost:11434/v1",
OPENAI_API_KEY="sk-test",
LLM_PROVIDER="openai",
LLM_NAME="llama3,gemma",
)
with patch("application.core.settings.settings", s):
reg = ModelRegistry()
ids = {m.id for m in reg.get_all_models()}
# Custom local endpoint suppresses both the openai catalog AND
# the docsgpt model (matching legacy behavior).
assert ids == {"llama3", "gemma"}
def test_anthropic_only(self):
s = _make_settings(ANTHROPIC_API_KEY="sk-ant")
with patch("application.core.settings.settings", s):
reg = ModelRegistry()
ids = {m.id for m in reg.get_all_models()}
assert ids == EXPECTED_IDS["anthropic"] | EXPECTED_IDS["docsgpt"]
def test_anthropic_via_llm_provider_with_llm_name(self):
# Mirrors the historical _add_anthropic_models filter: when only
# API_KEY (not ANTHROPIC_API_KEY) is set and LLM_NAME matches a
# known model, only that model is loaded.
s = _make_settings(
LLM_PROVIDER="anthropic", API_KEY="key", LLM_NAME="claude-haiku-4-5"
)
with patch("application.core.settings.settings", s):
reg = ModelRegistry()
anthropic_ids = {
m.id for m in reg.get_all_models() if m.provider.value == "anthropic"
}
assert anthropic_ids == {"claude-haiku-4-5"}
def test_google_only(self):
s = _make_settings(GOOGLE_API_KEY="g-test")
with patch("application.core.settings.settings", s):
reg = ModelRegistry()
ids = {m.id for m in reg.get_all_models()}
assert ids == EXPECTED_IDS["google"] | EXPECTED_IDS["docsgpt"]
def test_groq_only(self):
s = _make_settings(GROQ_API_KEY="g-test")
with patch("application.core.settings.settings", s):
reg = ModelRegistry()
ids = {m.id for m in reg.get_all_models()}
assert ids == EXPECTED_IDS["groq"] | EXPECTED_IDS["docsgpt"]
def test_openrouter_only(self):
s = _make_settings(OPEN_ROUTER_API_KEY="or-test")
with patch("application.core.settings.settings", s):
reg = ModelRegistry()
ids = {m.id for m in reg.get_all_models()}
assert ids == EXPECTED_IDS["openrouter"] | EXPECTED_IDS["docsgpt"]
def test_novita_only(self):
s = _make_settings(NOVITA_API_KEY="n-test")
with patch("application.core.settings.settings", s):
reg = ModelRegistry()
ids = {m.id for m in reg.get_all_models()}
assert ids == EXPECTED_IDS["novita"] | EXPECTED_IDS["docsgpt"]
def test_huggingface_only(self):
s = _make_settings(HUGGINGFACE_API_KEY="hf-test")
with patch("application.core.settings.settings", s):
reg = ModelRegistry()
ids = {m.id for m in reg.get_all_models()}
assert ids == EXPECTED_IDS["huggingface"] | EXPECTED_IDS["docsgpt"]
def test_no_credentials_only_docsgpt(self):
s = _make_settings()
with patch("application.core.settings.settings", s):
reg = ModelRegistry()
ids = {m.id for m in reg.get_all_models()}
assert ids == EXPECTED_IDS["docsgpt"]
def test_everything_set(self, monkeypatch):
monkeypatch.setenv("DEEPSEEK_API_KEY", "x")
s = _make_settings(
OPENAI_API_KEY="x",
ANTHROPIC_API_KEY="x",
GOOGLE_API_KEY="x",
GROQ_API_KEY="x",
OPEN_ROUTER_API_KEY="x",
NOVITA_API_KEY="x",
HUGGINGFACE_API_KEY="x",
OPENAI_API_BASE="x",
)
with patch("application.core.settings.settings", s):
reg = ModelRegistry()
ids = {m.id for m in reg.get_all_models()}
all_expected = set()
for v in EXPECTED_IDS.values():
all_expected |= v
assert ids == all_expected
# ── Default model resolution ─────────────────────────────────────────────
@pytest.mark.unit
class TestDefaultModelResolution:
def test_llm_name_picks_default(self):
s = _make_settings(
ANTHROPIC_API_KEY="sk-ant", LLM_NAME="claude-opus-4-7"
)
with patch("application.core.settings.settings", s):
reg = ModelRegistry()
assert reg.default_model_id == "claude-opus-4-7"
def test_falls_back_to_first_model_when_no_match(self):
s = _make_settings()
with patch("application.core.settings.settings", s):
reg = ModelRegistry()
assert reg.default_model_id is not None
assert reg.default_model_id in reg.models
# ── Forward-compat: user_id parameter is accepted everywhere ─────────────
@pytest.mark.unit
class TestUserIdForwardCompat:
def test_lookup_methods_accept_user_id(self):
s = _make_settings(OPENAI_API_KEY="sk-test")
with patch("application.core.settings.settings", s):
reg = ModelRegistry()
# All lookup methods must accept user_id (currently ignored,
# reserved for end-user BYOM).
assert reg.get_model("gpt-5.5", user_id="alice") is not None
assert len(reg.get_all_models(user_id="alice")) > 0
assert len(reg.get_enabled_models(user_id="alice")) > 0
assert reg.model_exists("gpt-5.5", user_id="alice") is True
+235
View File
@@ -0,0 +1,235 @@
"""Tests for application/core/model_settings.py.
The provider-specific load logic that used to live in private
``_add_<X>_models`` methods now lives in plugin classes under
``application/llm/providers/`` and YAML catalogs under
``application/core/models/``. End-to-end coverage of the registry +
plugin pipeline is in ``tests/core/test_model_registry_yaml.py``.
This file covers the data classes (``AvailableModel``,
``ModelCapabilities``, ``ModelProvider``) and the singleton/lookup
contract on ``ModelRegistry``.
"""
from unittest.mock import patch
import pytest
from application.core.model_settings import (
AvailableModel,
ModelCapabilities,
ModelProvider,
ModelRegistry,
)
class TestModelProvider:
@pytest.mark.unit
def test_all_providers_exist(self):
assert ModelProvider.OPENAI == "openai"
assert ModelProvider.ANTHROPIC == "anthropic"
assert ModelProvider.GOOGLE == "google"
assert ModelProvider.GROQ == "groq"
assert ModelProvider.DOCSGPT == "docsgpt"
assert ModelProvider.HUGGINGFACE == "huggingface"
assert ModelProvider.NOVITA == "novita"
assert ModelProvider.OPENROUTER == "openrouter"
assert ModelProvider.SAGEMAKER == "sagemaker"
assert ModelProvider.PREMAI == "premai"
assert ModelProvider.LLAMA_CPP == "llama.cpp"
class TestModelCapabilities:
@pytest.mark.unit
def test_defaults(self):
caps = ModelCapabilities()
assert caps.supports_tools is False
assert caps.supports_structured_output is False
assert caps.supports_streaming is True
assert caps.supported_attachment_types == []
assert caps.context_window == 128000
assert caps.input_cost_per_token is None
assert caps.output_cost_per_token is None
@pytest.mark.unit
def test_custom_values(self):
caps = ModelCapabilities(
supports_tools=True,
supports_structured_output=True,
context_window=32000,
input_cost_per_token=0.001,
)
assert caps.supports_tools is True
assert caps.context_window == 32000
class TestAvailableModel:
@pytest.mark.unit
def test_to_dict_basic(self):
model = AvailableModel(
id="gpt-4",
provider=ModelProvider.OPENAI,
display_name="GPT-4",
description="OpenAI GPT-4",
)
d = model.to_dict()
assert d["id"] == "gpt-4"
assert d["provider"] == "openai"
assert d["display_name"] == "GPT-4"
assert d["enabled"] is True
assert "base_url" not in d
@pytest.mark.unit
def test_to_dict_with_base_url(self):
model = AvailableModel(
id="local-model",
provider=ModelProvider.OPENAI,
display_name="Local",
base_url="http://localhost:11434/v1",
)
d = model.to_dict()
assert d["base_url"] == "http://localhost:11434/v1"
@pytest.mark.unit
def test_to_dict_includes_capabilities(self):
caps = ModelCapabilities(
supports_tools=True,
supports_structured_output=True,
context_window=200000,
supported_attachment_types=["image/png"],
)
model = AvailableModel(
id="m",
provider=ModelProvider.OPENAI,
display_name="M",
capabilities=caps,
)
d = model.to_dict()
assert d["supports_tools"] is True
assert d["supports_structured_output"] is True
assert d["context_window"] == 200000
assert d["supported_attachment_types"] == ["image/png"]
@pytest.mark.unit
def test_to_dict_disabled_model(self):
model = AvailableModel(
id="disabled",
provider=ModelProvider.OPENAI,
display_name="Disabled",
enabled=False,
)
d = model.to_dict()
assert d["enabled"] is False
@pytest.mark.unit
def test_api_key_field_never_serialized(self):
"""Forward-compat hook: AvailableModel.api_key (reserved for the
future end-user BYOM phase) must never leak into the wire format."""
model = AvailableModel(
id="byom",
provider=ModelProvider.OPENAI,
display_name="BYOM",
api_key="secret-key-do-not-leak",
)
d = model.to_dict()
assert "api_key" not in d
for v in d.values():
assert v != "secret-key-do-not-leak"
class TestModelRegistryPublicAPI:
"""Covers the public lookup contract. Loading behavior is exercised
end-to-end in tests/core/test_model_registry_yaml.py."""
@pytest.fixture(autouse=True)
def _reset_singleton(self):
ModelRegistry.reset()
yield
ModelRegistry.reset()
@pytest.mark.unit
def test_singleton(self):
with patch.object(ModelRegistry, "_load_models"):
r1 = ModelRegistry()
r2 = ModelRegistry()
assert r1 is r2
@pytest.mark.unit
def test_get_instance(self):
with patch.object(ModelRegistry, "_load_models"):
r = ModelRegistry.get_instance()
assert isinstance(r, ModelRegistry)
@pytest.mark.unit
def test_get_model(self):
with patch.object(ModelRegistry, "_load_models"):
reg = ModelRegistry()
model = AvailableModel(
id="test", provider=ModelProvider.OPENAI, display_name="Test"
)
reg.models["test"] = model
assert reg.get_model("test") is model
assert reg.get_model("nonexistent") is None
@pytest.mark.unit
def test_get_all_models(self):
with patch.object(ModelRegistry, "_load_models"):
reg = ModelRegistry()
reg.models["m1"] = AvailableModel(
id="m1", provider=ModelProvider.OPENAI, display_name="M1"
)
reg.models["m2"] = AvailableModel(
id="m2", provider=ModelProvider.ANTHROPIC, display_name="M2"
)
assert len(reg.get_all_models()) == 2
@pytest.mark.unit
def test_get_enabled_models(self):
with patch.object(ModelRegistry, "_load_models"):
reg = ModelRegistry()
reg.models["m1"] = AvailableModel(
id="m1",
provider=ModelProvider.OPENAI,
display_name="M1",
enabled=True,
)
reg.models["m2"] = AvailableModel(
id="m2",
provider=ModelProvider.OPENAI,
display_name="M2",
enabled=False,
)
enabled = reg.get_enabled_models()
assert len(enabled) == 1
assert enabled[0].id == "m1"
@pytest.mark.unit
def test_model_exists(self):
with patch.object(ModelRegistry, "_load_models"):
reg = ModelRegistry()
reg.models["m1"] = AvailableModel(
id="m1", provider=ModelProvider.OPENAI, display_name="M1"
)
assert reg.model_exists("m1") is True
assert reg.model_exists("m2") is False
@pytest.mark.unit
def test_lookups_accept_user_id_kwarg(self):
"""Reserved for the future end-user BYOM phase. Currently ignored."""
with patch.object(ModelRegistry, "_load_models"):
reg = ModelRegistry()
reg.models["m1"] = AvailableModel(
id="m1", provider=ModelProvider.OPENAI, display_name="M1"
)
assert reg.get_model("m1", user_id="alice") is not None
assert reg.model_exists("m1", user_id="alice") is True
assert len(reg.get_all_models(user_id="alice")) == 1
assert len(reg.get_enabled_models(user_id="alice")) == 1
@pytest.mark.unit
def test_reset(self):
with patch.object(ModelRegistry, "_load_models"):
r1 = ModelRegistry()
ModelRegistry.reset()
r2 = ModelRegistry()
assert r1 is not r2
+372
View File
@@ -0,0 +1,372 @@
from unittest.mock import MagicMock, patch
import pytest
from application.core.model_settings import (
AvailableModel,
ModelCapabilities,
ModelProvider,
ModelRegistry,
)
@pytest.fixture(autouse=True)
def _reset_registry():
"""Reset ModelRegistry singleton between tests."""
ModelRegistry._instance = None
ModelRegistry._initialized = False
yield
ModelRegistry._instance = None
ModelRegistry._initialized = False
def _make_model(
model_id="test-model",
provider=ModelProvider.OPENAI,
display_name="Test Model",
context_window=128000,
supports_tools=True,
supports_structured_output=False,
supported_attachment_types=None,
enabled=True,
base_url=None,
):
return AvailableModel(
id=model_id,
provider=provider,
display_name=display_name,
capabilities=ModelCapabilities(
supports_tools=supports_tools,
supports_structured_output=supports_structured_output,
supported_attachment_types=supported_attachment_types or [],
context_window=context_window,
),
enabled=enabled,
base_url=base_url,
)
# ── get_api_key_for_provider ─────────────────────────────────────────────────
class TestGetApiKeyForProvider:
"""settings is lazily imported inside the function body, so we patch
at application.core.settings.settings (the actual module attribute)."""
@pytest.mark.unit
def test_openai_key(self):
with patch("application.core.settings.settings") as mock_settings:
mock_settings.OPENAI_API_KEY = "sk-openai"
mock_settings.API_KEY = "sk-fallback"
mock_settings.OPEN_ROUTER_API_KEY = None
mock_settings.NOVITA_API_KEY = None
mock_settings.ANTHROPIC_API_KEY = None
mock_settings.GOOGLE_API_KEY = None
mock_settings.GROQ_API_KEY = None
mock_settings.HUGGINGFACE_API_KEY = None
from application.core.model_utils import get_api_key_for_provider
assert get_api_key_for_provider("openai") == "sk-openai"
@pytest.mark.unit
def test_anthropic_key(self):
with patch("application.core.settings.settings") as mock_settings:
mock_settings.ANTHROPIC_API_KEY = "sk-anthropic"
mock_settings.API_KEY = "sk-fallback"
from application.core.model_utils import get_api_key_for_provider
assert get_api_key_for_provider("anthropic") == "sk-anthropic"
@pytest.mark.unit
def test_google_key(self):
with patch("application.core.settings.settings") as mock_settings:
mock_settings.GOOGLE_API_KEY = "sk-google"
mock_settings.API_KEY = "sk-fallback"
from application.core.model_utils import get_api_key_for_provider
assert get_api_key_for_provider("google") == "sk-google"
@pytest.mark.unit
def test_groq_key(self):
with patch("application.core.settings.settings") as mock_settings:
mock_settings.GROQ_API_KEY = "sk-groq"
mock_settings.API_KEY = "sk-fallback"
from application.core.model_utils import get_api_key_for_provider
assert get_api_key_for_provider("groq") == "sk-groq"
@pytest.mark.unit
def test_openrouter_key(self):
with patch("application.core.settings.settings") as mock_settings:
mock_settings.OPEN_ROUTER_API_KEY = "sk-or"
mock_settings.API_KEY = "sk-fallback"
from application.core.model_utils import get_api_key_for_provider
assert get_api_key_for_provider("openrouter") == "sk-or"
@pytest.mark.unit
def test_novita_key(self):
with patch("application.core.settings.settings") as mock_settings:
mock_settings.NOVITA_API_KEY = "sk-novita"
mock_settings.API_KEY = "sk-fallback"
from application.core.model_utils import get_api_key_for_provider
assert get_api_key_for_provider("novita") == "sk-novita"
@pytest.mark.unit
def test_huggingface_key(self):
with patch("application.core.settings.settings") as mock_settings:
mock_settings.HUGGINGFACE_API_KEY = "hf-key"
mock_settings.API_KEY = "sk-fallback"
from application.core.model_utils import get_api_key_for_provider
assert get_api_key_for_provider("huggingface") == "hf-key"
@pytest.mark.unit
def test_docsgpt_returns_fallback(self):
with patch("application.core.settings.settings") as mock_settings:
mock_settings.API_KEY = "sk-fallback"
from application.core.model_utils import get_api_key_for_provider
assert get_api_key_for_provider("docsgpt") == "sk-fallback"
@pytest.mark.unit
def test_llama_cpp_returns_fallback(self):
with patch("application.core.settings.settings") as mock_settings:
mock_settings.API_KEY = "sk-fallback"
from application.core.model_utils import get_api_key_for_provider
assert get_api_key_for_provider("llama.cpp") == "sk-fallback"
@pytest.mark.unit
def test_unknown_provider_returns_fallback(self):
with patch("application.core.settings.settings") as mock_settings:
mock_settings.API_KEY = "sk-fallback"
from application.core.model_utils import get_api_key_for_provider
assert get_api_key_for_provider("unknown_provider") == "sk-fallback"
# ── get_all_available_models ─────────────────────────────────────────────────
class TestGetAllAvailableModels:
@pytest.mark.unit
@patch("application.core.model_utils.ModelRegistry.get_instance")
def test_returns_enabled_models_as_dict(self, mock_get_instance):
model_a = _make_model("model-a", display_name="Model A")
model_b = _make_model("model-b", display_name="Model B")
mock_registry = MagicMock()
mock_registry.get_enabled_models.return_value = [model_a, model_b]
mock_get_instance.return_value = mock_registry
from application.core.model_utils import get_all_available_models
result = get_all_available_models()
assert "model-a" in result
assert "model-b" in result
assert result["model-a"]["display_name"] == "Model A"
assert result["model-b"]["display_name"] == "Model B"
@pytest.mark.unit
@patch("application.core.model_utils.ModelRegistry.get_instance")
def test_empty_registry(self, mock_get_instance):
mock_registry = MagicMock()
mock_registry.get_enabled_models.return_value = []
mock_get_instance.return_value = mock_registry
from application.core.model_utils import get_all_available_models
assert get_all_available_models() == {}
# ── validate_model_id ────────────────────────────────────────────────────────
class TestValidateModelId:
@pytest.mark.unit
@patch("application.core.model_utils.ModelRegistry.get_instance")
def test_exists(self, mock_get_instance):
mock_registry = MagicMock()
mock_registry.model_exists.return_value = True
mock_get_instance.return_value = mock_registry
from application.core.model_utils import validate_model_id
assert validate_model_id("gpt-4") is True
@pytest.mark.unit
@patch("application.core.model_utils.ModelRegistry.get_instance")
def test_not_exists(self, mock_get_instance):
mock_registry = MagicMock()
mock_registry.model_exists.return_value = False
mock_get_instance.return_value = mock_registry
from application.core.model_utils import validate_model_id
assert validate_model_id("nonexistent") is False
# ── get_model_capabilities ───────────────────────────────────────────────────
class TestGetModelCapabilities:
@pytest.mark.unit
@patch("application.core.model_utils.ModelRegistry.get_instance")
def test_model_found(self, mock_get_instance):
model = _make_model(
"gpt-4",
context_window=8192,
supports_tools=True,
supports_structured_output=True,
supported_attachment_types=["image/png"],
)
mock_registry = MagicMock()
mock_registry.get_model.return_value = model
mock_get_instance.return_value = mock_registry
from application.core.model_utils import get_model_capabilities
caps = get_model_capabilities("gpt-4")
assert caps is not None
assert caps["supported_attachment_types"] == ["image/png"]
assert caps["supports_tools"] is True
assert caps["supports_structured_output"] is True
assert caps["context_window"] == 8192
@pytest.mark.unit
@patch("application.core.model_utils.ModelRegistry.get_instance")
def test_model_not_found(self, mock_get_instance):
mock_registry = MagicMock()
mock_registry.get_model.return_value = None
mock_get_instance.return_value = mock_registry
from application.core.model_utils import get_model_capabilities
assert get_model_capabilities("nonexistent") is None
# ── get_default_model_id ─────────────────────────────────────────────────────
class TestGetDefaultModelId:
@pytest.mark.unit
@patch("application.core.model_utils.ModelRegistry.get_instance")
def test_returns_default(self, mock_get_instance):
mock_registry = MagicMock()
mock_registry.default_model_id = "gpt-4"
mock_get_instance.return_value = mock_registry
from application.core.model_utils import get_default_model_id
assert get_default_model_id() == "gpt-4"
# ── get_provider_from_model_id ───────────────────────────────────────────────
class TestGetProviderFromModelId:
@pytest.mark.unit
@patch("application.core.model_utils.ModelRegistry.get_instance")
def test_model_found(self, mock_get_instance):
model = _make_model("gpt-4", provider=ModelProvider.OPENAI)
mock_registry = MagicMock()
mock_registry.get_model.return_value = model
mock_get_instance.return_value = mock_registry
from application.core.model_utils import get_provider_from_model_id
assert get_provider_from_model_id("gpt-4") == "openai"
@pytest.mark.unit
@patch("application.core.model_utils.ModelRegistry.get_instance")
def test_model_not_found(self, mock_get_instance):
mock_registry = MagicMock()
mock_registry.get_model.return_value = None
mock_get_instance.return_value = mock_registry
from application.core.model_utils import get_provider_from_model_id
assert get_provider_from_model_id("nonexistent") is None
# ── get_token_limit ──────────────────────────────────────────────────────────
class TestGetTokenLimit:
@pytest.mark.unit
@patch("application.core.model_utils.ModelRegistry.get_instance")
def test_model_found(self, mock_get_instance):
model = _make_model("gpt-4", context_window=8192)
mock_registry = MagicMock()
mock_registry.get_model.return_value = model
mock_get_instance.return_value = mock_registry
from application.core.model_utils import get_token_limit
assert get_token_limit("gpt-4") == 8192
@pytest.mark.unit
@patch("application.core.model_utils.ModelRegistry.get_instance")
def test_model_not_found_returns_default(self, mock_get_instance):
mock_registry = MagicMock()
mock_registry.get_model.return_value = None
mock_get_instance.return_value = mock_registry
with patch("application.core.settings.settings") as mock_settings:
mock_settings.DEFAULT_LLM_TOKEN_LIMIT = 128000
from application.core.model_utils import get_token_limit
assert get_token_limit("nonexistent") == 128000
# ── get_base_url_for_model ───────────────────────────────────────────────────
class TestGetBaseUrlForModel:
@pytest.mark.unit
@patch("application.core.model_utils.ModelRegistry.get_instance")
def test_model_with_base_url(self, mock_get_instance):
model = _make_model("custom-model", base_url="http://localhost:8080")
mock_registry = MagicMock()
mock_registry.get_model.return_value = model
mock_get_instance.return_value = mock_registry
from application.core.model_utils import get_base_url_for_model
assert get_base_url_for_model("custom-model") == "http://localhost:8080"
@pytest.mark.unit
@patch("application.core.model_utils.ModelRegistry.get_instance")
def test_model_without_base_url(self, mock_get_instance):
model = _make_model("gpt-4", base_url=None)
mock_registry = MagicMock()
mock_registry.get_model.return_value = model
mock_get_instance.return_value = mock_registry
from application.core.model_utils import get_base_url_for_model
assert get_base_url_for_model("gpt-4") is None
@pytest.mark.unit
@patch("application.core.model_utils.ModelRegistry.get_instance")
def test_model_not_found(self, mock_get_instance):
mock_registry = MagicMock()
mock_registry.get_model.return_value = None
mock_get_instance.return_value = mock_registry
from application.core.model_utils import get_base_url_for_model
assert get_base_url_for_model("nonexistent") is None
+208
View File
@@ -0,0 +1,208 @@
"""Tests for the operator MODELS_CONFIG_DIR.
Covers the operator-supplied directory of model YAMLs that's loaded
after the built-in catalog. Operators use this to add new
``openai_compatible`` providers, extend an existing provider's catalog
with extra models, or override a built-in model's capabilities — all
without forking the repo.
"""
from __future__ import annotations
import logging
from textwrap import dedent
from unittest.mock import MagicMock, patch
import pytest
from application.core.model_registry import ModelRegistry
def _make_settings(**overrides):
s = MagicMock()
s.OPENAI_BASE_URL = None
s.OPENAI_API_KEY = None
s.OPENAI_API_BASE = None
s.ANTHROPIC_API_KEY = None
s.GOOGLE_API_KEY = None
s.GROQ_API_KEY = None
s.OPEN_ROUTER_API_KEY = None
s.NOVITA_API_KEY = None
s.HUGGINGFACE_API_KEY = None
s.LLM_PROVIDER = ""
s.LLM_NAME = None
s.API_KEY = None
s.MODELS_CONFIG_DIR = None
for k, v in overrides.items():
setattr(s, k, v)
return s
@pytest.fixture(autouse=True)
def _reset_registry():
ModelRegistry.reset()
yield
ModelRegistry.reset()
# ── New provider via openai_compatible ───────────────────────────────────
@pytest.mark.unit
class TestOperatorAddsNewProvider:
def test_drop_in_yaml_appears_in_registry(
self, tmp_path, monkeypatch
):
(tmp_path / "fireworks.yaml").write_text(dedent("""
provider: openai_compatible
display_provider: fireworks
api_key_env: FIREWORKS_API_KEY
base_url: https://api.fireworks.ai/inference/v1
defaults:
supports_tools: true
models:
- id: accounts/fireworks/models/llama-v3p3-70b-instruct
display_name: Llama 3.3 70B (Fireworks)
"""))
monkeypatch.setenv("FIREWORKS_API_KEY", "fw-key")
s = _make_settings(MODELS_CONFIG_DIR=str(tmp_path))
with patch("application.core.settings.settings", s):
reg = ModelRegistry()
m = reg.get_model("accounts/fireworks/models/llama-v3p3-70b-instruct")
assert m is not None
assert m.api_key == "fw-key"
assert m.base_url == "https://api.fireworks.ai/inference/v1"
assert m.display_provider == "fireworks"
# ── Extending an existing provider's catalog ─────────────────────────────
@pytest.mark.unit
class TestOperatorExtendsExistingProvider:
def test_operator_adds_anthropic_model_to_builtin_catalog(
self, tmp_path
):
(tmp_path / "anthropic-extra.yaml").write_text(dedent("""
provider: anthropic
defaults:
supports_tools: true
context_window: 200000
models:
- id: claude-haiku-5-0-future
display_name: Claude Haiku 5.0
"""))
s = _make_settings(
ANTHROPIC_API_KEY="sk-ant",
MODELS_CONFIG_DIR=str(tmp_path),
)
with patch("application.core.settings.settings", s):
reg = ModelRegistry()
# Built-in models still present
assert reg.get_model("claude-sonnet-4-6") is not None
assert reg.get_model("claude-opus-4-7") is not None
# Operator-added model also present
added = reg.get_model("claude-haiku-5-0-future")
assert added is not None
assert added.display_name == "Claude Haiku 5.0"
# ── Overriding a built-in model's capabilities ───────────────────────────
@pytest.mark.unit
class TestOperatorOverridesBuiltinCapabilities:
def test_operator_yaml_overrides_builtin_context_window(
self, tmp_path, caplog
):
# Override anthropic claude-haiku-4-5 to claim a 1M context window
(tmp_path / "anthropic-override.yaml").write_text(dedent("""
provider: anthropic
defaults:
supports_tools: true
attachments: [image]
context_window: 1000000
models:
- id: claude-haiku-4-5
display_name: Claude Haiku 4.5 (extended)
description: Operator-overridden capabilities
"""))
s = _make_settings(
ANTHROPIC_API_KEY="sk-ant",
MODELS_CONFIG_DIR=str(tmp_path),
)
with caplog.at_level(logging.WARNING):
with patch("application.core.settings.settings", s):
reg = ModelRegistry()
m = reg.get_model("claude-haiku-4-5")
assert m.display_name == "Claude Haiku 4.5 (extended)"
assert m.description == "Operator-overridden capabilities"
assert m.capabilities.context_window == 1_000_000
# And the override warning fires so the operator can audit it
assert any(
"claude-haiku-4-5" in rec.message and "redefined" in rec.message
for rec in caplog.records
)
# ── Misconfigured MODELS_CONFIG_DIR ──────────────────────────────────────
@pytest.mark.unit
class TestMisconfiguredOperatorDir:
def test_missing_dir_logs_warning_and_continues(
self, tmp_path, caplog
):
bogus = tmp_path / "does-not-exist"
s = _make_settings(MODELS_CONFIG_DIR=str(bogus))
with caplog.at_level(logging.WARNING):
with patch("application.core.settings.settings", s):
reg = ModelRegistry()
# Built-in catalog still loaded
assert reg.get_model("docsgpt-local") is not None
# And the operator was warned
assert any("does not exist" in rec.message for rec in caplog.records)
def test_path_is_a_file_logs_warning(self, tmp_path, caplog):
afile = tmp_path / "not-a-dir.yaml"
afile.write_text("provider: anthropic\nmodels: []")
s = _make_settings(MODELS_CONFIG_DIR=str(afile))
with caplog.at_level(logging.WARNING):
with patch("application.core.settings.settings", s):
reg = ModelRegistry()
assert reg.get_model("docsgpt-local") is not None
assert any("not a directory" in rec.message for rec in caplog.records)
# ── Validation: unknown provider rejected ────────────────────────────────
@pytest.mark.unit
class TestOperatorValidation:
def test_unknown_provider_in_operator_yaml_aborts_boot(self, tmp_path):
(tmp_path / "bogus.yaml").write_text(dedent("""
provider: not_a_real_provider
models:
- id: x
display_name: X
"""))
s = _make_settings(MODELS_CONFIG_DIR=str(tmp_path))
with patch("application.core.settings.settings", s):
with pytest.raises(Exception) as exc_info:
ModelRegistry()
# Could be ModelYAMLError (enum check) or ValueError (registry check);
# either way the message must surface what's wrong.
msg = str(exc_info.value)
assert "not_a_real_provider" in msg
+305
View File
@@ -0,0 +1,305 @@
"""Tests for the openai_compatible provider.
Covers YAML loading from a temp directory, multiple coexisting catalogs
(Mistral + Together), env-var-based credential resolution, the legacy
OPENAI_BASE_URL + LLM_NAME fallback, and end-to-end model dispatch
through LLMCreator.
"""
from __future__ import annotations
from pathlib import Path
from textwrap import dedent
from unittest.mock import MagicMock, patch
import pytest
from application.core.model_registry import ModelRegistry
from application.core.model_settings import ModelProvider
def _make_settings(**overrides):
s = MagicMock()
s.OPENAI_BASE_URL = None
s.OPENAI_API_KEY = None
s.OPENAI_API_BASE = None
s.ANTHROPIC_API_KEY = None
s.GOOGLE_API_KEY = None
s.GROQ_API_KEY = None
s.OPEN_ROUTER_API_KEY = None
s.NOVITA_API_KEY = None
s.HUGGINGFACE_API_KEY = None
s.LLM_PROVIDER = ""
s.LLM_NAME = None
s.API_KEY = None
s.MODELS_CONFIG_DIR = None
for k, v in overrides.items():
setattr(s, k, v)
return s
def _write_mistral_yaml(directory: Path) -> Path:
path = directory / "mistral.yaml"
path.write_text(dedent("""
provider: openai_compatible
display_provider: mistral
api_key_env: MISTRAL_API_KEY
base_url: https://api.mistral.ai/v1
defaults:
supports_tools: true
context_window: 128000
models:
- id: mistral-large-latest
display_name: Mistral Large
- id: mistral-small-latest
display_name: Mistral Small
"""))
return path
def _write_together_yaml(directory: Path) -> Path:
path = directory / "together.yaml"
path.write_text(dedent("""
provider: openai_compatible
display_provider: together
api_key_env: TOGETHER_API_KEY
base_url: https://api.together.xyz/v1
defaults:
supports_tools: true
models:
- id: meta-llama/Llama-3.3-70B-Instruct-Turbo
display_name: Llama 3.3 70B (Together)
"""))
return path
@pytest.fixture(autouse=True)
def _reset_registry(monkeypatch):
# Builtin openai_compatible catalogs (e.g. deepseek.yaml) activate when
# their api_key_env is present in os.environ, which the openai_compatible
# provider reads directly, independent of the patched settings. Clear it
# so a key leaked by another test (or present in the dev .env) doesn't add
# extra models to the exact-match assertions below. Mirrors the delenv
# guard in test_model_registry_yaml.py.
monkeypatch.delenv("DEEPSEEK_API_KEY", raising=False)
ModelRegistry.reset()
yield
ModelRegistry.reset()
# ── YAML-driven catalogs ─────────────────────────────────────────────────
@pytest.mark.unit
class TestYAMLCompatibleProvider:
def test_mistral_yaml_loads_with_env_key(
self, tmp_path, monkeypatch
):
_write_mistral_yaml(tmp_path)
monkeypatch.setenv("MISTRAL_API_KEY", "sk-mistral-test")
s = _make_settings(MODELS_CONFIG_DIR=str(tmp_path))
with patch("application.core.settings.settings", s):
reg = ModelRegistry()
m = reg.get_model("mistral-large-latest")
assert m is not None
assert m.provider == ModelProvider.OPENAI_COMPATIBLE
assert m.display_provider == "mistral"
assert m.base_url == "https://api.mistral.ai/v1"
assert m.api_key == "sk-mistral-test"
assert m.capabilities.supports_tools is True
assert m.capabilities.context_window == 128000
def test_yaml_skipped_when_env_var_missing(
self, tmp_path, monkeypatch
):
_write_mistral_yaml(tmp_path)
monkeypatch.delenv("MISTRAL_API_KEY", raising=False)
s = _make_settings(MODELS_CONFIG_DIR=str(tmp_path))
with patch("application.core.settings.settings", s):
reg = ModelRegistry()
# Catalog skipped when no key — no Mistral models in the registry
assert reg.get_model("mistral-large-latest") is None
def test_two_compatible_catalogs_coexist_with_separate_keys(
self, tmp_path, monkeypatch
):
_write_mistral_yaml(tmp_path)
_write_together_yaml(tmp_path)
monkeypatch.setenv("MISTRAL_API_KEY", "sk-mistral")
monkeypatch.setenv("TOGETHER_API_KEY", "sk-together")
s = _make_settings(MODELS_CONFIG_DIR=str(tmp_path))
with patch("application.core.settings.settings", s):
reg = ModelRegistry()
mistral = reg.get_model("mistral-large-latest")
together = reg.get_model("meta-llama/Llama-3.3-70B-Instruct-Turbo")
assert mistral.api_key == "sk-mistral"
assert mistral.base_url == "https://api.mistral.ai/v1"
assert mistral.display_provider == "mistral"
assert together.api_key == "sk-together"
assert together.base_url == "https://api.together.xyz/v1"
assert together.display_provider == "together"
def test_one_catalog_enabled_other_skipped(
self, tmp_path, monkeypatch
):
_write_mistral_yaml(tmp_path)
_write_together_yaml(tmp_path)
monkeypatch.setenv("MISTRAL_API_KEY", "sk-mistral")
monkeypatch.delenv("TOGETHER_API_KEY", raising=False)
s = _make_settings(MODELS_CONFIG_DIR=str(tmp_path))
with patch("application.core.settings.settings", s):
reg = ModelRegistry()
assert reg.get_model("mistral-large-latest") is not None
assert reg.get_model("meta-llama/Llama-3.3-70B-Instruct-Turbo") is None
def test_missing_base_url_raises(self, tmp_path, monkeypatch):
bad = tmp_path / "broken.yaml"
bad.write_text(dedent("""
provider: openai_compatible
api_key_env: SOME_KEY
models:
- id: x
display_name: X
"""))
monkeypatch.setenv("SOME_KEY", "k")
s = _make_settings(MODELS_CONFIG_DIR=str(tmp_path))
with patch("application.core.settings.settings", s):
with pytest.raises(ValueError, match="must set 'base_url'"):
ModelRegistry()
def test_missing_api_key_env_raises(self, tmp_path, monkeypatch):
bad = tmp_path / "broken.yaml"
bad.write_text(dedent("""
provider: openai_compatible
base_url: https://x/v1
models:
- id: x
display_name: X
"""))
s = _make_settings(MODELS_CONFIG_DIR=str(tmp_path))
with patch("application.core.settings.settings", s):
with pytest.raises(ValueError, match="must set 'api_key_env'"):
ModelRegistry()
def test_to_dict_uses_display_provider(
self, tmp_path, monkeypatch
):
_write_mistral_yaml(tmp_path)
monkeypatch.setenv("MISTRAL_API_KEY", "sk")
s = _make_settings(MODELS_CONFIG_DIR=str(tmp_path))
with patch("application.core.settings.settings", s):
reg = ModelRegistry()
d = reg.get_model("mistral-large-latest").to_dict()
# /api/models response shows "mistral", not "openai_compatible"
assert d["provider"] == "mistral"
# api_key never leaks into the wire format
assert "api_key" not in d
for v in d.values():
assert v != "sk"
# ── Legacy OPENAI_BASE_URL fallback ──────────────────────────────────────
@pytest.mark.unit
class TestLegacyOpenAIBaseURLPath:
def test_legacy_models_now_provided_by_openai_compatible(self):
s = _make_settings(
OPENAI_BASE_URL="http://localhost:11434/v1",
OPENAI_API_KEY="sk-local",
LLM_PROVIDER="openai",
LLM_NAME="llama3,gemma",
)
with patch("application.core.settings.settings", s):
reg = ModelRegistry()
ids = {m.id for m in reg.get_all_models()}
assert ids == {"llama3", "gemma"}
llama = reg.get_model("llama3")
assert llama.base_url == "http://localhost:11434/v1"
assert llama.api_key == "sk-local"
assert llama.provider == ModelProvider.OPENAI_COMPATIBLE
# Display provider preserves the historical "openai" label
assert llama.display_provider == "openai"
assert llama.to_dict()["provider"] == "openai"
def test_legacy_uses_api_key_fallback_when_openai_api_key_missing(self):
s = _make_settings(
OPENAI_BASE_URL="http://localhost:11434/v1",
OPENAI_API_KEY=None,
API_KEY="sk-generic",
LLM_PROVIDER="openai",
LLM_NAME="llama3",
)
with patch("application.core.settings.settings", s):
reg = ModelRegistry()
assert reg.get_model("llama3").api_key == "sk-generic"
# ── Dispatch through LLMCreator ──────────────────────────────────────────
@pytest.mark.unit
class TestLLMCreatorDispatch:
def test_llmcreator_uses_per_model_api_key_and_base_url(
self, tmp_path, monkeypatch
):
"""End-to-end: when an openai_compatible model is dispatched, the
per-model api_key + base_url from the registry must override
whatever the caller passed."""
_write_mistral_yaml(tmp_path)
monkeypatch.setenv("MISTRAL_API_KEY", "sk-mistral-real")
s = _make_settings(MODELS_CONFIG_DIR=str(tmp_path))
captured = {}
class _FakeLLM:
def __init__(
self, api_key, user_api_key, *args, **kwargs
):
captured["api_key"] = api_key
captured["base_url"] = kwargs.get("base_url")
captured["model_id"] = kwargs.get("model_id")
with patch("application.core.settings.settings", s):
ModelRegistry.reset()
ModelRegistry() # warm up the registry under patched settings
# Now patch the OpenAI plugin's class so we can capture the
# constructor args without spinning up the real OpenAILLM.
from application.llm.providers import PROVIDERS_BY_NAME
with patch.object(
PROVIDERS_BY_NAME["openai_compatible"],
"llm_class",
_FakeLLM,
):
from application.llm.llm_creator import LLMCreator
LLMCreator.create_llm(
type="openai_compatible",
api_key="caller-passed-WRONG-key",
user_api_key=None,
decoded_token={"sub": "u"},
model_id="mistral-large-latest",
)
assert captured["api_key"] == "sk-mistral-real"
assert captured["base_url"] == "https://api.mistral.ai/v1"
assert captured["model_id"] == "mistral-large-latest"
+505
View File
@@ -0,0 +1,505 @@
"""Tests for the BYOM per-user layer on ModelRegistry.
Covers: per-user lookups don't leak across users, lookups without
user_id stay built-in only, get_all_models / get_enabled_models /
model_exists all consult the user layer when given user_id, and the
explicit invalidate_user clears the cache.
"""
from __future__ import annotations
from contextlib import contextmanager
from unittest.mock import MagicMock, patch
import pytest
from application.core.model_registry import ModelRegistry
from application.core.model_settings import ModelProvider
from application.storage.db.repositories.user_custom_models import (
UserCustomModelsRepository,
)
@pytest.fixture(autouse=True)
def _reset_registry():
ModelRegistry.reset()
yield
ModelRegistry.reset()
def _make_settings(**overrides):
s = MagicMock()
s.OPENAI_BASE_URL = None
s.OPENAI_API_KEY = None
s.OPENAI_API_BASE = None
s.ANTHROPIC_API_KEY = None
s.GOOGLE_API_KEY = None
s.GROQ_API_KEY = None
s.OPEN_ROUTER_API_KEY = None
s.NOVITA_API_KEY = None
s.HUGGINGFACE_API_KEY = None
s.LLM_PROVIDER = ""
s.LLM_NAME = None
s.API_KEY = None
s.MODELS_CONFIG_DIR = None
for k, v in overrides.items():
setattr(s, k, v)
return s
@contextmanager
def _yield(conn):
yield conn
@pytest.mark.unit
class TestPerUserLayer:
def test_user_models_isolated_per_user(self, pg_conn):
"""Alice's BYOM model must not appear in Bob's lookups."""
repo = UserCustomModelsRepository(pg_conn)
alice_model = repo.create(
user_id="alice",
upstream_model_id="alice-mistral",
display_name="Alice Mistral",
base_url="https://api.mistral.ai/v1",
api_key_plaintext="sk-alice",
)
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
):
reg = ModelRegistry()
assert reg.get_model(alice_model["id"], user_id="alice") is not None
assert reg.get_model(alice_model["id"], user_id="bob") is None
# And without a user_id at all, the per-user layer is invisible
assert reg.get_model(alice_model["id"]) is None
def test_get_all_models_includes_user_models(self, pg_conn):
repo = UserCustomModelsRepository(pg_conn)
created = repo.create(
user_id="user-1",
upstream_model_id="mistral-large-latest",
display_name="My Mistral",
base_url="https://api.mistral.ai/v1",
api_key_plaintext="sk-test",
)
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
):
reg = ModelRegistry()
ids_no_user = {m.id for m in reg.get_all_models()}
ids_with_user = {
m.id for m in reg.get_all_models(user_id="user-1")
}
assert created["id"] not in ids_no_user
assert created["id"] in ids_with_user
def test_user_models_carry_decrypted_api_key_and_upstream_id(self, pg_conn):
repo = UserCustomModelsRepository(pg_conn)
created = repo.create(
user_id="user-1",
upstream_model_id="mistral-large-latest",
display_name="My Mistral",
base_url="https://api.mistral.ai/v1",
api_key_plaintext="sk-test-XYZ",
)
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
):
reg = ModelRegistry()
m = reg.get_model(created["id"], user_id="user-1")
assert m is not None
assert m.provider == ModelProvider.OPENAI_COMPATIBLE
assert m.upstream_model_id == "mistral-large-latest"
assert m.api_key == "sk-test-XYZ"
assert m.base_url == "https://api.mistral.ai/v1"
assert m.source == "user"
# The wire format never leaks the api_key
d = m.to_dict()
assert "api_key" not in d
for v in d.values():
assert v != "sk-test-XYZ"
def test_invalidate_user_clears_cache(self, pg_conn):
repo = UserCustomModelsRepository(pg_conn)
created = repo.create(
user_id="user-1",
upstream_model_id="x",
display_name="X",
base_url="https://api.mistral.ai/v1",
api_key_plaintext="k",
)
s = _make_settings()
# Stub Redis so invalidate_user can publish its version bump
# without hitting a real broker. The P1 fix calls ``incr`` on
# invalidate; here we just need it not to raise.
fake_redis = MagicMock()
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
), patch(
"application.cache.get_redis_instance", return_value=fake_redis
):
reg = ModelRegistry()
assert reg.get_model(created["id"], user_id="user-1") is not None
# Cache populated
assert "user-1" in reg._user_models
ModelRegistry.invalidate_user("user-1")
assert "user-1" not in reg._user_models
# Re-lookup repopulates
reg.get_model(created["id"], user_id="user-1")
assert "user-1" in reg._user_models
@pytest.mark.unit
class TestLLMCreatorDispatchUsesUpstreamModelId:
def test_llmcreator_sends_upstream_id_not_uuid(self, pg_conn):
"""End-to-end: LLMCreator with a BYOM uuid must construct the
OpenAILLM with the user's upstream model name (e.g.
``mistral-large-latest``), not the registry uuid."""
repo = UserCustomModelsRepository(pg_conn)
created = repo.create(
user_id="user-1",
upstream_model_id="mistral-large-latest",
display_name="My Mistral",
base_url="https://api.mistral.ai/v1",
api_key_plaintext="sk-mistral-real",
)
captured = {}
class _FakeLLM:
def __init__(self, api_key, user_api_key, *args, **kwargs):
captured["api_key"] = api_key
captured["base_url"] = kwargs.get("base_url")
captured["model_id"] = kwargs.get("model_id")
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
):
ModelRegistry()
from application.llm.providers import PROVIDERS_BY_NAME
with patch.object(
PROVIDERS_BY_NAME["openai_compatible"], "llm_class", _FakeLLM
):
from application.llm.llm_creator import LLMCreator
LLMCreator.create_llm(
type="openai_compatible",
api_key="caller-passed-WRONG",
user_api_key=None,
decoded_token={"sub": "user-1"},
model_id=created["id"],
)
assert captured["api_key"] == "sk-mistral-real"
assert captured["base_url"] == "https://api.mistral.ai/v1"
assert captured["model_id"] == "mistral-large-latest" # NOT the uuid!
def test_llmcreator_forwards_byom_capabilities(self, pg_conn):
"""LLMCreator must thread the registry-resolved ``capabilities``
into the LLM. Without it the OpenAILLM hard-codes ``True`` for
tools/structured output and advertises image attachments
unconditionally, leaking unsupported features to BYOMs that
disabled them."""
repo = UserCustomModelsRepository(pg_conn)
created = repo.create(
user_id="user-2",
upstream_model_id="my-text-only-model",
display_name="Text-only BYOM",
base_url="https://api.mistral.ai/v1",
api_key_plaintext="sk-real",
capabilities={
"supports_tools": False,
"supports_structured_output": False,
"attachments": [],
"context_window": 8192,
},
)
captured = {}
class _FakeLLM:
def __init__(self, api_key, user_api_key, *args, **kwargs):
captured["capabilities"] = kwargs.get("capabilities")
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
):
ModelRegistry()
from application.llm.providers import PROVIDERS_BY_NAME
with patch.object(
PROVIDERS_BY_NAME["openai_compatible"], "llm_class", _FakeLLM
):
from application.llm.llm_creator import LLMCreator
LLMCreator.create_llm(
type="openai_compatible",
api_key="ignored",
user_api_key=None,
decoded_token={"sub": "user-2"},
model_id=created["id"],
)
caps = captured["capabilities"]
assert caps is not None
assert caps.supports_tools is False
assert caps.supports_structured_output is False
assert caps.supported_attachment_types == []
def test_byom_image_alias_expands_to_mime_types(self, pg_conn):
"""A BYOM stored with ``attachments: ["image"]`` (the alias the
UI sends) must surface as concrete MIME types on the registry
record, matching the built-in YAML expansion. Without this,
handlers/base.prepare_messages compares ``image/png`` against
the bare alias and filters every image upload as unsupported.
"""
repo = UserCustomModelsRepository(pg_conn)
created = repo.create(
user_id="user-1",
upstream_model_id="my-vision-model",
display_name="Vision BYOM",
base_url="https://api.mistral.ai/v1",
api_key_plaintext="sk-real",
capabilities={"attachments": ["image"]},
)
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
):
reg = ModelRegistry()
model = reg.get_model(created["id"], user_id="user-1")
assert model is not None
types = model.capabilities.supported_attachment_types
assert "image" not in types, (
"alias must be expanded, not stored verbatim"
)
assert any(t.startswith("image/") for t in types)
# Must include at least the common web image types so any image
# an end user uploads has a chance to match.
assert "image/png" in types
assert "image/jpeg" in types
def test_byom_unknown_alias_is_skipped_at_runtime(self, pg_conn):
"""Operator alias-map edits could orphan a stored alias. The
registry must drop the unknown entry rather than the entire
layer (which would hide every BYOM the user has).
"""
repo = UserCustomModelsRepository(pg_conn)
created = repo.create(
user_id="user-1",
upstream_model_id="m",
display_name="M",
base_url="https://api.mistral.ai/v1",
api_key_plaintext="k",
# Bypass the route validation: write a bad alias straight
# to the row to simulate the post-edit orphan case.
capabilities={"attachments": ["image", "not-a-real-alias"]},
)
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
):
reg = ModelRegistry()
model = reg.get_model(created["id"], user_id="user-1")
assert model is not None
types = model.capabilities.supported_attachment_types
assert "not-a-real-alias" not in types
assert any(t.startswith("image/") for t in types)
@pytest.mark.unit
class TestCrossProcessInvalidation:
"""The BYOM cache lives per-process. Without the P1 fix, a CRUD on
web-1 would leave the decrypted record (with old api_key/base_url)
cached forever in web-2 / Celery. These tests pin down that:
* ``invalidate_user`` publishes a version bump to Redis
* peers reload when the version they saw at load time is stale
* the local TTL bounds staleness even when Redis is unreachable
* unchanged version + expired TTL extends the entry without a
DB read (the common-case fast path)
"""
def test_invalidate_user_publishes_redis_version_bump(self, pg_conn):
repo = UserCustomModelsRepository(pg_conn)
repo.create(
user_id="user-1",
upstream_model_id="m1",
display_name="M1",
base_url="https://api.mistral.ai/v1",
api_key_plaintext="k",
)
fake_redis = MagicMock()
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
), patch(
"application.cache.get_redis_instance", return_value=fake_redis
):
ModelRegistry().get_model("anything", user_id="user-1")
ModelRegistry.invalidate_user("user-1")
fake_redis.incr.assert_called_once_with("byom:registry_version:user-1")
def test_peer_reloads_when_redis_version_changes(self, pg_conn):
"""Two-process simulation. Peer loads at version=0; another
process's CRUD bumps the version and updates Postgres; peer's
next post-TTL access sees the version mismatch and reloads,
picking up the rotated key it never invalidated locally."""
from application.core import model_registry as registry_mod
repo = UserCustomModelsRepository(pg_conn)
created = repo.create(
user_id="user-1",
upstream_model_id="m-orig",
display_name="orig",
base_url="https://api.mistral.ai/v1",
api_key_plaintext="orig-key",
)
state = {"version": 0}
class _FakeRedis:
def get(self, key):
if key == "byom:registry_version:user-1":
return str(state["version"]).encode()
return None
def incr(self, key):
if key == "byom:registry_version:user-1":
state["version"] += 1
s = _make_settings()
# Force TTL to 0 so any subsequent access takes the post-TTL
# path without waiting 60s.
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
), patch(
"application.cache.get_redis_instance", return_value=_FakeRedis()
), patch.object(
registry_mod, "_USER_CACHE_TTL_SECONDS", 0.0
):
reg = ModelRegistry()
assert (
reg.get_model(created["id"], user_id="user-1").api_key
== "orig-key"
)
# Another process's CRUD: bump Redis counter + mutate the
# row. Note we deliberately do NOT call ``invalidate_user``
# in this process — that's the whole point of the test.
state["version"] += 1
repo.update(
created["id"],
"user-1",
{"api_key_plaintext": "rotated-key"},
)
assert (
reg.get_model(created["id"], user_id="user-1").api_key
== "rotated-key"
)
def test_ttl_bounds_staleness_when_redis_unavailable(self, pg_conn):
"""Redis down → fall back to TTL-only invalidation. After the
TTL elapses, peers reload regardless."""
from application.core import model_registry as registry_mod
repo = UserCustomModelsRepository(pg_conn)
created = repo.create(
user_id="user-1",
upstream_model_id="m",
display_name="m",
base_url="https://api.mistral.ai/v1",
api_key_plaintext="orig",
)
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
), patch(
"application.cache.get_redis_instance", return_value=None
), patch.object(
registry_mod, "_USER_CACHE_TTL_SECONDS", 0.0
):
reg = ModelRegistry()
first = reg.get_model(created["id"], user_id="user-1")
assert first.api_key == "orig"
repo.update(
created["id"],
"user-1",
{"api_key_plaintext": "rotated"},
)
second = reg.get_model(created["id"], user_id="user-1")
assert second.api_key == "rotated"
def test_unchanged_version_extends_ttl_without_db_read(self, pg_conn):
"""Hot path: TTL expires but Redis says no invalidation
happened — extend the entry without re-reading Postgres."""
from application.core import model_registry as registry_mod
repo = UserCustomModelsRepository(pg_conn)
created = repo.create(
user_id="user-1",
upstream_model_id="m",
display_name="m",
base_url="https://api.mistral.ai/v1",
api_key_plaintext="k",
)
fake_redis = MagicMock()
fake_redis.get.return_value = b"7" # constant version
db_open_count = {"n": 0}
@contextmanager
def _counting_db_readonly():
db_open_count["n"] += 1
yield pg_conn
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
_counting_db_readonly,
), patch(
"application.cache.get_redis_instance", return_value=fake_redis
), patch.object(
registry_mod, "_USER_CACHE_TTL_SECONDS", 0.0
):
reg = ModelRegistry()
reg.get_model(created["id"], user_id="user-1")
first_open = db_open_count["n"]
# TTL has expired, but Redis returns the same version we
# captured at load time → no DB reload.
reg.get_model(created["id"], user_id="user-1")
reg.get_model(created["id"], user_id="user-1")
assert db_open_count["n"] == first_open
+37
View File
@@ -0,0 +1,37 @@
"""Unit tests for the process-wide graceful-shutdown flag."""
import pytest
from application.core import shutdown
@pytest.fixture(autouse=True)
def _reset_flag():
shutdown.reset_shutdown()
yield
shutdown.reset_shutdown()
@pytest.mark.unit
def test_flag_starts_clear():
assert shutdown.is_shutting_down() is False
@pytest.mark.unit
def test_begin_shutdown_sets_flag():
shutdown.begin_shutdown()
assert shutdown.is_shutting_down() is True
@pytest.mark.unit
def test_begin_shutdown_is_idempotent():
shutdown.begin_shutdown()
shutdown.begin_shutdown()
assert shutdown.is_shutting_down() is True
@pytest.mark.unit
def test_reset_clears_flag():
shutdown.begin_shutdown()
shutdown.reset_shutdown()
assert shutdown.is_shutting_down() is False
+261
View File
@@ -0,0 +1,261 @@
"""Tests for SSRF URL validation module."""
import pytest
from unittest.mock import patch
from application.core.url_validation import (
SSRFError,
validate_url,
validate_url_safe,
is_private_ip,
is_metadata_ip,
)
class TestIsPrivateIP:
"""Tests for is_private_ip function."""
def test_loopback_ipv4(self):
assert is_private_ip("127.0.0.1") is True
assert is_private_ip("127.255.255.255") is True
def test_private_class_a(self):
assert is_private_ip("10.0.0.1") is True
assert is_private_ip("10.255.255.255") is True
def test_private_class_b(self):
assert is_private_ip("172.16.0.1") is True
assert is_private_ip("172.31.255.255") is True
def test_private_class_c(self):
assert is_private_ip("192.168.0.1") is True
assert is_private_ip("192.168.255.255") is True
def test_link_local(self):
assert is_private_ip("169.254.0.1") is True
def test_public_ip(self):
assert is_private_ip("8.8.8.8") is False
assert is_private_ip("1.1.1.1") is False
assert is_private_ip("93.184.216.34") is False
def test_invalid_ip(self):
assert is_private_ip("not-an-ip") is False
assert is_private_ip("") is False
class TestIsMetadataIP:
"""Tests for is_metadata_ip function."""
def test_aws_metadata_ip(self):
assert is_metadata_ip("169.254.169.254") is True
def test_aws_ecs_metadata_ip(self):
assert is_metadata_ip("169.254.170.2") is True
def test_non_metadata_ip(self):
assert is_metadata_ip("8.8.8.8") is False
assert is_metadata_ip("10.0.0.1") is False
class TestValidateUrl:
"""Tests for validate_url function."""
def test_adds_scheme_if_missing(self):
with patch("application.core.url_validation.resolve_hostname") as mock_resolve:
mock_resolve.return_value = "93.184.216.34" # Public IP
result = validate_url("example.com")
assert result == "http://example.com"
def test_preserves_https_scheme(self):
with patch("application.core.url_validation.resolve_hostname") as mock_resolve:
mock_resolve.return_value = "93.184.216.34"
result = validate_url("https://example.com")
assert result == "https://example.com"
def test_blocks_localhost(self):
with pytest.raises(SSRFError) as exc_info:
validate_url("http://localhost")
assert "localhost" in str(exc_info.value).lower()
def test_blocks_localhost_localdomain(self):
with pytest.raises(SSRFError) as exc_info:
validate_url("http://localhost.localdomain")
assert "not allowed" in str(exc_info.value).lower()
def test_blocks_loopback_ip(self):
with pytest.raises(SSRFError) as exc_info:
validate_url("http://127.0.0.1")
assert "private" in str(exc_info.value).lower() or "internal" in str(exc_info.value).lower()
def test_blocks_private_ip_class_a(self):
with pytest.raises(SSRFError) as exc_info:
validate_url("http://10.0.0.1")
assert "private" in str(exc_info.value).lower() or "internal" in str(exc_info.value).lower()
def test_blocks_private_ip_class_b(self):
with pytest.raises(SSRFError) as exc_info:
validate_url("http://172.16.0.1")
assert "private" in str(exc_info.value).lower() or "internal" in str(exc_info.value).lower()
def test_blocks_private_ip_class_c(self):
with pytest.raises(SSRFError) as exc_info:
validate_url("http://192.168.1.1")
assert "private" in str(exc_info.value).lower() or "internal" in str(exc_info.value).lower()
def test_blocks_aws_metadata_ip(self):
with pytest.raises(SSRFError) as exc_info:
validate_url("http://169.254.169.254")
assert "metadata" in str(exc_info.value).lower()
def test_blocks_aws_metadata_with_path(self):
with pytest.raises(SSRFError) as exc_info:
validate_url("http://169.254.169.254/latest/meta-data/")
assert "metadata" in str(exc_info.value).lower()
def test_blocks_gcp_metadata_hostname(self):
with pytest.raises(SSRFError) as exc_info:
validate_url("http://metadata.google.internal")
assert "not allowed" in str(exc_info.value).lower()
def test_blocks_ftp_scheme(self):
with pytest.raises(SSRFError) as exc_info:
validate_url("ftp://example.com")
assert "scheme" in str(exc_info.value).lower()
def test_blocks_file_scheme(self):
with pytest.raises(SSRFError) as exc_info:
validate_url("file:///etc/passwd")
assert "scheme" in str(exc_info.value).lower()
def test_blocks_hostname_resolving_to_private_ip(self):
with patch("application.core.url_validation.resolve_hostname") as mock_resolve:
mock_resolve.return_value = "192.168.1.1"
with pytest.raises(SSRFError) as exc_info:
validate_url("http://internal.example.com")
assert "private" in str(exc_info.value).lower() or "internal" in str(exc_info.value).lower()
def test_blocks_hostname_resolving_to_metadata_ip(self):
with patch("application.core.url_validation.resolve_hostname") as mock_resolve:
mock_resolve.return_value = "169.254.169.254"
with pytest.raises(SSRFError) as exc_info:
validate_url("http://evil.example.com")
assert "metadata" in str(exc_info.value).lower()
def test_allows_public_ip(self):
result = validate_url("http://8.8.8.8")
assert result == "http://8.8.8.8"
def test_allows_public_hostname(self):
with patch("application.core.url_validation.resolve_hostname") as mock_resolve:
mock_resolve.return_value = "93.184.216.34"
result = validate_url("https://example.com")
assert result == "https://example.com"
def test_raises_on_unresolvable_hostname(self):
with patch("application.core.url_validation.resolve_hostname") as mock_resolve:
mock_resolve.return_value = None
with pytest.raises(SSRFError) as exc_info:
validate_url("http://nonexistent.invalid")
assert "resolve" in str(exc_info.value).lower()
def test_raises_on_empty_hostname(self):
with pytest.raises(SSRFError) as exc_info:
validate_url("http://")
assert "hostname" in str(exc_info.value).lower()
def test_allow_localhost_flag(self):
# Should work with allow_localhost=True
result = validate_url("http://localhost", allow_localhost=True)
assert result == "http://localhost"
result = validate_url("http://127.0.0.1", allow_localhost=True)
assert result == "http://127.0.0.1"
class TestValidateUrlSafe:
"""Tests for validate_url_safe non-throwing function."""
def test_returns_tuple_on_success(self):
with patch("application.core.url_validation.resolve_hostname") as mock_resolve:
mock_resolve.return_value = "93.184.216.34"
is_valid, url, error = validate_url_safe("https://example.com")
assert is_valid is True
assert url == "https://example.com"
assert error is None
def test_returns_tuple_on_failure(self):
is_valid, url, error = validate_url_safe("http://localhost")
assert is_valid is False
assert url == "http://localhost"
assert error is not None
assert "localhost" in error.lower()
def test_returns_error_message_for_private_ip(self):
is_valid, url, error = validate_url_safe("http://192.168.1.1")
assert is_valid is False
assert "private" in error.lower() or "internal" in error.lower()
def test_adds_scheme_when_missing(self):
with patch("application.core.url_validation.resolve_hostname") as mock_resolve:
mock_resolve.return_value = "93.184.216.34"
is_valid, url, error = validate_url_safe("example.com")
assert is_valid is True
assert url == "http://example.com"
class TestIsPrivateIPExtended:
"""Additional edge cases for IP classification."""
def test_multicast_ip(self):
assert is_private_ip("224.0.0.1") is True
def test_unspecified_ip(self):
assert is_private_ip("0.0.0.0") is True
def test_ipv6_loopback(self):
assert is_private_ip("::1") is True
def test_ipv6_private(self):
assert is_private_ip("fc00::1") is True
def test_ipv6_public(self):
assert is_private_ip("2607:f8b0:4004:800::200e") is False
def test_reserved_ip(self):
# 240.0.0.0/4 is reserved (future use), Python's ipaddress marks it as such
assert is_private_ip("240.0.0.1") is True
class TestValidateUrlExtended:
"""Additional URL validation tests."""
def test_blocks_metadata_hostname(self):
with pytest.raises(SSRFError):
validate_url("http://metadata")
def test_allows_localhost_with_flag(self):
with patch("application.core.url_validation.resolve_hostname") as mock_resolve:
mock_resolve.return_value = "192.168.1.1"
result = validate_url(
"http://internal.local", allow_localhost=True
)
assert result == "http://internal.local"
def test_blocks_aws_ecs_metadata_ip(self):
with pytest.raises(SSRFError, match="metadata"):
validate_url("http://169.254.170.2")
def test_blocks_aws_ipv6_metadata(self):
with pytest.raises(SSRFError, match="metadata"):
validate_url("http://[fd00:ec2::254]")
def test_blocks_hostname_resolving_to_loopback(self):
with patch("application.core.url_validation.resolve_hostname") as mock_resolve:
mock_resolve.return_value = "127.0.0.1"
with pytest.raises(SSRFError):
validate_url("http://sneaky.example.com")
def test_allows_localhost_ip_with_flag(self):
result = validate_url("http://10.0.0.1", allow_localhost=True)
assert result == "http://10.0.0.1"