refactor: rename the application package to docsgpt

The backend import package is now docsgpt, the name it will carry on PyPI;
application was far too generic to install into anyone's site-packages.
git mv plus a mechanical rewrite of every import, dotted string and path
reference: 734 Python files, the compose files, Dockerfile, workflows, docs,
setup scripts, devcontainer, k8s manifests, vscode config, pytest and coverage
config, .gitignore. Behaviour is unchanged.

Kept for one release:
- A top-level application package whose meta-path finder resolves
  application.x.y to the already-imported docsgpt.x.y object, so old imports
  and entry points (celery -A application.app.celery,
  uvicorn application.asgi:asgi_app) keep working with a FutureWarning.
- Celery registers every application.* task name as an alias of its
  docsgpt.* task on start-up, so messages queued by the previous release still
  run. The redbeat key prefix moves to redbeat:docsgpt:v2: so schedule entries
  the previous release wrote are left unread instead of firing twice.

The backend image builds from the repository root (docker build -f
docsgpt/Dockerfile .) so it can ship the alias package; a root .dockerignore
allow-lists docsgpt/ and application/ and keeps caches, local data, .env
files, the sample index files and the Dockerfile out. Compose and the image
workflows point at the new context.
This commit is contained in:
Alex committed 2026-09-07 10:20:43 +01:00
1 parent 3e7f1f91b4
commit 574f96341e
984 files changed
+9847 -9682

No files matched your search

