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

+55 -55
View File
@@ -2,7 +2,7 @@ from unittest.mock import Mock, patch
import pytest
from application.vectorstore.base import (
from docsgpt.vectorstore.base import (
BaseVectorStore,
EmbeddingsSingleton,
RemoteEmbeddings,
@@ -30,7 +30,7 @@ class TestRemoteEmbeddings:
emb = RemoteEmbeddings(api_url="http://host", model_name="m")
assert "Authorization" not in emb.headers
@patch("application.vectorstore.base.requests.post")
@patch("docsgpt.vectorstore.base.requests.post")
def test_embed_sends_correct_payload(self, mock_post):
mock_resp = Mock()
mock_resp.json.return_value = {
@@ -48,7 +48,7 @@ class TestRemoteEmbeddings:
assert call_kwargs[1]["json"]["model"] == "model-v1"
assert result == [[0.1, 0.2]]
@patch("application.vectorstore.base.requests.post")
@patch("docsgpt.vectorstore.base.requests.post")
def test_embed_sorts_by_index(self, mock_post):
mock_resp = Mock()
mock_resp.json.return_value = {
@@ -64,7 +64,7 @@ class TestRemoteEmbeddings:
result = emb._embed(["a", "b"])
assert result == [[0.1, 0.2], [0.3, 0.4]]
@patch("application.vectorstore.base.requests.post")
@patch("docsgpt.vectorstore.base.requests.post")
def test_embed_raises_on_error_response(self, mock_post):
mock_resp = Mock()
mock_resp.json.return_value = {"error": "rate limit exceeded"}
@@ -75,7 +75,7 @@ class TestRemoteEmbeddings:
with pytest.raises(ValueError, match="rate limit exceeded"):
emb._embed("test")
@patch("application.vectorstore.base.requests.post")
@patch("docsgpt.vectorstore.base.requests.post")
def test_embed_raises_on_unexpected_format(self, mock_post):
mock_resp = Mock()
mock_resp.json.return_value = {"unexpected": True}
@@ -86,7 +86,7 @@ class TestRemoteEmbeddings:
with pytest.raises(ValueError, match="Unexpected response format"):
emb._embed("test")
@patch("application.vectorstore.base.requests.post")
@patch("docsgpt.vectorstore.base.requests.post")
def test_embed_raises_on_non_dict_response(self, mock_post):
mock_resp = Mock()
mock_resp.json.return_value = [1, 2, 3]
@@ -97,7 +97,7 @@ class TestRemoteEmbeddings:
with pytest.raises(ValueError, match="Unexpected response format"):
emb._embed("test")
@patch("application.vectorstore.base.requests.post")
@patch("docsgpt.vectorstore.base.requests.post")
def test_embed_query(self, mock_post):
mock_resp = Mock()
mock_resp.json.return_value = {
@@ -112,7 +112,7 @@ class TestRemoteEmbeddings:
assert result == [0.1, 0.2, 0.3]
assert emb.dimension == 3
@patch("application.vectorstore.base.requests.post")
@patch("docsgpt.vectorstore.base.requests.post")
def test_embed_query_raises_on_bad_structure(self, mock_post):
mock_resp = Mock()
# Return multiple embeddings for a single query
@@ -129,7 +129,7 @@ class TestRemoteEmbeddings:
with pytest.raises(ValueError, match="Unexpected result structure"):
emb.embed_query("hello")
@patch("application.vectorstore.base.requests.post")
@patch("docsgpt.vectorstore.base.requests.post")
def test_embed_documents(self, mock_post):
mock_resp = Mock()
mock_resp.json.return_value = {
@@ -151,7 +151,7 @@ class TestRemoteEmbeddings:
emb = RemoteEmbeddings("http://host", "m")
assert emb.embed_documents([]) == []
@patch("application.vectorstore.base.requests.post")
@patch("docsgpt.vectorstore.base.requests.post")
def test_call_with_string(self, mock_post):
mock_resp = Mock()
mock_resp.json.return_value = {
@@ -164,7 +164,7 @@ class TestRemoteEmbeddings:
result = emb("hello")
assert result == [0.5]
@patch("application.vectorstore.base.requests.post")
@patch("docsgpt.vectorstore.base.requests.post")
def test_call_with_list(self, mock_post):
mock_resp = Mock()
mock_resp.json.return_value = {
@@ -191,7 +191,7 @@ class TestEmbeddingsSingleton:
def setup_method(self):
EmbeddingsSingleton._instances = {}
@patch("application.vectorstore.base.OpenAIEmbeddings")
@patch("docsgpt.vectorstore.base.OpenAIEmbeddings")
def test_get_instance_openai(self, mock_openai_cls):
mock_instance = Mock()
mock_openai_cls.return_value = mock_instance
@@ -199,7 +199,7 @@ class TestEmbeddingsSingleton:
result = EmbeddingsSingleton.get_instance("openai_text-embedding-ada-002")
assert result is mock_instance
@patch("application.vectorstore.base.OpenAIEmbeddings")
@patch("docsgpt.vectorstore.base.OpenAIEmbeddings")
def test_singleton_returns_same_instance(self, mock_openai_cls):
mock_instance = Mock()
mock_openai_cls.return_value = mock_instance
@@ -209,7 +209,7 @@ class TestEmbeddingsSingleton:
assert r1 is r2
mock_openai_cls.assert_called_once()
@patch("application.vectorstore.base._get_embeddings_wrapper")
@patch("docsgpt.vectorstore.base._get_embeddings_wrapper")
def test_get_instance_huggingface(self, mock_get_wrapper):
mock_wrapper_cls = Mock()
mock_instance = Mock()
@@ -221,7 +221,7 @@ class TestEmbeddingsSingleton:
)
assert result is mock_instance
@patch("application.vectorstore.base._get_embeddings_wrapper")
@patch("docsgpt.vectorstore.base._get_embeddings_wrapper")
def test_get_instance_unknown_falls_back_to_wrapper(self, mock_get_wrapper):
mock_wrapper_cls = Mock()
mock_instance = Mock()
@@ -232,7 +232,7 @@ class TestEmbeddingsSingleton:
mock_wrapper_cls.assert_called_once_with("custom_model_name")
assert result is mock_instance
@patch("application.vectorstore.base.settings")
@patch("docsgpt.vectorstore.base.settings")
def test_get_instance_uses_remote_when_base_url_set(self, mock_settings):
"""Direct callers (GraphRAG, semantic chunking) must route to the
remote embeddings API instead of loading a local model."""
@@ -246,7 +246,7 @@ class TestEmbeddingsSingleton:
assert result.model_name == "embeddinggemma"
assert result.headers["Authorization"] == "Bearer sk-remote"
@patch("application.vectorstore.base.settings")
@patch("docsgpt.vectorstore.base.settings")
def test_get_instance_remote_falls_back_to_settings_key(self, mock_settings):
"""When no key is passed, the remote dispatch uses EMBEDDINGS_KEY."""
mock_settings.EMBEDDINGS_BASE_URL = "http://remote:8080"
@@ -258,8 +258,8 @@ class TestEmbeddingsSingleton:
assert result.headers["Authorization"] == "Bearer sk-from-settings"
@patch("application.vectorstore.base.settings")
@patch("application.vectorstore.base._get_embeddings_wrapper")
@patch("docsgpt.vectorstore.base.settings")
@patch("docsgpt.vectorstore.base._get_embeddings_wrapper")
def test_get_instance_hf_ignores_positional_key(
self, mock_get_wrapper, mock_settings
):
@@ -281,8 +281,8 @@ class TestEmbeddingsSingleton:
# The configured name is passed through; the registry maps it to a repo.
mock_wrapper_cls.assert_called_once_with(HF_MPNET)
@patch("application.vectorstore.base.settings")
@patch("application.vectorstore.base._get_embeddings_wrapper")
@patch("docsgpt.vectorstore.base.settings")
@patch("docsgpt.vectorstore.base._get_embeddings_wrapper")
def test_get_instance_hf_ignores_keyword_args(
self, mock_get_wrapper, mock_settings
):
@@ -321,7 +321,7 @@ class TestBaseVectorStore:
assert store.add_chunk("text") is None
assert store.delete_chunk("id") is None
@patch("application.vectorstore.base.settings")
@patch("docsgpt.vectorstore.base.settings")
def test_is_azure_configured_true(self, mock_settings):
mock_settings.OPENAI_API_BASE = "https://azure.openai.com"
mock_settings.OPENAI_API_VERSION = "2023-05-15"
@@ -330,7 +330,7 @@ class TestBaseVectorStore:
store = ConcreteVectorStore()
assert store.is_azure_configured()
@patch("application.vectorstore.base.settings")
@patch("docsgpt.vectorstore.base.settings")
def test_is_azure_configured_false(self, mock_settings):
mock_settings.OPENAI_API_BASE = None
mock_settings.OPENAI_API_VERSION = None
@@ -339,7 +339,7 @@ class TestBaseVectorStore:
store = ConcreteVectorStore()
assert not store.is_azure_configured()
@patch("application.vectorstore.base.settings")
@patch("docsgpt.vectorstore.base.settings")
def test_get_embeddings_remote(self, mock_settings):
mock_settings.EMBEDDINGS_BASE_URL = "http://remote:8080"
@@ -349,8 +349,8 @@ class TestBaseVectorStore:
assert isinstance(result, RemoteEmbeddings)
assert result.api_url == "http://remote:8080"
@patch("application.vectorstore.base.settings")
@patch("application.vectorstore.base.EmbeddingsSingleton.get_instance")
@patch("docsgpt.vectorstore.base.settings")
@patch("docsgpt.vectorstore.base.EmbeddingsSingleton.get_instance")
def test_get_embeddings_openai(self, mock_get_instance, mock_settings):
mock_settings.EMBEDDINGS_BASE_URL = None
mock_settings.OPENAI_API_BASE = None
@@ -364,8 +364,8 @@ class TestBaseVectorStore:
result = store._get_embeddings("openai_text-embedding-ada-002", "sk-key")
assert result is mock_emb
@patch("application.vectorstore.base.settings")
@patch("application.vectorstore.base.EmbeddingsSingleton.get_instance")
@patch("docsgpt.vectorstore.base.settings")
@patch("docsgpt.vectorstore.base.EmbeddingsSingleton.get_instance")
def test_get_embeddings_openai_azure(self, mock_get_instance, mock_settings):
mock_settings.EMBEDDINGS_BASE_URL = None
mock_settings.OPENAI_API_BASE = "https://azure.openai.com"
@@ -380,8 +380,8 @@ class TestBaseVectorStore:
result = store._get_embeddings("openai_text-embedding-ada-002", "sk-key")
assert result is mock_emb
@patch("application.vectorstore.base.settings")
@patch("application.vectorstore.base.EmbeddingsSingleton.get_instance")
@patch("docsgpt.vectorstore.base.settings")
@patch("docsgpt.vectorstore.base.EmbeddingsSingleton.get_instance")
@patch("os.path.exists", return_value=False)
def test_get_embeddings_huggingface_no_local_model(
self, mock_exists, mock_get_instance, mock_settings
@@ -396,8 +396,8 @@ class TestBaseVectorStore:
)
assert result is mock_emb
@patch("application.vectorstore.base.settings")
@patch("application.vectorstore.base.EmbeddingsSingleton.get_instance")
@patch("docsgpt.vectorstore.base.settings")
@patch("docsgpt.vectorstore.base.EmbeddingsSingleton.get_instance")
def test_get_embeddings_registered_model_passes_configured_name(
self, mock_get_instance, mock_settings
):
@@ -419,8 +419,8 @@ class TestBaseVectorStore:
"huggingface_sentence-transformers/all-mpnet-base-v2"
)
@patch("application.vectorstore.base.settings")
@patch("application.vectorstore.base.EmbeddingsSingleton.get_instance")
@patch("docsgpt.vectorstore.base.settings")
@patch("docsgpt.vectorstore.base.EmbeddingsSingleton.get_instance")
def test_get_embeddings_generic(self, mock_get_instance, mock_settings):
mock_settings.EMBEDDINGS_BASE_URL = None
mock_emb = Mock()
@@ -437,7 +437,7 @@ class TestSearchWithScoresDefault:
def test_pairs_hits_with_none(self):
"""A store that reports no score still satisfies the contract, so the
retriever never has to special-case it."""
from application.vectorstore.base import BaseVectorStore
from docsgpt.vectorstore.base import BaseVectorStore
class _Store(BaseVectorStore):
def search(self, question, k=2, *args, **kwargs):
@@ -451,7 +451,7 @@ class TestSearchWithScoresDefault:
assert store.search_with_scores("q", k=2) == [("a", None), ("b", None)]
def test_handles_store_returning_none(self):
from application.vectorstore.base import BaseVectorStore
from docsgpt.vectorstore.base import BaseVectorStore
class _Store(BaseVectorStore):
def search(self, question, k=2, *args, **kwargs):
@@ -477,8 +477,8 @@ class TestGetEmbeddingsResolver:
def setup_method(self):
EmbeddingsSingleton._instances = {}
@patch("application.vectorstore.base.settings")
@patch("application.vectorstore.base._get_embeddings_wrapper")
@patch("docsgpt.vectorstore.base.settings")
@patch("docsgpt.vectorstore.base._get_embeddings_wrapper")
@patch("os.path.exists", return_value=False)
def test_defaults_from_settings_do_not_raise(
self, _mock_exists, mock_get_wrapper, mock_settings
@@ -497,8 +497,8 @@ class TestGetEmbeddingsResolver:
assert result is mock_instance
assert set(EmbeddingsSingleton._instances) == {HF_MPNET}
@patch("application.vectorstore.base.settings")
@patch("application.vectorstore.base._get_embeddings_wrapper")
@patch("docsgpt.vectorstore.base.settings")
@patch("docsgpt.vectorstore.base._get_embeddings_wrapper")
@patch("os.path.exists", return_value=False)
def test_shares_cache_entry_with_vectorstore_helper(
self, _mock_exists, mock_get_wrapper, mock_settings
@@ -518,8 +518,8 @@ class TestGetEmbeddingsResolver:
assert set(EmbeddingsSingleton._instances) == {HF_MPNET}
mock_wrapper_cls.assert_called_once()
@patch("application.vectorstore.base.settings")
@patch("application.vectorstore.base._get_embeddings_wrapper")
@patch("docsgpt.vectorstore.base.settings")
@patch("docsgpt.vectorstore.base._get_embeddings_wrapper")
def test_repeated_resolution_loads_one_model(
self, mock_get_wrapper, mock_settings
):
@@ -543,7 +543,7 @@ class TestGetEmbeddingsResolver:
assert set(EmbeddingsSingleton._instances) == {HF_MPNET}
mock_wrapper_cls.assert_called_once_with(HF_MPNET)
@patch("application.vectorstore.base.settings")
@patch("docsgpt.vectorstore.base.settings")
def test_remote_when_base_url_configured(self, mock_settings):
mock_settings.EMBEDDINGS_BASE_URL = "http://remote:8080"
mock_settings.EMBEDDINGS_NAME = HF_MPNET
@@ -556,8 +556,8 @@ class TestGetEmbeddingsResolver:
assert result.model_name == HF_MPNET
assert result.headers["Authorization"] == "Bearer sk-remote"
@patch("application.vectorstore.base.settings")
@patch("application.vectorstore.base.EmbeddingsSingleton.get_instance")
@patch("docsgpt.vectorstore.base.settings")
@patch("docsgpt.vectorstore.base.EmbeddingsSingleton.get_instance")
def test_openai_passes_key(self, mock_get_instance, mock_settings):
mock_settings.EMBEDDINGS_BASE_URL = None
mock_settings.OPENAI_API_BASE = None
@@ -572,8 +572,8 @@ class TestGetEmbeddingsResolver:
"openai_text-embedding-ada-002", openai_api_key="sk-from-settings"
)
@patch("application.vectorstore.base.settings")
@patch("application.vectorstore.base.EmbeddingsSingleton.get_instance")
@patch("docsgpt.vectorstore.base.settings")
@patch("docsgpt.vectorstore.base.EmbeddingsSingleton.get_instance")
def test_openai_azure_uses_deployment_name(
self, mock_get_instance, mock_settings
):
@@ -591,8 +591,8 @@ class TestGetEmbeddingsResolver:
"openai_text-embedding-ada-002", model="embed-deploy"
)
@patch("application.vectorstore.base.settings")
@patch("application.vectorstore.base.EmbeddingsSingleton.get_instance")
@patch("docsgpt.vectorstore.base.settings")
@patch("docsgpt.vectorstore.base.EmbeddingsSingleton.get_instance")
def test_openai_alias_also_reaches_the_azure_deployment(
self, mock_get_instance, mock_settings
):
@@ -616,8 +616,8 @@ class TestGetEmbeddingsResolver:
"text-embedding-ada-002", model="embed-deploy"
)
@patch("application.vectorstore.base.settings")
@patch("application.vectorstore.base.EmbeddingsSingleton.get_instance")
@patch("docsgpt.vectorstore.base.settings")
@patch("docsgpt.vectorstore.base.EmbeddingsSingleton.get_instance")
def test_openai_name_is_matched_case_insensitively(
self, mock_get_instance, mock_settings
):
@@ -634,8 +634,8 @@ class TestGetEmbeddingsResolver:
"OpenAI_Text-Embedding-Ada-002", openai_api_key="sk-from-settings"
)
@patch("application.vectorstore.base.settings")
@patch("application.vectorstore.base.EmbeddingsSingleton.get_instance")
@patch("docsgpt.vectorstore.base.settings")
@patch("docsgpt.vectorstore.base.EmbeddingsSingleton.get_instance")
def test_explicit_arguments_win_over_settings(
self, mock_get_instance, mock_settings
):
@@ -647,8 +647,8 @@ class TestGetEmbeddingsResolver:
mock_get_instance.assert_called_once_with("some_custom_embedding")
@patch("application.vectorstore.base.settings")
@patch("application.vectorstore.base.get_embeddings")
@patch("docsgpt.vectorstore.base.settings")
@patch("docsgpt.vectorstore.base.get_embeddings")
def test_vectorstore_helper_delegates_to_resolver(
self, mock_resolver, _mock_settings
):
+1 -1
View File
@@ -1,5 +1,5 @@
import pytest
from application.vectorstore.document_class import Document
from docsgpt.vectorstore.document_class import Document
@pytest.mark.unit
+13 -13
View File
@@ -6,12 +6,12 @@ import pytest
def _make_es_store(source_id="test-source"):
"""Helper to create an ElasticsearchStore with mocked deps."""
# Reset class-level connection
from application.vectorstore.elasticsearch import ElasticsearchStore
from docsgpt.vectorstore.elasticsearch import ElasticsearchStore
ElasticsearchStore._es_connection = None
with patch(
"application.vectorstore.elasticsearch.settings"
"docsgpt.vectorstore.elasticsearch.settings"
) as mock_settings, patch.dict(
"sys.modules", {"elasticsearch": MagicMock(), "elasticsearch.helpers": MagicMock()}
):
@@ -39,7 +39,7 @@ def _make_es_store(source_id="test-source"):
@pytest.mark.unit
class TestElasticsearchStoreInit:
def test_source_id_cleaned(self):
store, _, _ = _make_es_store(source_id="application/indexes/abc123/")
store, _, _ = _make_es_store(source_id="docsgpt/indexes/abc123/")
assert store.source_id == "abc123"
def test_init_with_url(self):
@@ -48,12 +48,12 @@ class TestElasticsearchStoreInit:
assert store.index_name == "test_index"
def test_init_with_cloud_id(self):
from application.vectorstore.elasticsearch import ElasticsearchStore
from docsgpt.vectorstore.elasticsearch import ElasticsearchStore
ElasticsearchStore._es_connection = None
with patch(
"application.vectorstore.elasticsearch.settings"
"docsgpt.vectorstore.elasticsearch.settings"
) as mock_settings, patch.dict(
"sys.modules", {"elasticsearch": MagicMock()}
):
@@ -70,12 +70,12 @@ class TestElasticsearchStoreInit:
assert store.docsearch is not None
def test_init_no_url_no_cloud_id_raises(self):
from application.vectorstore.elasticsearch import ElasticsearchStore
from docsgpt.vectorstore.elasticsearch import ElasticsearchStore
ElasticsearchStore._es_connection = None
with patch(
"application.vectorstore.elasticsearch.settings"
"docsgpt.vectorstore.elasticsearch.settings"
) as mock_settings, patch.dict(
"sys.modules", {"elasticsearch": MagicMock()}
):
@@ -88,12 +88,12 @@ class TestElasticsearchStoreInit:
ElasticsearchStore(source_id="src", embeddings_key="k")
def test_reuses_class_connection(self):
from application.vectorstore.elasticsearch import ElasticsearchStore
from docsgpt.vectorstore.elasticsearch import ElasticsearchStore
ElasticsearchStore._es_connection = None
with patch(
"application.vectorstore.elasticsearch.settings"
"docsgpt.vectorstore.elasticsearch.settings"
) as mock_settings, patch.dict(
"sys.modules", {"elasticsearch": MagicMock()}
):
@@ -245,7 +245,7 @@ class TestElasticsearchStoreIndex:
@pytest.mark.unit
class TestElasticsearchStoreConnectToElasticsearch:
def test_connect_with_url(self):
from application.vectorstore.elasticsearch import ElasticsearchStore
from docsgpt.vectorstore.elasticsearch import ElasticsearchStore
with patch.dict("sys.modules", {"elasticsearch": MagicMock()}):
import elasticsearch
@@ -261,7 +261,7 @@ class TestElasticsearchStoreConnectToElasticsearch:
assert result is mock_es
def test_connect_with_both_raises(self):
from application.vectorstore.elasticsearch import ElasticsearchStore
from docsgpt.vectorstore.elasticsearch import ElasticsearchStore
with patch.dict("sys.modules", {"elasticsearch": MagicMock()}):
with pytest.raises(ValueError, match="Both es_url and cloud_id"):
@@ -270,14 +270,14 @@ class TestElasticsearchStoreConnectToElasticsearch:
)
def test_connect_with_neither_raises(self):
from application.vectorstore.elasticsearch import ElasticsearchStore
from docsgpt.vectorstore.elasticsearch import ElasticsearchStore
with patch.dict("sys.modules", {"elasticsearch": MagicMock()}):
with pytest.raises(ValueError, match="provide either"):
ElasticsearchStore.connect_to_elasticsearch()
def test_connect_with_api_key(self):
from application.vectorstore.elasticsearch import ElasticsearchStore
from docsgpt.vectorstore.elasticsearch import ElasticsearchStore
with patch.dict("sys.modules", {"elasticsearch": MagicMock()}):
import elasticsearch
+31 -31
View File
@@ -6,8 +6,8 @@ from unittest.mock import MagicMock, patch
import pytest
from application.vectorstore import base
from application.vectorstore.embeddings_delegated import EMBED_TASK, DelegatedEmbeddings
from docsgpt.vectorstore import base
from docsgpt.vectorstore.embeddings_delegated import EMBED_TASK, DelegatedEmbeddings
@pytest.fixture(autouse=True)
@@ -19,7 +19,7 @@ def _clear_singleton():
@pytest.fixture
def not_in_worker():
with patch("application.vectorstore.embeddings_delegated._in_worker", return_value=False):
with patch("docsgpt.vectorstore.embeddings_delegated._in_worker", return_value=False):
yield
@@ -27,7 +27,7 @@ class TestDispatch:
def test_query_is_embedded_on_the_worker(self, not_in_worker):
celery = MagicMock()
celery.send_task.return_value.get.return_value = [[0.1, 0.2, 0.3]]
with patch("application.celery_init.celery", celery):
with patch("docsgpt.celery_init.celery", celery):
vector = DelegatedEmbeddings("some/model").embed_query("hello")
assert vector == [0.1, 0.2, 0.3]
assert celery.send_task.call_args.args[0] == EMBED_TASK
@@ -36,7 +36,7 @@ class TestDispatch:
def test_routed_to_the_embeddings_queue(self, not_in_worker):
celery = MagicMock()
celery.send_task.return_value.get.return_value = [[0.0]]
with patch("application.celery_init.celery", celery):
with patch("docsgpt.celery_init.celery", celery):
with patch.object(base.settings, "EMBEDDINGS_QUEUE", "embeddings"):
DelegatedEmbeddings("some/model").embed_query("hi")
assert celery.send_task.call_args.kwargs["queue"] == "embeddings"
@@ -44,7 +44,7 @@ class TestDispatch:
def test_no_worker_gives_an_actionable_error(self, not_in_worker):
celery = MagicMock()
celery.send_task.return_value.get.side_effect = TimeoutError("no worker")
with patch("application.celery_init.celery", celery):
with patch("docsgpt.celery_init.celery", celery):
with pytest.raises(RuntimeError) as excinfo:
DelegatedEmbeddings("some/model").embed_query("hi")
message = str(excinfo.value)
@@ -53,7 +53,7 @@ class TestDispatch:
def test_empty_input_never_reaches_the_broker(self, not_in_worker):
celery = MagicMock()
with patch("application.celery_init.celery", celery):
with patch("docsgpt.celery_init.celery", celery):
assert DelegatedEmbeddings("some/model").embed_documents([]) == []
celery.send_task.assert_not_called()
@@ -65,9 +65,9 @@ class TestInsideAWorker:
local = MagicMock()
local.embed_documents.return_value = [[1.0, 2.0]]
celery = MagicMock()
with patch("application.vectorstore.embeddings_delegated._in_worker", return_value=True):
with patch("application.vectorstore.base.build_local_embeddings", return_value=local):
with patch("application.celery_init.celery", celery):
with patch("docsgpt.vectorstore.embeddings_delegated._in_worker", return_value=True):
with patch("docsgpt.vectorstore.base.build_local_embeddings", return_value=local):
with patch("docsgpt.celery_init.celery", celery):
vector = DelegatedEmbeddings("some/model").embed_query("hi")
assert vector == [1.0, 2.0]
celery.send_task.assert_not_called()
@@ -77,8 +77,8 @@ class TestInsideAWorker:
local.embed_documents.return_value = [[1.0]]
builder = MagicMock(return_value=local)
client = DelegatedEmbeddings("some/model")
with patch("application.vectorstore.embeddings_delegated._in_worker", return_value=True):
with patch("application.vectorstore.base.build_local_embeddings", builder):
with patch("docsgpt.vectorstore.embeddings_delegated._in_worker", return_value=True):
with patch("docsgpt.vectorstore.base.build_local_embeddings", builder):
client.embed_query("a")
client.embed_query("b")
builder.assert_called_once()
@@ -87,7 +87,7 @@ class TestInsideAWorker:
class TestDimension:
def test_registry_width_costs_no_round_trip(self):
celery = MagicMock()
with patch("application.celery_init.celery", celery):
with patch("docsgpt.celery_init.celery", celery):
client = DelegatedEmbeddings("ibm-granite/granite-embedding-311m-multilingual-r2")
assert client.dimension == 768
celery.send_task.assert_not_called()
@@ -95,7 +95,7 @@ class TestDimension:
def test_unknown_width_is_probed_once(self, not_in_worker):
celery = MagicMock()
celery.send_task.return_value.get.return_value = [[0.0] * 1024]
with patch("application.celery_init.celery", celery):
with patch("docsgpt.celery_init.celery", celery):
client = DelegatedEmbeddings("some/unregistered")
assert client.dimension == 1024
assert client.dimension == 1024
@@ -104,7 +104,7 @@ class TestDimension:
def test_an_unreachable_worker_reports_no_width(self, not_in_worker):
celery = MagicMock()
celery.send_task.return_value.get.side_effect = TimeoutError("down")
with patch("application.celery_init.celery", celery):
with patch("docsgpt.celery_init.celery", celery):
assert DelegatedEmbeddings("some/unregistered").dimension is None
@@ -152,7 +152,7 @@ class TestFailureCooldown:
def test_only_the_first_call_waits_out_the_timeout(self, not_in_worker):
celery, _ = self._celery(TimeoutError("no worker"))
embeddings = DelegatedEmbeddings("granite-311m")
with patch.dict("sys.modules", {"application.celery_init": MagicMock(celery=celery)}):
with patch.dict("sys.modules", {"docsgpt.celery_init": MagicMock(celery=celery)}):
for _ in range(4):
with pytest.raises(RuntimeError):
embeddings.embed_query("q")
@@ -161,7 +161,7 @@ class TestFailureCooldown:
def test_the_fast_failure_still_names_the_remedy(self, not_in_worker):
celery, _ = self._celery(TimeoutError("no worker"))
embeddings = DelegatedEmbeddings("granite-311m")
with patch.dict("sys.modules", {"application.celery_init": MagicMock(celery=celery)}):
with patch.dict("sys.modules", {"docsgpt.celery_init": MagicMock(celery=celery)}):
with pytest.raises(RuntimeError):
embeddings.embed_query("q")
with pytest.raises(RuntimeError, match="EMBEDDINGS_DELEGATE_TO_WORKER=false"):
@@ -170,7 +170,7 @@ class TestFailureCooldown:
def test_the_latch_clears_once_the_worker_answers(self, not_in_worker):
celery, result = self._celery(TimeoutError("no worker"))
embeddings = DelegatedEmbeddings("granite-311m")
with patch.dict("sys.modules", {"application.celery_init": MagicMock(celery=celery)}):
with patch.dict("sys.modules", {"docsgpt.celery_init": MagicMock(celery=celery)}):
with pytest.raises(RuntimeError):
embeddings.embed_query("q")
embeddings._failed_at = None # stand in for the cooldown elapsing
@@ -183,7 +183,7 @@ class TestFailureCooldown:
celery, result = self._celery(None)
result.get.return_value = [[0.1, 0.2]]
embeddings = DelegatedEmbeddings("granite-311m")
with patch.dict("sys.modules", {"application.celery_init": MagicMock(celery=celery)}):
with patch.dict("sys.modules", {"docsgpt.celery_init": MagicMock(celery=celery)}):
for _ in range(3):
assert embeddings.embed_query("q") == [0.1, 0.2]
assert celery.send_task.call_count == 3
@@ -225,7 +225,7 @@ class TestTheConcurrentFirstWave:
except Exception as exc: # noqa: BLE001 -- recorded for the assertions
errors.append(exc)
with patch.dict("sys.modules", {"application.celery_init": MagicMock(celery=celery)}):
with patch.dict("sys.modules", {"docsgpt.celery_init": MagicMock(celery=celery)}):
workers = [threading.Thread(target=call) for _ in range(threads)]
for worker in workers:
worker.start()
@@ -241,7 +241,7 @@ class TestTheConcurrentFirstWave:
celery = self._blocking_celery(release, TimeoutError("no worker"))
embeddings = DelegatedEmbeddings("granite-311m")
with patch(
"application.vectorstore.embeddings_delegated._PROBE_WAIT", 0.05
"docsgpt.vectorstore.embeddings_delegated._PROBE_WAIT", 0.05
):
values, errors = self._race(celery, embeddings, release)
@@ -255,7 +255,7 @@ class TestTheConcurrentFirstWave:
release = threading.Event()
celery = self._blocking_celery(release, TimeoutError("no worker"))
embeddings = DelegatedEmbeddings("granite-311m")
with patch("application.vectorstore.embeddings_delegated._PROBE_WAIT", 0.05):
with patch("docsgpt.vectorstore.embeddings_delegated._PROBE_WAIT", 0.05):
_, errors = self._race(celery, embeddings, release, threads=3)
assert all("EMBEDDINGS_DELEGATE_TO_WORKER=false" in str(e) for e in errors)
@@ -277,13 +277,13 @@ class TestTheConcurrentFirstWave:
release.set()
celery = self._blocking_celery(release, [[0.3]])
embeddings = DelegatedEmbeddings("granite-311m")
with patch.dict("sys.modules", {"application.celery_init": MagicMock(celery=celery)}):
with patch.dict("sys.modules", {"docsgpt.celery_init": MagicMock(celery=celery)}):
embeddings.embed_query("warm")
assert embeddings._verified is True
with patch.object(embeddings, "_state_lock") as lock:
with patch.dict(
"sys.modules", {"application.celery_init": MagicMock(celery=celery)}
"sys.modules", {"docsgpt.celery_init": MagicMock(celery=celery)}
):
embeddings.embed_query("q")
lock.__enter__.assert_not_called()
@@ -302,7 +302,7 @@ class TestTheConcurrentFirstWave:
healthy = self._blocking_celery(warm, [[0.4]])
embeddings = DelegatedEmbeddings("granite-311m")
with patch.dict(
"sys.modules", {"application.celery_init": MagicMock(celery=healthy)}
"sys.modules", {"docsgpt.celery_init": MagicMock(celery=healthy)}
):
embeddings.embed_query("warm")
assert embeddings._verified is True
@@ -310,8 +310,8 @@ class TestTheConcurrentFirstWave:
# No cooldown, so anything that gates the second wave can only be the
# probe -- which engages only because the failure cleared _verified.
with patch(
"application.vectorstore.embeddings_delegated._FAILURE_COOLDOWN", 0.0
), patch("application.vectorstore.embeddings_delegated._PROBE_WAIT", 0.05):
"docsgpt.vectorstore.embeddings_delegated._FAILURE_COOLDOWN", 0.0
), patch("docsgpt.vectorstore.embeddings_delegated._PROBE_WAIT", 0.05):
dying = threading.Event()
died = self._blocking_celery(dying, TimeoutError("worker went away"))
self._race(died, embeddings, dying)
@@ -352,14 +352,14 @@ class TestTheResultIsForgotten:
def test_a_successful_embed_forgets_its_result(self, not_in_worker):
celery, result = self._celery(value=[[0.1, 0.2]])
embeddings = DelegatedEmbeddings("granite-311m")
with patch.dict("sys.modules", {"application.celery_init": MagicMock(celery=celery)}):
with patch.dict("sys.modules", {"docsgpt.celery_init": MagicMock(celery=celery)}):
assert embeddings.embed_query("q") == [0.1, 0.2]
result.forget.assert_called_once()
def test_a_failed_embed_still_forgets(self, not_in_worker):
celery, result = self._celery(side_effect=TimeoutError("no worker"))
embeddings = DelegatedEmbeddings("granite-311m")
with patch.dict("sys.modules", {"application.celery_init": MagicMock(celery=celery)}):
with patch.dict("sys.modules", {"docsgpt.celery_init": MagicMock(celery=celery)}):
with pytest.raises(RuntimeError):
embeddings.embed_query("q")
result.forget.assert_called_once()
@@ -368,13 +368,13 @@ class TestTheResultIsForgotten:
celery, result = self._celery(value=[[0.3, 0.4]])
result.forget.side_effect = ConnectionError("backend down")
embeddings = DelegatedEmbeddings("granite-311m")
with patch.dict("sys.modules", {"application.celery_init": MagicMock(celery=celery)}):
with patch.dict("sys.modules", {"docsgpt.celery_init": MagicMock(celery=celery)}):
assert embeddings.embed_query("q") == [0.3, 0.4]
def test_forgetting_does_not_mask_the_dispatch_failure(self, not_in_worker):
celery, result = self._celery(side_effect=TimeoutError("no worker"))
result.forget.side_effect = ConnectionError("backend down")
embeddings = DelegatedEmbeddings("granite-311m")
with patch.dict("sys.modules", {"application.celery_init": MagicMock(celery=celery)}):
with patch.dict("sys.modules", {"docsgpt.celery_init": MagicMock(celery=celery)}):
with pytest.raises(RuntimeError, match="timed out or failed"):
embeddings.embed_query("q")
+3 -3
View File
@@ -5,9 +5,9 @@ from unittest.mock import MagicMock, patch
import numpy as np
import pytest
from application.vectorstore import embeddings_local
from application.vectorstore.embeddings_local import EmbeddingsWrapper
from application.vectorstore.model_registry import GRANITE_97M, MPNET
from docsgpt.vectorstore import embeddings_local
from docsgpt.vectorstore.embeddings_local import EmbeddingsWrapper
from docsgpt.vectorstore.model_registry import GRANITE_97M, MPNET
@pytest.fixture(autouse=True)
+15 -15
View File
@@ -10,8 +10,8 @@ from unittest.mock import MagicMock, Mock, patch
import pytest
from application.storage.local import LocalStorage
from application.vectorstore.faiss import FaissStore
from docsgpt.storage.local import LocalStorage
from docsgpt.vectorstore.faiss import FaissStore
class _FakeEmbeddings:
@@ -52,16 +52,16 @@ def storage(tmp_path):
@pytest.fixture
def make_store(storage):
from application.vectorstore.faiss import FaissStore
from docsgpt.vectorstore.faiss import FaissStore
def _make(source_id="src", docs_init=None):
with patch(
"application.vectorstore.base.BaseVectorStore._get_embeddings",
"docsgpt.vectorstore.base.BaseVectorStore._get_embeddings",
return_value=_FakeEmbeddings(),
), patch(
"application.vectorstore.faiss.StorageCreator.get_storage",
"docsgpt.vectorstore.faiss.StorageCreator.get_storage",
return_value=storage,
), patch("application.vectorstore.faiss.settings") as mock_settings:
), patch("docsgpt.vectorstore.faiss.settings") as mock_settings:
mock_settings.EMBEDDINGS_NAME = "test_model"
return FaissStore(source_id, "key", docs_init=docs_init)
@@ -180,7 +180,7 @@ class TestFaissPersistence:
@pytest.mark.unit
class TestFaissStoreAssertEmbeddingDimensions:
def test_dimension_mismatch_raises(self, populated):
with patch("application.vectorstore.faiss.settings") as mock_settings:
with patch("docsgpt.vectorstore.faiss.settings") as mock_settings:
mock_settings.EMBEDDINGS_NAME = (
"huggingface_sentence-transformers/all-mpnet-base-v2"
)
@@ -194,7 +194,7 @@ class TestFaissStoreAssertEmbeddingDimensions:
perfectly valid remote configuration, so an unknown width is deferred,
not treated as a mismatch.
"""
with patch("application.vectorstore.faiss.settings") as mock_settings:
with patch("docsgpt.vectorstore.faiss.settings") as mock_settings:
mock_settings.EMBEDDINGS_NAME = (
"huggingface_sentence-transformers/all-mpnet-base-v2"
)
@@ -203,7 +203,7 @@ class TestFaissStoreAssertEmbeddingDimensions:
assert populated.assert_embedding_dimensions(embeddings) is None
def test_dimension_match_passes(self, populated):
with patch("application.vectorstore.faiss.settings") as mock_settings:
with patch("docsgpt.vectorstore.faiss.settings") as mock_settings:
mock_settings.EMBEDDINGS_NAME = (
"huggingface_sentence-transformers/all-mpnet-base-v2"
)
@@ -215,33 +215,33 @@ class TestFaissStoreAssertEmbeddingDimensions:
That skipped exactly the case it exists for: an index built with one
model being opened under a different one.
"""
with patch("application.vectorstore.faiss.settings") as mock_settings:
with patch("docsgpt.vectorstore.faiss.settings") as mock_settings:
mock_settings.EMBEDDINGS_NAME = "openai_text-embedding-ada-002"
with pytest.raises(ValueError, match="Embedding dimension mismatch"):
populated.assert_embedding_dimensions(Mock(dimension=1536))
def test_mismatch_message_points_at_the_reembed_script(self, populated):
with patch("application.vectorstore.faiss.settings") as mock_settings:
with patch("docsgpt.vectorstore.faiss.settings") as mock_settings:
mock_settings.EMBEDDINGS_NAME = "granite-311m"
with pytest.raises(ValueError, match="application.scripts.reembed"):
with pytest.raises(ValueError, match="docsgpt.scripts.reembed"):
populated.assert_embedding_dimensions(Mock(dimension=768))
@pytest.mark.unit
class TestGetVectorstore:
def test_empty_path_returns_base(self):
from application.vectorstore.faiss import get_vectorstore
from docsgpt.vectorstore.faiss import get_vectorstore
assert get_vectorstore("") == "indexes"
def test_normal_path(self):
from application.vectorstore.faiss import get_vectorstore
from docsgpt.vectorstore.faiss import get_vectorstore
assert get_vectorstore("abc") == "indexes/abc"
@pytest.mark.parametrize("bad", ["../etc", "..\\etc", "a/../../b"])
def test_traversal_rejected(self, bad):
from application.vectorstore.faiss import get_vectorstore
from docsgpt.vectorstore.faiss import get_vectorstore
with pytest.raises(ValueError, match="Invalid source_id path"):
get_vectorstore(bad)
+5 -5
View File
@@ -14,7 +14,7 @@ import pickletools
import pytest
from application.vectorstore.faiss_docstore import (
from docsgpt.vectorstore.faiss_docstore import (
CompatUnpickler,
LegacyDocstore,
LegacyDocument,
@@ -61,8 +61,8 @@ class TestPickleSidecar:
dump_pickle_sidecar(DOCUMENTS, MAPPING)
assert not [m for m in sys.modules if m.startswith("langchain")]
assert LegacyDocument.__module__.startswith("application.")
assert LegacyDocstore.__module__.startswith("application.")
assert LegacyDocument.__module__.startswith("docsgpt.")
assert LegacyDocstore.__module__.startswith("docsgpt.")
def test_reads_pydantic_state_shape(self):
"""langchain's Document is a pydantic model; that is the state we get."""
@@ -174,7 +174,7 @@ class TestRealLegacyFixture:
"""The index.pkl checked into the repo was written by langchain in 2025."""
def test_reads_committed_legacy_index(self):
with open("application/index.pkl", "rb") as f:
with open("docsgpt/index.pkl", "rb") as f:
documents, mapping = load_pickle_sidecar(f.read())
assert len(documents) == 3
assert len(mapping) == 3
@@ -182,7 +182,7 @@ class TestRealLegacyFixture:
assert all(d["metadata"].get("title") for d in documents.values())
def test_legacy_survives_conversion_to_json(self):
with open("application/index.pkl", "rb") as f:
with open("docsgpt/index.pkl", "rb") as f:
documents, mapping = load_pickle_sidecar(f.read())
restored, restored_mapping = load_json_sidecar(
dump_json_sidecar(documents, mapping)
+4 -4
View File
@@ -6,13 +6,13 @@ import pytest
def _make_lancedb_store(source_id="test-source"):
"""Helper to create a LanceDBVectorStore with mocked deps."""
with patch(
"application.vectorstore.lancedb.settings"
"docsgpt.vectorstore.lancedb.settings"
) as mock_settings:
mock_settings.LANCEDB_PATH = "/tmp/lancedb"
mock_settings.LANCEDB_TABLE_NAME = "docs"
mock_settings.EMBEDDINGS_NAME = "test_model"
from application.vectorstore.lancedb import LanceDBVectorStore
from docsgpt.vectorstore.lancedb import LanceDBVectorStore
store = LanceDBVectorStore(
path="/tmp/lancedb",
@@ -31,11 +31,11 @@ class TestLanceDBVectorStoreInit:
assert store.table_name == "docs_src1"
def test_table_name_without_source_id(self):
with patch("application.vectorstore.lancedb.settings") as mock_settings:
with patch("docsgpt.vectorstore.lancedb.settings") as mock_settings:
mock_settings.LANCEDB_PATH = "/tmp"
mock_settings.LANCEDB_TABLE_NAME = "docs"
from application.vectorstore.lancedb import LanceDBVectorStore
from docsgpt.vectorstore.lancedb import LanceDBVectorStore
store = LanceDBVectorStore(
path="/tmp", table_name_prefix="docs", source_id=None
+8 -8
View File
@@ -40,12 +40,12 @@ class _FakeEmbeddings:
@pytest.fixture
def store(tmp_path):
from application.vectorstore.milvus import MilvusStore
from docsgpt.vectorstore.milvus import MilvusStore
with patch(
"application.vectorstore.base.BaseVectorStore._get_embeddings",
"docsgpt.vectorstore.base.BaseVectorStore._get_embeddings",
return_value=_FakeEmbeddings(),
), patch("application.vectorstore.milvus.settings") as mock_settings:
), patch("docsgpt.vectorstore.milvus.settings") as mock_settings:
mock_settings.EMBEDDINGS_NAME = "test_model"
mock_settings.MILVUS_COLLECTION_NAME = "test_collection"
mock_settings.MILVUS_URI = str(tmp_path / "milvus.db")
@@ -114,12 +114,12 @@ class TestMilvusStore:
assert len(populated.get_chunks()) == 2
def test_delete_index_removes_only_this_source(self, populated):
from application.vectorstore.milvus import MilvusStore
from docsgpt.vectorstore.milvus import MilvusStore
with patch(
"application.vectorstore.base.BaseVectorStore._get_embeddings",
"docsgpt.vectorstore.base.BaseVectorStore._get_embeddings",
return_value=_FakeEmbeddings(),
), patch("application.vectorstore.milvus.settings") as mock_settings:
), patch("docsgpt.vectorstore.milvus.settings") as mock_settings:
mock_settings.EMBEDDINGS_NAME = "test_model"
mock_settings.MILVUS_COLLECTION_NAME = "test_collection"
mock_settings.MILVUS_URI = ":memory:"
@@ -156,7 +156,7 @@ class TestMilvusUriEnvGuard:
"""
def test_env_var_hidden_during_import_and_restored(self):
from application.vectorstore.milvus import _without_milvus_uri_env
from docsgpt.vectorstore.milvus import _without_milvus_uri_env
with patch.dict(os.environ, {"MILVUS_URI": "./milvus_local.db"}):
with _without_milvus_uri_env():
@@ -164,7 +164,7 @@ class TestMilvusUriEnvGuard:
assert os.environ["MILVUS_URI"] == "./milvus_local.db"
def test_absent_env_var_stays_absent(self):
from application.vectorstore.milvus import _without_milvus_uri_env
from docsgpt.vectorstore.milvus import _without_milvus_uri_env
env = {k: v for k, v in os.environ.items() if k != "MILVUS_URI"}
with patch.dict(os.environ, env, clear=True):
+2 -2
View File
@@ -4,7 +4,7 @@ import os
import pytest
from application.vectorstore import model_registry as reg
from docsgpt.vectorstore import model_registry as reg
class TestResolve:
@@ -107,7 +107,7 @@ class TestRegistryMatchesTheHub:
"model", [m for m in reg.MODELS if m.provider == "fastembed"]
)
def test_entry_matches_repository_metadata(self, model):
from application.vectorstore.embeddings_local import _describe_from_repo
from docsgpt.vectorstore.embeddings_local import _describe_from_repo
described = _describe_from_repo(model.repo)
assert described is not None, f"{model.repo} declares no pooling metadata"
+5 -5
View File
@@ -6,9 +6,9 @@ import pytest
def _make_mongodb_store(source_id="test-source"):
"""Helper to create a MongoDBVectorStore with all external deps mocked."""
with patch(
"application.vectorstore.base.BaseVectorStore._get_embeddings"
"docsgpt.vectorstore.base.BaseVectorStore._get_embeddings"
) as mock_get_emb, patch(
"application.vectorstore.mongodb.settings"
"docsgpt.vectorstore.mongodb.settings"
) as mock_settings, patch.dict(
"sys.modules", {"pymongo": MagicMock()}
):
@@ -19,7 +19,7 @@ def _make_mongodb_store(source_id="test-source"):
mock_settings.EMBEDDINGS_NAME = "test_model"
mock_settings.MONGO_URI = "mongodb://localhost:27017"
from application.vectorstore.mongodb import MongoDBVectorStore
from docsgpt.vectorstore.mongodb import MongoDBVectorStore
store = MongoDBVectorStore(
source_id=source_id,
@@ -37,7 +37,7 @@ def _make_mongodb_store(source_id="test-source"):
@pytest.mark.unit
class TestMongoDBVectorStoreInit:
def test_source_id_cleaned(self):
store, _, _ = _make_mongodb_store(source_id="application/indexes/abc123/")
store, _, _ = _make_mongodb_store(source_id="docsgpt/indexes/abc123/")
assert store._source_id == "abc123"
@@ -269,7 +269,7 @@ class TestMongoDBVectorStoreDeleteChunk:
store, mock_collection, _ = _make_mongodb_store()
mock_collection.delete_one.return_value = Mock(deleted_count=1)
with patch("application.vectorstore.mongodb.ObjectId", create=True):
with patch("docsgpt.vectorstore.mongodb.ObjectId", create=True):
# We need to mock bson.objectid.ObjectId
with patch.dict("sys.modules", {"bson": MagicMock(), "bson.objectid": MagicMock()}):
from unittest.mock import MagicMock as MM
+7 -7
View File
@@ -10,9 +10,9 @@ def _make_store(
):
"""Helper to create a PGVectorStore with all external deps mocked."""
with patch(
"application.vectorstore.base.BaseVectorStore._get_embeddings"
"docsgpt.vectorstore.base.BaseVectorStore._get_embeddings"
) as mock_get_emb, patch(
"application.vectorstore.pgvector.settings"
"docsgpt.vectorstore.pgvector.settings"
) as mock_settings, patch.dict(
"sys.modules",
{
@@ -29,7 +29,7 @@ def _make_store(
mock_settings.EMBEDDINGS_NAME = "test_model"
mock_settings.PGVECTOR_CONNECTION_STRING = connection_string
from application.vectorstore.pgvector import PGVectorStore
from docsgpt.vectorstore.pgvector import PGVectorStore
store = PGVectorStore(
source_id=source_id,
@@ -55,14 +55,14 @@ def _make_store(
@pytest.mark.unit
class TestPGVectorStoreInit:
def test_source_id_cleaned(self):
store, _, _, _ = _make_store(source_id="application/indexes/abc123/")
store, _, _, _ = _make_store(source_id="docsgpt/indexes/abc123/")
assert store._source_id == "abc123"
def test_missing_connection_string_raises(self):
with patch(
"application.vectorstore.base.BaseVectorStore._get_embeddings"
"docsgpt.vectorstore.base.BaseVectorStore._get_embeddings"
) as mock_get_emb, patch(
"application.vectorstore.pgvector.settings"
"docsgpt.vectorstore.pgvector.settings"
) as mock_settings, patch.dict(
"sys.modules",
{
@@ -76,7 +76,7 @@ class TestPGVectorStoreInit:
mock_settings.PGVECTOR_CONNECTION_STRING = None
mock_settings.POSTGRES_URI = None
from application.vectorstore.pgvector import PGVectorStore
from docsgpt.vectorstore.pgvector import PGVectorStore
with pytest.raises(ValueError, match="connection string is required"):
PGVectorStore(
+11 -11
View File
@@ -13,9 +13,9 @@ from unittest.mock import patch
import pytest
from application.storage.db.bootstrap import ensure_vector_schema
from application.vectorstore import pgvector as pgvector_module
from application.vectorstore.pgvector import PGVectorStore
from docsgpt.storage.db.bootstrap import ensure_vector_schema
from docsgpt.vectorstore import pgvector as pgvector_module
from docsgpt.vectorstore.pgvector import PGVectorStore
pytestmark = pytest.mark.integration
@@ -88,7 +88,7 @@ def live_dsn(postgresql, monkeypatch):
pytest.skip(f"pgvector extension unavailable: {exc}")
dsn = _dsn(postgresql.info)
from application.core import settings as settings_module
from docsgpt.core import settings as settings_module
settings = settings_module.settings
monkeypatch.setattr(settings, "VECTOR_STORE", "pgvector", raising=False)
@@ -111,14 +111,14 @@ def stub_embeddings():
"""
stub = _StubEmbeddings()
with patch(
"application.vectorstore.base.get_embeddings", return_value=stub
"docsgpt.vectorstore.base.get_embeddings", return_value=stub
), patch(
"application.vectorstore.base.build_local_embeddings", return_value=stub
"docsgpt.vectorstore.base.build_local_embeddings", return_value=stub
), patch(
"application.vectorstore.model_registry.dimension_for",
"docsgpt.vectorstore.model_registry.dimension_for",
return_value=STUB_DIM,
), patch(
"application.vectorstore.base.BaseVectorStore._get_embeddings",
"docsgpt.vectorstore.base.BaseVectorStore._get_embeddings",
return_value=stub,
):
yield stub
@@ -150,9 +150,9 @@ class TestBootHookCreatesTheSchema:
wide = _WideStubEmbeddings()
with patch(
"application.vectorstore.base.get_embeddings", return_value=wide
"docsgpt.vectorstore.base.get_embeddings", return_value=wide
), patch(
"application.vectorstore.model_registry.dimension_for",
"docsgpt.vectorstore.model_registry.dimension_for",
return_value=wide.dimension,
):
with pytest.raises(RuntimeError) as excinfo:
@@ -165,7 +165,7 @@ class TestBootHookCreatesTheSchema:
def test_creates_graph_tables_when_graphrag_is_enabled(
self, live_dsn, postgresql, stub_embeddings, monkeypatch
):
from application.core import settings as settings_module
from docsgpt.core import settings as settings_module
monkeypatch.setattr(
settings_module.settings, "GRAPHRAG_ENABLED", True, raising=False
+2 -2
View File
@@ -13,8 +13,8 @@ from unittest.mock import MagicMock
import pytest
from application.vectorstore import pgvector as pgvector_module
from application.vectorstore.pgvector import PGVectorStore
from docsgpt.vectorstore import pgvector as pgvector_module
from docsgpt.vectorstore.pgvector import PGVectorStore
@pytest.fixture(autouse=True)
@@ -15,7 +15,7 @@ from unittest.mock import MagicMock, Mock, patch
import pytest
from application.vectorstore import pgvector as pgvector_module
from docsgpt.vectorstore import pgvector as pgvector_module
CONNECTION_STRING = "postgresql://user:pass@localhost/db"
@@ -23,9 +23,9 @@ CONNECTION_STRING = "postgresql://user:pass@localhost/db"
def _make_store(source_id="test-source", connection_string=CONNECTION_STRING):
"""Build a store with every external dependency mocked, no DDL patching."""
with patch(
"application.vectorstore.base.BaseVectorStore._get_embeddings"
"docsgpt.vectorstore.base.BaseVectorStore._get_embeddings"
) as mock_get_emb, patch(
"application.vectorstore.pgvector.settings"
"docsgpt.vectorstore.pgvector.settings"
) as mock_settings, patch.dict(
"sys.modules",
{
@@ -42,7 +42,7 @@ def _make_store(source_id="test-source", connection_string=CONNECTION_STRING):
mock_settings.EMBEDDINGS_NAME = "test_model"
mock_settings.PGVECTOR_CONNECTION_STRING = connection_string
from application.vectorstore.pgvector import PGVectorStore
from docsgpt.vectorstore.pgvector import PGVectorStore
store = PGVectorStore(
source_id=source_id,
@@ -64,9 +64,9 @@ def _make_store(source_id="test-source", connection_string=CONNECTION_STRING):
class TestConstructionTouchesNothing:
def test_init_opens_no_connection_and_runs_no_ddl(self):
with patch(
"application.vectorstore.base.BaseVectorStore._get_embeddings"
"docsgpt.vectorstore.base.BaseVectorStore._get_embeddings"
) as mock_get_emb, patch(
"application.vectorstore.pgvector.settings"
"docsgpt.vectorstore.pgvector.settings"
) as mock_settings, patch.dict(
"sys.modules",
{
@@ -79,7 +79,7 @@ class TestConstructionTouchesNothing:
mock_settings.EMBEDDINGS_NAME = "test_model"
mock_settings.PGVECTOR_CONNECTION_STRING = CONNECTION_STRING
from application.vectorstore.pgvector import PGVectorStore
from docsgpt.vectorstore.pgvector import PGVectorStore
with patch.object(
PGVectorStore, "_get_connection"
@@ -112,8 +112,8 @@ class TestPoolSizingHasOneHome:
"""
def test_both_stores_share_one_implementation(self):
from application.graphrag import store as store_module
from application.vectorstore import pgconn
from docsgpt.graphrag import store as store_module
from docsgpt.vectorstore import pgconn
assert pgvector_module.DEFAULT_POOL_MAX_SIZE is pgconn.DEFAULT_POOL_MAX_SIZE
assert store_module.DEFAULT_POOL_MAX_SIZE is pgconn.DEFAULT_POOL_MAX_SIZE
@@ -128,8 +128,8 @@ class TestPoolSizingHasOneHome:
[(0, 0), (2, 2), (None, 8), ("4", 8), (True, 8), (-1, 8)],
)
def test_pool_size_is_resolved_defensively(self, monkeypatch, value, expected):
from application.core import settings as settings_module
from application.vectorstore import pgconn
from docsgpt.core import settings as settings_module
from docsgpt.vectorstore import pgconn
monkeypatch.setattr(
settings_module.settings, "PGVECTOR_POOL_MAX_SIZE", value, raising=False
@@ -176,7 +176,7 @@ class TestWritePathEnsuresSchemaOnce:
@pytest.mark.unit
class TestCreateSchema:
def test_emits_the_ddl_and_leaves_the_commit_to_the_caller(self):
from application.vectorstore.pgvector import PGVectorStore
from docsgpt.vectorstore.pgvector import PGVectorStore
conn, cursor = MagicMock(), MagicMock()
conn.cursor.return_value = cursor
@@ -213,19 +213,19 @@ class TestTableDimension:
return conn
def test_parses_the_declared_vector_width(self):
from application.vectorstore.pgvector import PGVectorStore
from docsgpt.vectorstore.pgvector import PGVectorStore
conn = self._conn([("documents",), ("vector(768)",)])
assert PGVectorStore.table_dimension(conn) == 768
def test_returns_none_when_the_table_is_absent(self):
from application.vectorstore.pgvector import PGVectorStore
from docsgpt.vectorstore.pgvector import PGVectorStore
conn = self._conn([(None,)])
assert PGVectorStore.table_dimension(conn) is None
def test_returns_none_when_the_column_type_is_not_a_vector(self):
from application.vectorstore.pgvector import PGVectorStore
from docsgpt.vectorstore.pgvector import PGVectorStore
conn = self._conn([("documents",), ("text",)])
assert PGVectorStore.table_dimension(conn) is None
+10 -10
View File
@@ -51,12 +51,12 @@ def _settings(mock_settings, collection="test_collection"):
@pytest.fixture
def store():
from application.vectorstore.qdrant import QdrantStore
from docsgpt.vectorstore.qdrant import QdrantStore
with patch(
"application.vectorstore.base.BaseVectorStore._get_embeddings",
"docsgpt.vectorstore.base.BaseVectorStore._get_embeddings",
return_value=_FakeEmbeddings(),
), patch("application.vectorstore.qdrant.settings") as mock_settings:
), patch("docsgpt.vectorstore.qdrant.settings") as mock_settings:
_settings(mock_settings)
yield QdrantStore(source_id="src-A", embeddings_key="k")
@@ -135,12 +135,12 @@ class TestQdrantStore:
assert len(populated.get_chunks()) == 2
def test_delete_index_removes_only_this_source(self, populated):
from application.vectorstore.qdrant import QdrantStore
from docsgpt.vectorstore.qdrant import QdrantStore
with patch(
"application.vectorstore.base.BaseVectorStore._get_embeddings",
"docsgpt.vectorstore.base.BaseVectorStore._get_embeddings",
return_value=_FakeEmbeddings(),
), patch("application.vectorstore.qdrant.settings") as mock_settings:
), patch("docsgpt.vectorstore.qdrant.settings") as mock_settings:
_settings(mock_settings)
other = QdrantStore(source_id="src-B", embeddings_key="k")
# Share the in-memory backend so both sources live in one collection.
@@ -159,9 +159,9 @@ class TestQdrantStore:
@pytest.mark.unit
class TestQdrantClientKwargs:
def test_unset_settings_are_omitted(self):
from application.vectorstore.qdrant import QdrantStore
from docsgpt.vectorstore.qdrant import QdrantStore
with patch("application.vectorstore.qdrant.settings") as mock_settings:
with patch("docsgpt.vectorstore.qdrant.settings") as mock_settings:
_settings(mock_settings)
kwargs = QdrantStore._client_kwargs()
# location/url/path are mutually exclusive in qdrant-client, so only
@@ -170,9 +170,9 @@ class TestQdrantClientKwargs:
assert "url" not in kwargs and "path" not in kwargs and "host" not in kwargs
def test_configured_settings_are_forwarded(self):
from application.vectorstore.qdrant import QdrantStore
from docsgpt.vectorstore.qdrant import QdrantStore
with patch("application.vectorstore.qdrant.settings") as mock_settings:
with patch("docsgpt.vectorstore.qdrant.settings") as mock_settings:
_settings(mock_settings)
mock_settings.QDRANT_LOCATION = None
mock_settings.QDRANT_URL = "http://qdrant:6333"
@@ -8,10 +8,10 @@ the request; the overflow is dropped (lossy by design).
from unittest.mock import MagicMock
from application.core.settings import settings
from application.utils import get_encoding
from application.vectorstore import base
from application.vectorstore.base import RemoteEmbeddings
from docsgpt.core.settings import settings
from docsgpt.utils import get_encoding
from docsgpt.vectorstore import base
from docsgpt.vectorstore.base import RemoteEmbeddings
def _capture_post(monkeypatch):
@@ -89,20 +89,20 @@ class TestInputLimitResolution:
"""The cap falls back to the model's own context window."""
def _remote(self, model_name):
from application.vectorstore.base import RemoteEmbeddings
from docsgpt.vectorstore.base import RemoteEmbeddings
return RemoteEmbeddings(
api_url="http://embeddings", model_name=model_name, api_key=None
)
def test_explicit_setting_wins(self, monkeypatch):
from application.vectorstore import base
from docsgpt.vectorstore import base
monkeypatch.setattr(base.settings, "EMBEDDINGS_MAX_INPUT_TOKENS", 123)
assert self._remote("granite-311m")._resolve_input_limit() == 123
def test_registered_model_supplies_its_own_ceiling(self, monkeypatch):
from application.vectorstore import base
from docsgpt.vectorstore import base
monkeypatch.setattr(base.settings, "EMBEDDINGS_MAX_INPUT_TOKENS", None)
monkeypatch.setattr(base, "_embeddings_name_is_explicit", lambda: True)
@@ -117,27 +117,27 @@ class TestInputLimitResolution:
settings default contribute mpnet's 384-token window would clip every
chunk on a server that may well serve a 32k-context model.
"""
from application.vectorstore import base
from docsgpt.vectorstore import base
monkeypatch.setattr(base.settings, "EMBEDDINGS_MAX_INPUT_TOKENS", None)
monkeypatch.setattr(base, "_embeddings_name_is_explicit", lambda: False)
assert self._remote("all-mpnet-base-v2")._resolve_input_limit() is None
def test_explicit_setting_still_wins_over_an_unset_name(self, monkeypatch):
from application.vectorstore import base
from docsgpt.vectorstore import base
monkeypatch.setattr(base.settings, "EMBEDDINGS_MAX_INPUT_TOKENS", 512)
monkeypatch.setattr(base, "_embeddings_name_is_explicit", lambda: False)
assert self._remote("all-mpnet-base-v2")._resolve_input_limit() == 512
def test_unknown_model_stays_unlimited(self, monkeypatch):
from application.vectorstore import base
from docsgpt.vectorstore import base
monkeypatch.setattr(base.settings, "EMBEDDINGS_MAX_INPUT_TOKENS", None)
assert self._remote("some-org/mystery")._resolve_input_limit() is None
def test_non_positive_setting_falls_through_to_the_registry(self, monkeypatch):
from application.vectorstore import base
from docsgpt.vectorstore import base
monkeypatch.setattr(base.settings, "EMBEDDINGS_MAX_INPUT_TOKENS", 0)
monkeypatch.setattr(base, "_embeddings_name_is_explicit", lambda: True)
@@ -161,7 +161,7 @@ class TestEmbeddingsNameIsExplicit:
"""
def _default(self):
from application.core.settings import Settings
from docsgpt.core.settings import Settings
return Settings.model_fields["EMBEDDINGS_NAME"].default
+1 -1
View File
@@ -2,7 +2,7 @@ from unittest.mock import patch
import pytest
from application.vectorstore.vector_creator import VectorCreator
from docsgpt.vectorstore.vector_creator import VectorCreator
@pytest.mark.unit