+82 -82
View File
@@ -18,8 +18,8 @@ from unittest.mock import MagicMock, patch
import pytest
from application.core.model_registry import ModelRegistry
from application.core.model_utils import (
from docsgpt.core.model_registry import ModelRegistry
from docsgpt.core.model_utils import (
get_all_available_models,
get_api_key_for_model,
get_base_url_for_model,
@@ -28,7 +28,7 @@ from application.core.model_utils import (
get_token_limit,
validate_model_id,
)
from application.storage.db.repositories.user_custom_models import (
from docsgpt.storage.db.repositories.user_custom_models import (
UserCustomModelsRepository,
)
@@ -61,7 +61,7 @@ def _make_settings(**overrides):
s.MODELS_CONFIG_DIR = None
s.DEFAULT_LLM_TOKEN_LIMIT = 128000
# Concrete strings — module-level imports under patched settings
# (e.g. application.api.user.base's storage init) fail with
# (e.g. docsgpt.api.user.base's storage init) fail with
# MagicMock attribute values.
s.STORAGE_TYPE = "local"
for k, v in overrides.items():
@@ -110,8 +110,8 @@ class TestHelpersThreadUserId:
):
model_uuid, user_id = byom_model
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
with patch("docsgpt.core.settings.settings", s), patch(
"docsgpt.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
):
ModelRegistry()
@@ -122,8 +122,8 @@ class TestHelpersThreadUserId:
):
model_uuid, user_id = byom_model
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
with patch("docsgpt.core.settings.settings", s), patch(
"docsgpt.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
):
ModelRegistry()
@@ -134,8 +134,8 @@ class TestHelpersThreadUserId:
):
model_uuid, user_id = byom_model
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
with patch("docsgpt.core.settings.settings", s), patch(
"docsgpt.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
):
ModelRegistry()
@@ -150,8 +150,8 @@ class TestHelpersThreadUserId:
):
model_uuid, user_id = byom_model
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
with patch("docsgpt.core.settings.settings", s), patch(
"docsgpt.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
):
ModelRegistry()
@@ -165,8 +165,8 @@ class TestHelpersThreadUserId:
):
model_uuid, user_id = byom_model
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
with patch("docsgpt.core.settings.settings", s), patch(
"docsgpt.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
):
ModelRegistry()
@@ -181,8 +181,8 @@ class TestHelpersThreadUserId:
):
model_uuid, user_id = byom_model
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
with patch("docsgpt.core.settings.settings", s), patch(
"docsgpt.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
):
ModelRegistry()
@@ -197,8 +197,8 @@ class TestHelpersThreadUserId:
):
model_uuid, user_id = byom_model
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
with patch("docsgpt.core.settings.settings", s), patch(
"docsgpt.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
):
ModelRegistry()
@@ -213,8 +213,8 @@ class TestHelpersThreadUserId:
):
model_uuid, user_id = byom_model
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
with patch("docsgpt.core.settings.settings", s), patch(
"docsgpt.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
):
ModelRegistry()
@@ -238,7 +238,7 @@ class TestSecurityNoCredLeakOnUndecryptableBYOM:
def test_undecryptable_record_skipped_in_registry(
self, pg_conn, monkeypatch
):
from application.storage.db.repositories.user_custom_models import (
from docsgpt.storage.db.repositories.user_custom_models import (
UserCustomModelsRepository,
)
@@ -260,8 +260,8 @@ class TestSecurityNoCredLeakOnUndecryptableBYOM:
)
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
with patch("docsgpt.core.settings.settings", s), patch(
"docsgpt.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
):
ModelRegistry()
@@ -276,13 +276,13 @@ class TestSecurityNoCredLeakOnUndecryptableBYOM:
def test_llmcreator_refuses_user_model_with_no_api_key(self, pg_conn):
"""Belt-and-braces: even if a source=user AvailableModel slipped
into the registry without an api_key, LLMCreator must refuse."""
from application.core.model_settings import (
from docsgpt.core.model_settings import (
AvailableModel,
ModelProvider,
)
s = _make_settings()
with patch("application.core.settings.settings", s):
with patch("docsgpt.core.settings.settings", s):
reg = ModelRegistry()
# Inject a malformed BYOM record directly to bypass the
# registry-level guard tested above.
@@ -303,7 +303,7 @@ class TestSecurityNoCredLeakOnUndecryptableBYOM:
},
)
from application.llm.llm_creator import LLMCreator
from docsgpt.llm.llm_creator import LLMCreator
with pytest.raises(ValueError, match="no usable API key"):
LLMCreator.create_llm(
@@ -323,13 +323,13 @@ class TestSecurityDispatchSSRFGuard:
guard or were inserted via direct DB edits."""
def test_dispatch_rejects_user_model_with_unsafe_base_url(self, pg_conn):
from application.core.model_settings import (
from docsgpt.core.model_settings import (
AvailableModel,
ModelProvider,
)
s = _make_settings()
with patch("application.core.settings.settings", s):
with patch("docsgpt.core.settings.settings", s):
reg = ModelRegistry()
uuid = "0b7e0f4c-1234-5678-9abc-deadbeef0099"
_seed_user_layer(
@@ -348,7 +348,7 @@ class TestSecurityDispatchSSRFGuard:
},
)
from application.llm.llm_creator import LLMCreator
from docsgpt.llm.llm_creator import LLMCreator
with pytest.raises(ValueError, match="Refusing to dispatch"):
LLMCreator.create_llm(
@@ -361,7 +361,7 @@ class TestSecurityDispatchSSRFGuard:
def test_dispatch_allows_user_model_with_safe_base_url(self, pg_conn):
"""The SSRF guard must not break the happy path."""
from application.core.model_settings import (
from docsgpt.core.model_settings import (
AvailableModel,
ModelProvider,
)
@@ -374,8 +374,8 @@ class TestSecurityDispatchSSRFGuard:
captured["base_url"] = kw.get("base_url")
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.security.safe_url.socket.getaddrinfo"
with patch("docsgpt.core.settings.settings", s), patch(
"docsgpt.security.safe_url.socket.getaddrinfo"
) as gai:
gai.return_value = [(None, None, None, None, ("104.18.0.1", 0))]
reg = ModelRegistry()
@@ -396,8 +396,8 @@ class TestSecurityDispatchSSRFGuard:
},
)
from application.llm.llm_creator import LLMCreator
from application.llm.providers import PROVIDERS_BY_NAME
from docsgpt.llm.llm_creator import LLMCreator
from docsgpt.llm.providers import PROVIDERS_BY_NAME
with patch.object(
PROVIDERS_BY_NAME["openai_compatible"], "llm_class", _FakeLLM
@@ -423,11 +423,11 @@ class TestSecurityDispatchSSRFGuard:
rebinding window — the pinned client is what does."""
import httpx
from application.core.model_settings import (
from docsgpt.core.model_settings import (
AvailableModel,
ModelProvider,
)
from application.security.safe_url import _PinnedHTTPSTransport
from docsgpt.security.safe_url import _PinnedHTTPSTransport
captured: dict = {}
@@ -436,8 +436,8 @@ class TestSecurityDispatchSSRFGuard:
captured["http_client"] = kw.get("http_client")
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.security.safe_url.socket.getaddrinfo"
with patch("docsgpt.core.settings.settings", s), patch(
"docsgpt.security.safe_url.socket.getaddrinfo"
) as gai:
gai.return_value = [(None, None, None, None, ("104.18.0.1", 0))]
reg = ModelRegistry()
@@ -458,8 +458,8 @@ class TestSecurityDispatchSSRFGuard:
},
)
from application.llm.llm_creator import LLMCreator
from application.llm.providers import PROVIDERS_BY_NAME
from docsgpt.llm.llm_creator import LLMCreator
from docsgpt.llm.providers import PROVIDERS_BY_NAME
with patch.object(
PROVIDERS_BY_NAME["openai_compatible"], "llm_class", _FakeLLM
@@ -491,7 +491,7 @@ class TestSecurityDispatchSSRFGuard:
their endpoints are operator-trusted. Skipping avoids
unnecessary DNS lookups and keeps the SDK's default httpx
client behavior unchanged for the common path."""
from application.core.model_settings import (
from docsgpt.core.model_settings import (
AvailableModel,
ModelProvider,
)
@@ -503,7 +503,7 @@ class TestSecurityDispatchSSRFGuard:
captured["http_client"] = kw.get("http_client")
s = _make_settings()
with patch("application.core.settings.settings", s):
with patch("docsgpt.core.settings.settings", s):
reg = ModelRegistry()
uuid = "0b7e0f4c-1234-5678-9abc-deadbeef0102"
reg._builtin_models = {
@@ -517,8 +517,8 @@ class TestSecurityDispatchSSRFGuard:
)
}
from application.llm.llm_creator import LLMCreator
from application.llm.providers import PROVIDERS_BY_NAME
from docsgpt.llm.llm_creator import LLMCreator
from docsgpt.llm.providers import PROVIDERS_BY_NAME
with patch.object(
PROVIDERS_BY_NAME["openai_compatible"], "llm_class", _FakeLLM
@@ -544,10 +544,10 @@ class TestSharedAgentResolvesOwnerBYOM:
against the *owner's* per-user layer, not the caller's."""
def test_owner_byom_resolves_for_caller_request(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
from application.storage.db.repositories.user_custom_models import (
from docsgpt.storage.db.repositories.user_custom_models import (
UserCustomModelsRepository,
)
@@ -562,8 +562,8 @@ class TestSharedAgentResolvesOwnerBYOM:
)
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
with patch("docsgpt.core.settings.settings", s), patch(
"docsgpt.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
):
ModelRegistry()
@@ -588,9 +588,9 @@ class TestSharedAgentResolvesOwnerBYOM:
the rephrase LLM gets the owner's api_key/base_url and dispatches
the upstream model name (e.g. ``mistral-large-latest``) — not the
registry UUID — when called with chat history + active docs."""
from application.llm.providers import PROVIDERS_BY_NAME
from application.retriever.classic_rag import ClassicRAG
from application.storage.db.repositories.user_custom_models import (
from docsgpt.llm.providers import PROVIDERS_BY_NAME
from docsgpt.retriever.classic_rag import ClassicRAG
from docsgpt.storage.db.repositories.user_custom_models import (
UserCustomModelsRepository,
)
@@ -623,8 +623,8 @@ class TestSharedAgentResolvesOwnerBYOM:
)
s = _make_settings(API_KEY="instance-secret")
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
with patch("docsgpt.core.settings.settings", s), patch(
"docsgpt.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
):
ModelRegistry()
@@ -654,10 +654,10 @@ class TestSharedAgentResolvesOwnerBYOM:
agent record so _validate_and_set_model can reach the owner's
BYOM layer. Without this, shared-agent BYOM defaults silently
fall back to the system default for any non-owner caller."""
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
from application.storage.db.repositories.user_custom_models import (
from docsgpt.storage.db.repositories.user_custom_models import (
UserCustomModelsRepository,
)
@@ -671,8 +671,8 @@ class TestSharedAgentResolvesOwnerBYOM:
)
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
with patch("docsgpt.core.settings.settings", s), patch(
"docsgpt.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
):
ModelRegistry()
@@ -722,10 +722,10 @@ class TestCompressionThresholdHonorsByomContextWindow:
BYOM context windows are honored (not the default 128k limit)."""
def test_orchestrator_passes_user_id_to_should_compress(self, monkeypatch):
from application.api.answer.services.compression.orchestrator import (
from docsgpt.api.answer.services.compression.orchestrator import (
CompressionOrchestrator,
)
from application.api.answer.services.compression.threshold_checker import (
from docsgpt.api.answer.services.compression.threshold_checker import (
CompressionThresholdChecker,
)
@@ -770,9 +770,9 @@ class TestContinuationPreservesByomScope:
"""``resume_from_tool_actions`` reads ``model_user_id`` from the
saved ``agent_config`` and forwards it to LLMCreator so the
resumed dispatch resolves the owner's BYOM."""
from application.api.answer.services import stream_processor as sp_mod
from application.llm import llm_creator as llm_creator_mod
from application.llm.handlers import handler_creator as handler_mod
from docsgpt.api.answer.services import stream_processor as sp_mod
from docsgpt.llm import llm_creator as llm_creator_mod
from docsgpt.llm.handlers import handler_creator as handler_mod
captured: dict = {}
@@ -813,14 +813,14 @@ class TestContinuationPreservesByomScope:
# ContinuationService is imported lazily inside the method —
# patch the source module so the lookup at call time resolves
# to our mock.
from application.api.answer.services import (
from docsgpt.api.answer.services import (
continuation_service as cont_mod,
)
monkeypatch.setattr(
cont_mod, "ContinuationService", lambda: cont_service
)
from application.agents import tool_executor as te_mod
from docsgpt.agents import tool_executor as te_mod
monkeypatch.setattr(
te_mod, "ToolExecutor", lambda **kw: MagicMock(client_tools=None)
@@ -828,7 +828,7 @@ class TestContinuationPreservesByomScope:
# Stub AgentCreator.create_agent so we don't need a full agent
# tree — we're only exercising the LLMCreator forwarding.
from application.agents import agent_creator as ac_mod
from docsgpt.agents import agent_creator as ac_mod
monkeypatch.setattr(
ac_mod.AgentCreator, "create_agent", lambda *a, **kw: MagicMock()
@@ -867,7 +867,7 @@ class TestContinuationPreservesByomScope:
to the continuation store."""
import inspect
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
# complete_stream is the only callsite that constructs the
# save dict. Confirm the parameter is in scope and the key is
@@ -891,7 +891,7 @@ class TestStreamProcessorForwardsByomScopeToBudgetCalls:
sized against the default 128k, overfilling the upstream provider."""
def _make_processor(self, model_user_id="owner", initial_user_id="caller"):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
@@ -913,7 +913,7 @@ class TestStreamProcessorForwardsByomScopeToBudgetCalls:
def test_configure_retriever_passes_model_user_id_to_doc_budget(
self, monkeypatch
):
from application.api.answer.services import stream_processor as sp_mod
from docsgpt.api.answer.services import stream_processor as sp_mod
captured: dict = {}
@@ -939,7 +939,7 @@ class TestStreamProcessorForwardsByomScopeToBudgetCalls:
def test_load_conversation_history_passes_model_user_id_to_trim(
self, monkeypatch
):
from application.api.answer.services import stream_processor as sp_mod
from docsgpt.api.answer.services import stream_processor as sp_mod
captured: dict = {}
@@ -1001,7 +1001,7 @@ class TestBaseAgentTokenLimitUsesModelUserId:
owner-BYOM token-limit checks size against the caller's layer."""
def _build_agent(self, model_user_id="owner", caller="caller"):
from application.agents.classic_agent import ClassicAgent
from docsgpt.agents.classic_agent import ClassicAgent
# Stub LLM/handler/executor — we only exercise the get_token_limit
# callsites in BaseAgent.
@@ -1025,7 +1025,7 @@ class TestBaseAgentTokenLimitUsesModelUserId:
return 8000
monkeypatch.setattr(
"application.core.model_utils.get_token_limit", _fake
"docsgpt.core.model_utils.get_token_limit", _fake
)
agent = self._build_agent(model_user_id="owner", caller="caller")
@@ -1041,7 +1041,7 @@ class TestBaseAgentTokenLimitUsesModelUserId:
return 8000
monkeypatch.setattr(
"application.core.model_utils.get_token_limit", _fake
"docsgpt.core.model_utils.get_token_limit", _fake
)
agent = self._build_agent(model_user_id="owner", caller="caller")
@@ -1057,7 +1057,7 @@ class TestBaseAgentTokenLimitUsesModelUserId:
return 8000
monkeypatch.setattr(
"application.core.model_utils.get_token_limit", _fake
"docsgpt.core.model_utils.get_token_limit", _fake
)
agent = self._build_agent(model_user_id="owner", caller="caller")
@@ -1078,7 +1078,7 @@ class TestBaseAgentTokenLimitUsesModelUserId:
return 8000
monkeypatch.setattr(
"application.core.model_utils.get_token_limit", _fake
"docsgpt.core.model_utils.get_token_limit", _fake
)
agent = self._build_agent(model_user_id=None, caller="worker-owner")
@@ -1099,7 +1099,7 @@ class TestNonAgentCallSitesUseUpstreamId:
def test_handler_tool_loop_uses_upstream_id(self):
"""``handlers.base.handle_response`` re-calls the LLM after a
tool invocation. The model arg must be the upstream name."""
from application.llm.handlers.base import LLMHandler
from docsgpt.llm.handlers.base import LLMHandler
captured: dict = {}
@@ -1143,7 +1143,7 @@ class TestNonAgentCallSitesUseUpstreamId:
"""``conversation_service.save_conversation`` calls
``llm.gen(model=model_id, ...)`` to summarize. ``model_id`` is
the registry UUID; must use ``llm.model_id`` instead."""
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
@@ -1180,7 +1180,7 @@ class TestNonAgentCallSitesUseUpstreamId:
from inspect import getsourcefile
from pathlib import Path
from application.api.answer.services.compression.service import (
from docsgpt.api.answer.services.compression.service import (
CompressionService,
)
@@ -1194,7 +1194,7 @@ class TestNonAgentCallSitesUseUpstreamId:
from inspect import getsourcefile
from pathlib import Path
from application.retriever.classic_rag import ClassicRAG
from docsgpt.retriever.classic_rag import ClassicRAG
text = Path(getsourcefile(ClassicRAG)).read_text()
assert "getattr(self.llm" in text
@@ -1235,20 +1235,20 @@ class TestAgentSendsUpstreamModelId:
return None
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
with patch("docsgpt.core.settings.settings", s), patch(
"docsgpt.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
):
ModelRegistry()
from application.llm.providers import PROVIDERS_BY_NAME
from docsgpt.llm.providers import PROVIDERS_BY_NAME
with patch.object(
PROVIDERS_BY_NAME["openai_compatible"],
"llm_class",
_FakeOpenAILLM,
):
from application.agents.classic_agent import ClassicAgent
from docsgpt.agents.classic_agent import ClassicAgent
agent = ClassicAgent(
endpoint="https://x",
+3 -3
View File
@@ -1,4 +1,4 @@
"""Tests for ``application.core.db_uri``.
"""Tests for ``docsgpt.core.db_uri``.
DocsGPT has two Postgres connection strings — ``POSTGRES_URI`` (consumed
by SQLAlchemy) and ``PGVECTOR_CONNECTION_STRING`` (consumed by
@@ -8,7 +8,7 @@ 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
The normalizers live in ``docsgpt.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).
"""
@@ -17,7 +17,7 @@ from __future__ import annotations
import pytest
from application.core.db_uri import (
from docsgpt.core.db_uri import (
normalize_pgvector_connection_string,
normalize_postgres_uri,
)
+3 -3
View File
@@ -4,9 +4,9 @@ from unittest import mock
import pytest
from application.core import shutdown
from application.core.settings import settings
from application.gunicorn_worker import (
from docsgpt.core import shutdown
from docsgpt.core.settings import settings
from docsgpt.gunicorn_worker import (
BoundedDrainUvicornWorker,
_ShutdownAwareServer,
)
+3 -3
View File
@@ -6,8 +6,8 @@ import logging
import pytest
from application.core import log_context
from application.core.logging_config import _ContextFilter
from docsgpt.core import log_context
from docsgpt.core.logging_config import _ContextFilter
@pytest.fixture(autouse=True)
@@ -129,7 +129,7 @@ class TestFilterWiringEndToEnd:
"""
def test_propagated_record_gets_stamped(self):
from application.core.logging_config import _install_context_filter
from docsgpt.core.logging_config import _install_context_filter
captured: list[logging.LogRecord] = []
+10 -10
View File
@@ -1,7 +1,7 @@
"""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`.
logger before our module-level `setup_logging()` runs in `docsgpt/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.
@@ -17,7 +17,7 @@ from unittest.mock import Mock
import pytest
from application.core.logging_config import setup_logging
from docsgpt.core.logging_config import setup_logging
@pytest.fixture(autouse=True)
@@ -125,7 +125,7 @@ class TestAlembicDoesNotSilenceApplicationLoggers:
import ast
import pathlib
source = pathlib.Path("application/alembic/env.py").read_text()
source = pathlib.Path("docsgpt/alembic/env.py").read_text()
calls = [
node
for node in ast.walk(ast.parse(source))
@@ -152,12 +152,12 @@ class TestAlembicDoesNotSilenceApplicationLoggers:
probe.write_text(
"import logging\n"
"from logging.config import fileConfig\n"
"log = logging.getLogger('application.api.answer.routes.stream')\n"
"fileConfig('application/alembic.ini', disable_existing_loggers=True)\n"
"kept = logging.getLogger('application.api.answer.routes.stream')\n"
"log = logging.getLogger('docsgpt.api.answer.routes.stream')\n"
"fileConfig('docsgpt/alembic.ini', disable_existing_loggers=True)\n"
"kept = logging.getLogger('docsgpt.api.answer.routes.stream')\n"
"print('default_disables', kept.disabled)\n"
"kept.disabled = False\n"
"fileConfig('application/alembic.ini', disable_existing_loggers=False)\n"
"fileConfig('docsgpt/alembic.ini', disable_existing_loggers=False)\n"
"print('flag_preserves', not kept.disabled)\n"
)
out = subprocess.run(
@@ -184,7 +184,7 @@ class TestMigrationsDoNotClobberAppLogging:
def test_bootstrap_tells_env_py_to_leave_logging_alone(self, monkeypatch):
from logging.config import fileConfig
from application.storage.db import bootstrap
from docsgpt.storage.db import bootstrap
# Skip the best-effort revision precheck; it needs a live DB. It is
# wrapped in try/except, so raising here lands on the "upgrade anyway"
@@ -217,13 +217,13 @@ class TestMigrationsDoNotClobberAppLogging:
assert seen["configure_logger"] is False
assert root.level == before_level
assert marker in root.handlers
assert logging.getLogger("application.probe").isEnabledFor(logging.INFO)
assert logging.getLogger("docsgpt.probe").isEnabledFor(logging.INFO)
def test_env_py_honours_the_opt_out(self):
"""The other half of the contract lives in env.py's module-level guard."""
env_py = (
pathlib.Path(__file__).resolve().parents[2]
/ "application"
/ "docsgpt"
/ "alembic"
/ "env.py"
)
+16 -16
View File
@@ -16,8 +16,8 @@ from unittest.mock import MagicMock, patch
import pytest
from application.core.model_registry import ModelRegistry
from application.core.model_yaml import (
from docsgpt.core.model_registry import ModelRegistry
from docsgpt.core.model_yaml import (
BUILTIN_MODELS_DIR,
load_model_yamls,
)
@@ -167,7 +167,7 @@ class TestYAMLLoader:
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):
with patch("docsgpt.core.settings.settings", s):
reg = ModelRegistry()
ids = {m.id for m in reg.get_all_models()}
assert ids == EXPECTED_IDS["openai"] | EXPECTED_IDS["docsgpt"]
@@ -179,7 +179,7 @@ class TestRegistryPermutations:
LLM_PROVIDER="openai",
LLM_NAME="llama3,gemma",
)
with patch("application.core.settings.settings", s):
with patch("docsgpt.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
@@ -188,7 +188,7 @@ class TestRegistryPermutations:
def test_anthropic_only(self):
s = _make_settings(ANTHROPIC_API_KEY="sk-ant")
with patch("application.core.settings.settings", s):
with patch("docsgpt.core.settings.settings", s):
reg = ModelRegistry()
ids = {m.id for m in reg.get_all_models()}
assert ids == EXPECTED_IDS["anthropic"] | EXPECTED_IDS["docsgpt"]
@@ -200,7 +200,7 @@ class TestRegistryPermutations:
s = _make_settings(
LLM_PROVIDER="anthropic", API_KEY="key", LLM_NAME="claude-haiku-4-5"
)
with patch("application.core.settings.settings", s):
with patch("docsgpt.core.settings.settings", s):
reg = ModelRegistry()
anthropic_ids = {
m.id for m in reg.get_all_models() if m.provider.value == "anthropic"
@@ -209,42 +209,42 @@ class TestRegistryPermutations:
def test_google_only(self):
s = _make_settings(GOOGLE_API_KEY="g-test")
with patch("application.core.settings.settings", s):
with patch("docsgpt.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):
with patch("docsgpt.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):
with patch("docsgpt.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):
with patch("docsgpt.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):
with patch("docsgpt.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):
with patch("docsgpt.core.settings.settings", s):
reg = ModelRegistry()
ids = {m.id for m in reg.get_all_models()}
assert ids == EXPECTED_IDS["docsgpt"]
@@ -261,7 +261,7 @@ class TestRegistryPermutations:
HUGGINGFACE_API_KEY="x",
OPENAI_API_BASE="x",
)
with patch("application.core.settings.settings", s):
with patch("docsgpt.core.settings.settings", s):
reg = ModelRegistry()
ids = {m.id for m in reg.get_all_models()}
all_expected = set()
@@ -279,13 +279,13 @@ class TestDefaultModelResolution:
s = _make_settings(
ANTHROPIC_API_KEY="sk-ant", LLM_NAME="claude-opus-4-7"
)
with patch("application.core.settings.settings", s):
with patch("docsgpt.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):
with patch("docsgpt.core.settings.settings", s):
reg = ModelRegistry()
assert reg.default_model_id is not None
assert reg.default_model_id in reg.models
@@ -298,7 +298,7 @@ class TestDefaultModelResolution:
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):
with patch("docsgpt.core.settings.settings", s):
reg = ModelRegistry()
# All lookup methods must accept user_id (currently ignored,
# reserved for end-user BYOM).
+4 -4
View File
@@ -1,9 +1,9 @@
"""Tests for application/core/model_settings.py.
"""Tests for docsgpt/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 +
``docsgpt/llm/providers/`` and YAML catalogs under
``docsgpt/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``,
@@ -15,7 +15,7 @@ from unittest.mock import patch
import pytest
from application.core.model_settings import (
from docsgpt.core.model_settings import (
AvailableModel,
ModelCapabilities,
ModelProvider,
+51 -51
View File
@@ -2,7 +2,7 @@ from unittest.mock import MagicMock, patch
import pytest
from application.core.model_settings import (
from docsgpt.core.model_settings import (
AvailableModel,
ModelCapabilities,
ModelProvider,
@@ -51,11 +51,11 @@ def _make_model(
class TestGetApiKeyForProvider:
"""settings is lazily imported inside the function body, so we patch
at application.core.settings.settings (the actual module attribute)."""
at docsgpt.core.settings.settings (the actual module attribute)."""
@pytest.mark.unit
def test_openai_key(self):
with patch("application.core.settings.settings") as mock_settings:
with patch("docsgpt.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
@@ -65,94 +65,94 @@ class TestGetApiKeyForProvider:
mock_settings.GROQ_API_KEY = None
mock_settings.HUGGINGFACE_API_KEY = None
from application.core.model_utils import get_api_key_for_provider
from docsgpt.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:
with patch("docsgpt.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
from docsgpt.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:
with patch("docsgpt.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
from docsgpt.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:
with patch("docsgpt.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
from docsgpt.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:
with patch("docsgpt.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
from docsgpt.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:
with patch("docsgpt.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
from docsgpt.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:
with patch("docsgpt.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
from docsgpt.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:
with patch("docsgpt.core.settings.settings") as mock_settings:
mock_settings.API_KEY = "sk-fallback"
from application.core.model_utils import get_api_key_for_provider
from docsgpt.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:
with patch("docsgpt.core.settings.settings") as mock_settings:
mock_settings.API_KEY = "sk-fallback"
from application.core.model_utils import get_api_key_for_provider
from docsgpt.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:
with patch("docsgpt.core.settings.settings") as mock_settings:
mock_settings.API_KEY = "sk-fallback"
from application.core.model_utils import get_api_key_for_provider
from docsgpt.core.model_utils import get_api_key_for_provider
assert get_api_key_for_provider("unknown_provider") == "sk-fallback"
@@ -161,7 +161,7 @@ class TestGetApiKeyForProvider:
class TestGetAllAvailableModels:
@pytest.mark.unit
@patch("application.core.model_utils.ModelRegistry.get_instance")
@patch("docsgpt.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")
@@ -169,7 +169,7 @@ class TestGetAllAvailableModels:
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
from docsgpt.core.model_utils import get_all_available_models
result = get_all_available_models()
@@ -179,13 +179,13 @@ class TestGetAllAvailableModels:
assert result["model-b"]["display_name"] == "Model B"
@pytest.mark.unit
@patch("application.core.model_utils.ModelRegistry.get_instance")
@patch("docsgpt.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
from docsgpt.core.model_utils import get_all_available_models
assert get_all_available_models() == {}
@@ -195,24 +195,24 @@ class TestGetAllAvailableModels:
class TestValidateModelId:
@pytest.mark.unit
@patch("application.core.model_utils.ModelRegistry.get_instance")
@patch("docsgpt.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
from docsgpt.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")
@patch("docsgpt.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
from docsgpt.core.model_utils import validate_model_id
assert validate_model_id("nonexistent") is False
@@ -222,7 +222,7 @@ class TestValidateModelId:
class TestGetModelCapabilities:
@pytest.mark.unit
@patch("application.core.model_utils.ModelRegistry.get_instance")
@patch("docsgpt.core.model_utils.ModelRegistry.get_instance")
def test_model_found(self, mock_get_instance):
model = _make_model(
"gpt-4",
@@ -235,7 +235,7 @@ class TestGetModelCapabilities:
mock_registry.get_model.return_value = model
mock_get_instance.return_value = mock_registry
from application.core.model_utils import get_model_capabilities
from docsgpt.core.model_utils import get_model_capabilities
caps = get_model_capabilities("gpt-4")
@@ -246,13 +246,13 @@ class TestGetModelCapabilities:
assert caps["context_window"] == 8192
@pytest.mark.unit
@patch("application.core.model_utils.ModelRegistry.get_instance")
@patch("docsgpt.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
from docsgpt.core.model_utils import get_model_capabilities
assert get_model_capabilities("nonexistent") is None
@@ -262,13 +262,13 @@ class TestGetModelCapabilities:
class TestGetDefaultModelId:
@pytest.mark.unit
@patch("application.core.model_utils.ModelRegistry.get_instance")
@patch("docsgpt.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
from docsgpt.core.model_utils import get_default_model_id
assert get_default_model_id() == "gpt-4"
@@ -278,25 +278,25 @@ class TestGetDefaultModelId:
class TestGetProviderFromModelId:
@pytest.mark.unit
@patch("application.core.model_utils.ModelRegistry.get_instance")
@patch("docsgpt.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
from docsgpt.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")
@patch("docsgpt.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
from docsgpt.core.model_utils import get_provider_from_model_id
assert get_provider_from_model_id("nonexistent") is None
@@ -306,28 +306,28 @@ class TestGetProviderFromModelId:
class TestGetTokenLimit:
@pytest.mark.unit
@patch("application.core.model_utils.ModelRegistry.get_instance")
@patch("docsgpt.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
from docsgpt.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")
@patch("docsgpt.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:
with patch("docsgpt.core.settings.settings") as mock_settings:
mock_settings.DEFAULT_LLM_TOKEN_LIMIT = 128000
from application.core.model_utils import get_token_limit
from docsgpt.core.model_utils import get_token_limit
assert get_token_limit("nonexistent") == 128000
@@ -337,36 +337,36 @@ class TestGetTokenLimit:
class TestGetBaseUrlForModel:
@pytest.mark.unit
@patch("application.core.model_utils.ModelRegistry.get_instance")
@patch("docsgpt.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
from docsgpt.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")
@patch("docsgpt.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
from docsgpt.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")
@patch("docsgpt.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
from docsgpt.core.model_utils import get_base_url_for_model
assert get_base_url_for_model("nonexistent") is None
+7 -7
View File
@@ -15,7 +15,7 @@ from unittest.mock import MagicMock, patch
import pytest
from application.core.model_registry import ModelRegistry
from docsgpt.core.model_registry import ModelRegistry
def _make_settings(**overrides):
@@ -67,7 +67,7 @@ class TestOperatorAddsNewProvider:
monkeypatch.setenv("FIREWORKS_API_KEY", "fw-key")
s = _make_settings(MODELS_CONFIG_DIR=str(tmp_path))
with patch("application.core.settings.settings", s):
with patch("docsgpt.core.settings.settings", s):
reg = ModelRegistry()
m = reg.get_model("accounts/fireworks/models/llama-v3p3-70b-instruct")
@@ -99,7 +99,7 @@ class TestOperatorExtendsExistingProvider:
ANTHROPIC_API_KEY="sk-ant",
MODELS_CONFIG_DIR=str(tmp_path),
)
with patch("application.core.settings.settings", s):
with patch("docsgpt.core.settings.settings", s):
reg = ModelRegistry()
# Built-in models still present
@@ -137,7 +137,7 @@ class TestOperatorOverridesBuiltinCapabilities:
MODELS_CONFIG_DIR=str(tmp_path),
)
with caplog.at_level(logging.WARNING):
with patch("application.core.settings.settings", s):
with patch("docsgpt.core.settings.settings", s):
reg = ModelRegistry()
m = reg.get_model("claude-haiku-4-5")
@@ -164,7 +164,7 @@ class TestMisconfiguredOperatorDir:
s = _make_settings(MODELS_CONFIG_DIR=str(bogus))
with caplog.at_level(logging.WARNING):
with patch("application.core.settings.settings", s):
with patch("docsgpt.core.settings.settings", s):
reg = ModelRegistry()
# Built-in catalog still loaded
@@ -178,7 +178,7 @@ class TestMisconfiguredOperatorDir:
s = _make_settings(MODELS_CONFIG_DIR=str(afile))
with caplog.at_level(logging.WARNING):
with patch("application.core.settings.settings", s):
with patch("docsgpt.core.settings.settings", s):
reg = ModelRegistry()
assert reg.get_model("docsgpt-local") is not None
@@ -199,7 +199,7 @@ class TestOperatorValidation:
"""))
s = _make_settings(MODELS_CONFIG_DIR=str(tmp_path))
with patch("application.core.settings.settings", s):
with patch("docsgpt.core.settings.settings", s):
with pytest.raises(Exception) as exc_info:
ModelRegistry()
# Could be ModelYAMLError (enum check) or ValueError (registry check);
+16 -16
View File
@@ -14,9 +14,9 @@ from unittest.mock import MagicMock, patch
import pytest
from application.core.model_registry import ModelRegistry
from application.core.model_settings import ModelProvider
from application.core.model_yaml import BUILTIN_MODELS_DIR, load_model_yamls
from docsgpt.core.model_registry import ModelRegistry
from docsgpt.core.model_settings import ModelProvider
from docsgpt.core.model_yaml import BUILTIN_MODELS_DIR, load_model_yamls
def _make_settings(**overrides):
@@ -102,7 +102,7 @@ class TestYAMLCompatibleProvider:
monkeypatch.setenv("MISTRAL_API_KEY", "sk-mistral-test")
s = _make_settings(MODELS_CONFIG_DIR=str(tmp_path))
with patch("application.core.settings.settings", s):
with patch("docsgpt.core.settings.settings", s):
reg = ModelRegistry()
m = reg.get_model("mistral-large-latest")
@@ -121,7 +121,7 @@ class TestYAMLCompatibleProvider:
monkeypatch.delenv("MISTRAL_API_KEY", raising=False)
s = _make_settings(MODELS_CONFIG_DIR=str(tmp_path))
with patch("application.core.settings.settings", s):
with patch("docsgpt.core.settings.settings", s):
reg = ModelRegistry()
# Catalog skipped when no key — no Mistral models in the registry
@@ -136,7 +136,7 @@ class TestYAMLCompatibleProvider:
monkeypatch.setenv("TOGETHER_API_KEY", "sk-together")
s = _make_settings(MODELS_CONFIG_DIR=str(tmp_path))
with patch("application.core.settings.settings", s):
with patch("docsgpt.core.settings.settings", s):
reg = ModelRegistry()
mistral = reg.get_model("mistral-large-latest")
@@ -159,7 +159,7 @@ class TestYAMLCompatibleProvider:
monkeypatch.delenv("TOGETHER_API_KEY", raising=False)
s = _make_settings(MODELS_CONFIG_DIR=str(tmp_path))
with patch("application.core.settings.settings", s):
with patch("docsgpt.core.settings.settings", s):
reg = ModelRegistry()
assert reg.get_model("mistral-large-latest") is not None
@@ -177,7 +177,7 @@ class TestYAMLCompatibleProvider:
monkeypatch.setenv("SOME_KEY", "k")
s = _make_settings(MODELS_CONFIG_DIR=str(tmp_path))
with patch("application.core.settings.settings", s):
with patch("docsgpt.core.settings.settings", s):
with pytest.raises(ValueError, match="must set 'base_url'"):
ModelRegistry()
@@ -192,7 +192,7 @@ class TestYAMLCompatibleProvider:
"""))
s = _make_settings(MODELS_CONFIG_DIR=str(tmp_path))
with patch("application.core.settings.settings", s):
with patch("docsgpt.core.settings.settings", s):
with pytest.raises(ValueError, match="must set 'api_key_env'"):
ModelRegistry()
@@ -203,7 +203,7 @@ class TestYAMLCompatibleProvider:
monkeypatch.setenv("MISTRAL_API_KEY", "sk")
s = _make_settings(MODELS_CONFIG_DIR=str(tmp_path))
with patch("application.core.settings.settings", s):
with patch("docsgpt.core.settings.settings", s):
reg = ModelRegistry()
d = reg.get_model("mistral-large-latest").to_dict()
@@ -233,7 +233,7 @@ class TestYAMLCompatibleProvider:
monkeypatch.setenv("MISTRAL_API_KEY", "sk")
s = _make_settings(MODELS_CONFIG_DIR=str(tmp_path))
with patch("application.core.settings.settings", s):
with patch("docsgpt.core.settings.settings", s):
reg = ModelRegistry()
assert (
@@ -254,7 +254,7 @@ class TestLegacyOpenAIBaseURLPath:
LLM_PROVIDER="openai",
LLM_NAME="llama3,gemma",
)
with patch("application.core.settings.settings", s):
with patch("docsgpt.core.settings.settings", s):
reg = ModelRegistry()
ids = {m.id for m in reg.get_all_models()}
@@ -276,7 +276,7 @@ class TestLegacyOpenAIBaseURLPath:
LLM_PROVIDER="openai",
LLM_NAME="llama3",
)
with patch("application.core.settings.settings", s):
with patch("docsgpt.core.settings.settings", s):
reg = ModelRegistry()
assert reg.get_model("llama3").api_key == "sk-generic"
@@ -307,20 +307,20 @@ class TestLLMCreatorDispatch:
captured["base_url"] = kwargs.get("base_url")
captured["model_id"] = kwargs.get("model_id")
with patch("application.core.settings.settings", s):
with patch("docsgpt.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
from docsgpt.llm.providers import PROVIDERS_BY_NAME
with patch.object(
PROVIDERS_BY_NAME["openai_compatible"],
"llm_class",
_FakeLLM,
):
from application.llm.llm_creator import LLMCreator
from docsgpt.llm.llm_creator import LLMCreator
LLMCreator.create_llm(
type="openai_compatible",
+1 -1
View File
@@ -6,7 +6,7 @@ from unittest.mock import patch
import pytest
from application.core import optional_deps
from docsgpt.core import optional_deps
class TestExtras:
+39 -39
View File
@@ -13,9 +13,9 @@ 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 (
from docsgpt.core.model_registry import ModelRegistry
from docsgpt.core.model_settings import ModelProvider
from docsgpt.storage.db.repositories.user_custom_models import (
UserCustomModelsRepository,
)
@@ -66,8 +66,8 @@ class TestPerUserLayer:
)
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
with patch("docsgpt.core.settings.settings", s), patch(
"docsgpt.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
):
reg = ModelRegistry()
@@ -87,8 +87,8 @@ class TestPerUserLayer:
)
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
with patch("docsgpt.core.settings.settings", s), patch(
"docsgpt.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
):
reg = ModelRegistry()
@@ -110,8 +110,8 @@ class TestPerUserLayer:
)
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
with patch("docsgpt.core.settings.settings", s), patch(
"docsgpt.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
):
reg = ModelRegistry()
@@ -144,11 +144,11 @@ class TestPerUserLayer:
# 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",
with patch("docsgpt.core.settings.settings", s), patch(
"docsgpt.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
), patch(
"application.cache.get_redis_instance", return_value=fake_redis
"docsgpt.cache.get_redis_instance", return_value=fake_redis
):
reg = ModelRegistry()
assert reg.get_model(created["id"], user_id="user-1") is not None
@@ -185,17 +185,17 @@ class TestLLMCreatorDispatchUsesUpstreamModelId:
captured["model_id"] = kwargs.get("model_id")
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
with patch("docsgpt.core.settings.settings", s), patch(
"docsgpt.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
):
ModelRegistry()
from application.llm.providers import PROVIDERS_BY_NAME
from docsgpt.llm.providers import PROVIDERS_BY_NAME
with patch.object(
PROVIDERS_BY_NAME["openai_compatible"], "llm_class", _FakeLLM
):
from application.llm.llm_creator import LLMCreator
from docsgpt.llm.llm_creator import LLMCreator
LLMCreator.create_llm(
type="openai_compatible",
@@ -239,17 +239,17 @@ class TestLLMCreatorDispatchUsesUpstreamModelId:
captured["capabilities"] = kwargs.get("capabilities")
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
with patch("docsgpt.core.settings.settings", s), patch(
"docsgpt.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
):
ModelRegistry()
from application.llm.providers import PROVIDERS_BY_NAME
from docsgpt.llm.providers import PROVIDERS_BY_NAME
with patch.object(
PROVIDERS_BY_NAME["openai_compatible"], "llm_class", _FakeLLM
):
from application.llm.llm_creator import LLMCreator
from docsgpt.llm.llm_creator import LLMCreator
LLMCreator.create_llm(
type="openai_compatible",
@@ -285,8 +285,8 @@ class TestLLMCreatorDispatchUsesUpstreamModelId:
)
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
with patch("docsgpt.core.settings.settings", s), patch(
"docsgpt.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
):
reg = ModelRegistry()
@@ -321,8 +321,8 @@ class TestLLMCreatorDispatchUsesUpstreamModelId:
)
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
with patch("docsgpt.core.settings.settings", s), patch(
"docsgpt.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
):
reg = ModelRegistry()
@@ -359,11 +359,11 @@ class TestCrossProcessInvalidation:
fake_redis = MagicMock()
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
with patch("docsgpt.core.settings.settings", s), patch(
"docsgpt.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
), patch(
"application.cache.get_redis_instance", return_value=fake_redis
"docsgpt.cache.get_redis_instance", return_value=fake_redis
):
ModelRegistry().get_model("anything", user_id="user-1")
ModelRegistry.invalidate_user("user-1")
@@ -375,7 +375,7 @@ class TestCrossProcessInvalidation:
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
from docsgpt.core import model_registry as registry_mod
repo = UserCustomModelsRepository(pg_conn)
created = repo.create(
@@ -401,11 +401,11 @@ class TestCrossProcessInvalidation:
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",
with patch("docsgpt.core.settings.settings", s), patch(
"docsgpt.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
), patch(
"application.cache.get_redis_instance", return_value=_FakeRedis()
"docsgpt.cache.get_redis_instance", return_value=_FakeRedis()
), patch.object(
registry_mod, "_USER_CACHE_TTL_SECONDS", 0.0
):
@@ -433,7 +433,7 @@ class TestCrossProcessInvalidation:
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
from docsgpt.core import model_registry as registry_mod
repo = UserCustomModelsRepository(pg_conn)
created = repo.create(
@@ -445,11 +445,11 @@ class TestCrossProcessInvalidation:
)
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
with patch("docsgpt.core.settings.settings", s), patch(
"docsgpt.storage.db.session.db_readonly",
lambda: _yield(pg_conn),
), patch(
"application.cache.get_redis_instance", return_value=None
"docsgpt.cache.get_redis_instance", return_value=None
), patch.object(
registry_mod, "_USER_CACHE_TTL_SECONDS", 0.0
):
@@ -469,7 +469,7 @@ class TestCrossProcessInvalidation:
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
from docsgpt.core import model_registry as registry_mod
repo = UserCustomModelsRepository(pg_conn)
created = repo.create(
@@ -491,11 +491,11 @@ class TestCrossProcessInvalidation:
yield pg_conn
s = _make_settings()
with patch("application.core.settings.settings", s), patch(
"application.storage.db.session.db_readonly",
with patch("docsgpt.core.settings.settings", s), patch(
"docsgpt.storage.db.session.db_readonly",
_counting_db_readonly,
), patch(
"application.cache.get_redis_instance", return_value=fake_redis
"docsgpt.cache.get_redis_instance", return_value=fake_redis
), patch.object(
registry_mod, "_USER_CACHE_TTL_SECONDS", 0.0
):
+3 -3
View File
@@ -4,7 +4,7 @@ import pytest
def test_configured_secret_is_used_without_touching_the_filesystem(tmp_path):
from application.core.secret_key import resolve_jwt_secret_key
from docsgpt.core.secret_key import resolve_jwt_secret_key
key_file = tmp_path / "missing" / "jwt-secret"
@@ -16,14 +16,14 @@ def test_configured_secret_is_used_without_touching_the_filesystem(tmp_path):
def test_cloud_deployment_requires_an_explicit_shared_secret(tmp_path):
from application.core.secret_key import resolve_jwt_secret_key
from docsgpt.core.secret_key import resolve_jwt_secret_key
with pytest.raises(RuntimeError, match="JWT_SECRET_KEY must be set"):
resolve_jwt_secret_key("", "cloud", tmp_path / "jwt-secret")
def test_local_secret_is_created_once_with_owner_only_permissions(tmp_path):
from application.core.secret_key import resolve_jwt_secret_key
from docsgpt.core.secret_key import resolve_jwt_secret_key
key_file = tmp_path / "jwt-secret"
+1 -1
View File
@@ -2,7 +2,7 @@
import pytest
from application.core import shutdown
from docsgpt.core import shutdown
@pytest.fixture(autouse=True)
+11 -11
View File
@@ -3,7 +3,7 @@
import pytest
from unittest.mock import patch
from application.core.url_validation import (
from docsgpt.core.url_validation import (
SSRFError,
validate_url,
validate_url_safe,
@@ -62,13 +62,13 @@ 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:
with patch("docsgpt.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:
with patch("docsgpt.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"
@@ -129,14 +129,14 @@ class TestValidateUrl:
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:
with patch("docsgpt.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:
with patch("docsgpt.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")
@@ -147,13 +147,13 @@ class TestValidateUrl:
assert result == "http://8.8.8.8"
def test_allows_public_hostname(self):
with patch("application.core.url_validation.resolve_hostname") as mock_resolve:
with patch("docsgpt.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:
with patch("docsgpt.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")
@@ -177,7 +177,7 @@ 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:
with patch("docsgpt.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
@@ -197,7 +197,7 @@ class TestValidateUrlSafe:
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:
with patch("docsgpt.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
@@ -235,7 +235,7 @@ class TestValidateUrlExtended:
validate_url("http://metadata")
def test_allows_localhost_with_flag(self):
with patch("application.core.url_validation.resolve_hostname") as mock_resolve:
with patch("docsgpt.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
@@ -251,7 +251,7 @@ class TestValidateUrlExtended:
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:
with patch("docsgpt.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")