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

+27 -27
View File
@@ -1,4 +1,4 @@
"""Tests for application/api/answer/routes/answer.py"""
"""Tests for docsgpt/api/answer/routes/answer.py"""
import json
import uuid
@@ -15,7 +15,7 @@ _AGENT_ID = "507f1f77bcf86cd799439012"
def mock_stream_processor():
"""Create a mock StreamProcessor."""
with patch(
"application.api.answer.routes.answer.StreamProcessor"
"docsgpt.api.answer.routes.answer.StreamProcessor"
) as MockProcessor:
processor = MagicMock()
processor.decoded_token = {"sub": "test_user"}
@@ -35,7 +35,7 @@ def answer_client(mock_mongo_db, flask_app):
"""Create a test client with the answer route registered."""
from flask_restx import Api
from application.api.answer.routes.answer import answer_ns
from docsgpt.api.answer.routes.answer import answer_ns
api = Api(flask_app)
api.add_namespace(answer_ns)
@@ -61,13 +61,13 @@ class TestAnswerResourcePost:
return_value=iter([]),
):
with patch(
"application.api.answer.routes.answer.AnswerResource.validate_request",
"docsgpt.api.answer.routes.answer.AnswerResource.validate_request",
return_value=None,
), patch(
"application.api.answer.routes.answer.AnswerResource.check_usage",
"docsgpt.api.answer.routes.answer.AnswerResource.check_usage",
return_value=None,
), patch(
"application.api.answer.routes.answer.AnswerResource.complete_stream",
"docsgpt.api.answer.routes.answer.AnswerResource.complete_stream",
return_value=iter(
[
f'data: {json.dumps({"type": "answer", "answer": "Hello"})}\n\n',
@@ -76,7 +76,7 @@ class TestAnswerResourcePost:
]
),
), patch(
"application.api.answer.routes.answer.AnswerResource.process_response_stream",
"docsgpt.api.answer.routes.answer.AnswerResource.process_response_stream",
return_value={"conversation_id": conv_id, "answer": "Hello", "sources": [], "tool_calls": [], "thought": "", "error": None},
):
resp = answer_client.post(
@@ -92,7 +92,7 @@ class TestAnswerResourcePost:
def test_unauthorized_returns_401(self, answer_client, mock_stream_processor):
mock_stream_processor.decoded_token = None
with patch(
"application.api.answer.routes.answer.AnswerResource.validate_request",
"docsgpt.api.answer.routes.answer.AnswerResource.validate_request",
return_value=None,
):
resp = answer_client.post(
@@ -106,10 +106,10 @@ class TestAnswerResourcePost:
def test_usage_exceeded_returns_error(self, answer_client, mock_stream_processor):
with patch(
"application.api.answer.routes.answer.AnswerResource.validate_request",
"docsgpt.api.answer.routes.answer.AnswerResource.validate_request",
return_value=None,
), patch(
"application.api.answer.routes.answer.AnswerResource.check_usage",
"docsgpt.api.answer.routes.answer.AnswerResource.check_usage",
) as mock_check:
with flask_app_context(answer_client):
mock_check.return_value = ({"error": "Usage limit exceeded"}, 429)
@@ -123,16 +123,16 @@ class TestAnswerResourcePost:
def test_stream_error_returns_400(self, answer_client, mock_stream_processor):
with patch(
"application.api.answer.routes.answer.AnswerResource.validate_request",
"docsgpt.api.answer.routes.answer.AnswerResource.validate_request",
return_value=None,
), patch(
"application.api.answer.routes.answer.AnswerResource.check_usage",
"docsgpt.api.answer.routes.answer.AnswerResource.check_usage",
return_value=None,
), patch(
"application.api.answer.routes.answer.AnswerResource.complete_stream",
"docsgpt.api.answer.routes.answer.AnswerResource.complete_stream",
return_value=iter([]),
), patch(
"application.api.answer.routes.answer.AnswerResource.process_response_stream",
"docsgpt.api.answer.routes.answer.AnswerResource.process_response_stream",
return_value={"conversation_id": None, "answer": None, "sources": None, "tool_calls": None, "thought": None, "error": "Stream error"},
):
resp = answer_client.post(
@@ -154,7 +154,7 @@ class TestAnswerResourcePost:
traceback on the ERROR channel. ``/stream`` and
``/v1/chat/completions`` both answer 409 for the identical condition.
"""
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ResumeInProgressError,
)
@@ -163,7 +163,7 @@ class TestAnswerResourcePost:
)
with patch(
"application.api.answer.routes.answer.AnswerResource.validate_request",
"docsgpt.api.answer.routes.answer.AnswerResource.validate_request",
return_value=None,
):
resp = answer_client.post(
@@ -186,13 +186,13 @@ class TestAnswerResourcePost:
def test_exception_returns_500(self, answer_client, mock_stream_processor):
with patch(
"application.api.answer.routes.answer.AnswerResource.validate_request",
"docsgpt.api.answer.routes.answer.AnswerResource.validate_request",
return_value=None,
), patch(
"application.api.answer.routes.answer.AnswerResource.check_usage",
"docsgpt.api.answer.routes.answer.AnswerResource.check_usage",
return_value=None,
), patch(
"application.api.answer.routes.answer.AnswerResource.complete_stream",
"docsgpt.api.answer.routes.answer.AnswerResource.complete_stream",
side_effect=RuntimeError("unexpected"),
):
resp = answer_client.post(
@@ -208,16 +208,16 @@ class TestAnswerResourcePost:
):
conv_id = str(uuid.uuid4())
with patch(
"application.api.answer.routes.answer.AnswerResource.validate_request",
"docsgpt.api.answer.routes.answer.AnswerResource.validate_request",
return_value=None,
), patch(
"application.api.answer.routes.answer.AnswerResource.check_usage",
"docsgpt.api.answer.routes.answer.AnswerResource.check_usage",
return_value=None,
), patch(
"application.api.answer.routes.answer.AnswerResource.complete_stream",
"docsgpt.api.answer.routes.answer.AnswerResource.complete_stream",
return_value=iter([]),
), patch(
"application.api.answer.routes.answer.AnswerResource.process_response_stream",
"docsgpt.api.answer.routes.answer.AnswerResource.process_response_stream",
return_value={"conversation_id": conv_id, "answer": '{"key": "val"}', "sources": [], "tool_calls": [], "thought": "", "error": None, "extra": {"structured": True, "schema": {"type": "object"}}},
):
resp = answer_client.post(
@@ -235,16 +235,16 @@ class TestAnswerResourcePost:
):
conv_id = str(uuid.uuid4())
with patch(
"application.api.answer.routes.answer.AnswerResource.validate_request",
"docsgpt.api.answer.routes.answer.AnswerResource.validate_request",
return_value=None,
), patch(
"application.api.answer.routes.answer.AnswerResource.check_usage",
"docsgpt.api.answer.routes.answer.AnswerResource.check_usage",
return_value=None,
), patch(
"application.api.answer.routes.answer.AnswerResource.complete_stream",
"docsgpt.api.answer.routes.answer.AnswerResource.complete_stream",
return_value=iter([]),
), patch(
"application.api.answer.routes.answer.AnswerResource.process_response_stream",
"docsgpt.api.answer.routes.answer.AnswerResource.process_response_stream",
return_value={"conversation_id": conv_id, "answer": "answer text", "sources": [{"title": "src"}], "tool_calls": [{"tool": "t"}], "thought": "thinking...", "error": None},
):
resp = answer_client.post(
+36 -36
View File
@@ -12,7 +12,7 @@ class TestBaseAnswerValidation:
def test_validate_request_passes_with_required_fields(
self, mock_mongo_db, flask_app
):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -23,7 +23,7 @@ class TestBaseAnswerValidation:
assert result is None
def test_validate_request_fails_without_question(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -36,7 +36,7 @@ class TestBaseAnswerValidation:
assert "question" in result.json["message"].lower()
def test_validate_with_conversation_id_required(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -49,7 +49,7 @@ class TestBaseAnswerValidation:
assert "conversation_id" in result.json["message"].lower()
def test_validate_passes_with_all_required_fields(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -65,7 +65,7 @@ class TestUsageChecking:
pass
def test_returns_none_when_no_api_key(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -88,7 +88,7 @@ class TestGPTModelRetrieval:
pass
def test_initializes_gpt_model(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -102,7 +102,7 @@ class TestConversationServiceIntegration:
pass
def test_initializes_conversation_service(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -117,7 +117,7 @@ class TestCompleteStreamMethod:
pass
def test_streams_answer_chunks(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -149,7 +149,7 @@ class TestCompleteStreamMethod:
assert '"answer": "world!"' in answer_chunks[1]
def test_streams_sources(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -180,7 +180,7 @@ class TestCompleteStreamMethod:
assert '"title": "doc1.txt"' in source_chunks[0]
def test_handles_error_during_streaming(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -209,7 +209,7 @@ class TestCompleteStreamMethod:
Without the flag, sanitize_api_error substring-matches "quota" and rewrites the
message into a misleading rate-limit notice.
"""
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -248,7 +248,7 @@ class TestCompleteStreamMethod:
emitted as ``type: error`` -- the client treats an error event as terminal and
disables reconnect -- and its text must not be run through sanitize_api_error.
"""
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -277,7 +277,7 @@ class TestCompleteStreamMethod:
def test_non_user_facing_error_is_sanitized(self, mock_mongo_db, flask_app):
"""A raw error without the flag is still routed through sanitize_api_error."""
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -303,7 +303,7 @@ class TestCompleteStreamMethod:
assert "Rate limit exceeded" in error_chunks[0]
def test_saves_conversation_when_enabled(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -354,7 +354,7 @@ class TestCompleteStreamMethod:
so tools needing a conversation home (``scheduler`` in an agentless
chat) see it on the very first call.
"""
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -439,7 +439,7 @@ class TestCompleteStreamMethod:
"""A pure ``requires_client_execution`` pause must NOT publish a
``tool.approval.required`` event — the client resolves it, so the
notification would be non-actionable noise."""
from application.api.answer.routes import base as base_mod
from docsgpt.api.answer.routes import base as base_mod
with flask_app.app_context(), patch.object(
base_mod, "publish_user_event"
@@ -465,7 +465,7 @@ class TestCompleteStreamMethod:
):
"""A pause with an ``awaiting_approval`` call publishes once, and the
payload surfaces only the approval call (not the client-side one)."""
from application.api.answer.routes import base as base_mod
from docsgpt.api.answer.routes import base as base_mod
with flask_app.app_context(), patch.object(
base_mod, "publish_user_event"
@@ -505,7 +505,7 @@ class TestProcessResponseStream:
def test_processes_complete_stream(self, mock_mongo_db, flask_app):
import json
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -529,7 +529,7 @@ class TestProcessResponseStream:
def test_handles_stream_error(self, mock_mongo_db, flask_app):
import json
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -544,7 +544,7 @@ class TestProcessResponseStream:
assert result["error"] == "Test error"
def test_handles_malformed_stream_data(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -564,7 +564,7 @@ class TestErrorStreamGenerate:
pass
def test_generates_error_stream(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -588,9 +588,9 @@ def _patch_base_db(conn):
yield conn
with patch(
"application.api.answer.routes.base.db_readonly", _yield
"docsgpt.api.answer.routes.base.db_readonly", _yield
), patch(
"application.api.answer.routes.base.db_session", _yield
"docsgpt.api.answer.routes.base.db_session", _yield
):
yield
@@ -598,7 +598,7 @@ def _patch_base_db(conn):
@pytest.mark.unit
class TestCheckUsagePgConn:
def test_invalid_api_key_returns_401(self, pg_conn, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with _patch_base_db(pg_conn), flask_app.app_context():
resource = BaseAnswerResource()
@@ -607,8 +607,8 @@ class TestCheckUsagePgConn:
assert result.status_code == 401
def test_no_limits_returns_none(self, pg_conn, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.answer.routes.base import BaseAnswerResource
from docsgpt.storage.db.repositories.agents import AgentsRepository
AgentsRepository(pg_conn).create(
"owner", "a", "published", key="k1",
@@ -620,8 +620,8 @@ class TestCheckUsagePgConn:
assert result is None
def test_within_limit_returns_none(self, pg_conn, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.answer.routes.base import BaseAnswerResource
from docsgpt.storage.db.repositories.agents import AgentsRepository
AgentsRepository(pg_conn).create(
"owner", "a", "published", key="k2",
@@ -633,9 +633,9 @@ class TestCheckUsagePgConn:
assert result is None
def test_token_limit_exceeded_returns_429(self, pg_conn, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.token_usage import (
from docsgpt.api.answer.routes.base import BaseAnswerResource
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.token_usage import (
TokenUsageRepository,
)
@@ -655,9 +655,9 @@ class TestCheckUsagePgConn:
assert result.status_code == 429
def test_request_limit_exceeded_returns_429(self, pg_conn, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.token_usage import (
from docsgpt.api.answer.routes.base import BaseAnswerResource
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.token_usage import (
TokenUsageRepository,
)
@@ -678,8 +678,8 @@ class TestCheckUsagePgConn:
def test_string_True_limited_token_mode_parsed(self, pg_conn, flask_app):
"""Legacy Mongo sometimes stored ``limited_token_mode`` as the
string 'True'; verify the parse branch."""
from application.api.answer.routes.base import BaseAnswerResource
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.answer.routes.base import BaseAnswerResource
from docsgpt.storage.db.repositories.agents import AgentsRepository
# Store bool=False in DB (limited_token_mode default). Test uses
# string 'True' by mutating the row directly.
+26 -26
View File
@@ -1,6 +1,6 @@
"""Tests for /api/search route (application/api/answer/routes/search.py).
"""Tests for /api/search route (docsgpt/api/answer/routes/search.py).
Retrieval logic lives in ``application/services/search_service.py`` and
Retrieval logic lives in ``docsgpt/services/search_service.py`` and
has its own unit tests in ``tests/services/test_search_service.py``. The
tests below focus on what the route specifically owns:
@@ -21,7 +21,7 @@ import pytest
@pytest.mark.unit
class TestSearchResourceValidation:
def test_returns_400_when_question_missing(self, flask_app):
from application.api.answer.routes.search import SearchResource
from docsgpt.api.answer.routes.search import SearchResource
with flask_app.app_context():
with flask_app.test_request_context(json={"api_key": "test_key"}):
@@ -30,7 +30,7 @@ class TestSearchResourceValidation:
assert "question" in result.json["error"]
def test_returns_400_when_api_key_missing(self, flask_app):
from application.api.answer.routes.search import SearchResource
from docsgpt.api.answer.routes.search import SearchResource
with flask_app.app_context():
with flask_app.test_request_context(json={"question": "test query"}):
@@ -49,13 +49,13 @@ class TestSearchResourceExceptionMapping:
"""
def test_invalid_api_key_returns_401(self, flask_app):
from application.api.answer.routes.search import SearchResource
from application.services.search_service import InvalidAPIKey
from docsgpt.api.answer.routes.search import SearchResource
from docsgpt.services.search_service import InvalidAPIKey
with flask_app.app_context(), flask_app.test_request_context(
json={"question": "q", "api_key": "bad"}
), patch(
"application.api.answer.routes.search.search",
"docsgpt.api.answer.routes.search.search",
side_effect=InvalidAPIKey(),
):
result = SearchResource().post()
@@ -63,13 +63,13 @@ class TestSearchResourceExceptionMapping:
assert result.json == {"error": "Invalid API key"}
def test_search_failed_returns_500(self, flask_app):
from application.api.answer.routes.search import SearchResource
from application.services.search_service import SearchFailed
from docsgpt.api.answer.routes.search import SearchResource
from docsgpt.services.search_service import SearchFailed
with flask_app.app_context(), flask_app.test_request_context(
json={"question": "q", "api_key": "k"}
), patch(
"application.api.answer.routes.search.search",
"docsgpt.api.answer.routes.search.search",
side_effect=SearchFailed("boom"),
):
result = SearchResource().post()
@@ -77,13 +77,13 @@ class TestSearchResourceExceptionMapping:
assert result.json == {"error": "Search failed"}
def test_happy_path_passes_service_result_through(self, flask_app):
from application.api.answer.routes.search import SearchResource
from docsgpt.api.answer.routes.search import SearchResource
hits = [{"text": "t", "title": "T", "source": "s"}]
with flask_app.app_context(), flask_app.test_request_context(
json={"question": "q", "api_key": "k", "chunks": 7}
), patch(
"application.api.answer.routes.search.search",
"docsgpt.api.answer.routes.search.search",
return_value=hits,
) as mock_search:
result = SearchResource().post()
@@ -92,12 +92,12 @@ class TestSearchResourceExceptionMapping:
mock_search.assert_called_once_with("k", "q", 7)
def test_default_chunks_is_5(self, flask_app):
from application.api.answer.routes.search import SearchResource
from docsgpt.api.answer.routes.search import SearchResource
with flask_app.app_context(), flask_app.test_request_context(
json={"question": "q", "api_key": "k"} # no chunks field
), patch(
"application.api.answer.routes.search.search",
"docsgpt.api.answer.routes.search.search",
return_value=[],
) as mock_search:
SearchResource().post()
@@ -121,14 +121,14 @@ def _patch_search_db(conn):
yield conn
with patch(
"application.services.search_service.db_readonly", _yield
"docsgpt.services.search_service.db_readonly", _yield
):
yield
class TestSearchResourcePgConn:
def test_invalid_api_key_returns_401(self, pg_conn, flask_app):
from application.api.answer.routes.search import SearchResource
from docsgpt.api.answer.routes.search import SearchResource
with _patch_search_db(pg_conn), flask_app.app_context():
with flask_app.test_request_context(
@@ -138,8 +138,8 @@ class TestSearchResourcePgConn:
assert result.status_code == 401
def test_no_sources_returns_empty(self, pg_conn, flask_app):
from application.api.answer.routes.search import SearchResource
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.answer.routes.search import SearchResource
from docsgpt.storage.db.repositories.agents import AgentsRepository
AgentsRepository(pg_conn).create(
"u", "a", "published", key="no-src-key",
@@ -153,9 +153,9 @@ class TestSearchResourcePgConn:
assert result.json == []
def test_search_returns_results(self, pg_conn, flask_app):
from application.api.answer.routes.search import SearchResource
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.api.answer.routes.search import SearchResource
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.sources import SourcesRepository
src = SourcesRepository(pg_conn).create("src", user_id="u")
AgentsRepository(pg_conn).create(
@@ -170,7 +170,7 @@ class TestSearchResourcePgConn:
]
with _patch_search_db(pg_conn), patch(
"application.services.search_service.VectorCreator.create_vectorstore",
"docsgpt.services.search_service.VectorCreator.create_vectorstore",
return_value=fake_vs,
), flask_app.app_context():
with flask_app.test_request_context(
@@ -181,9 +181,9 @@ class TestSearchResourcePgConn:
assert len(result.json) == 1
def test_search_uses_extra_source_ids(self, pg_conn, flask_app):
from application.api.answer.routes.search import SearchResource
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.api.answer.routes.search import SearchResource
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.sources import SourcesRepository
src1 = SourcesRepository(pg_conn).create("s1", user_id="u")
src2 = SourcesRepository(pg_conn).create("s2", user_id="u")
@@ -198,7 +198,7 @@ class TestSearchResourcePgConn:
{"text": "one", "metadata": {"title": "A"}},
]
with _patch_search_db(pg_conn), patch(
"application.services.search_service.VectorCreator.create_vectorstore",
"docsgpt.services.search_service.VectorCreator.create_vectorstore",
return_value=fake_vs,
), flask_app.app_context():
with flask_app.test_request_context(
+14 -14
View File
@@ -1,4 +1,4 @@
"""Tests for application/api/answer/routes/stream.py"""
"""Tests for docsgpt/api/answer/routes/stream.py"""
import json
import uuid
@@ -15,7 +15,7 @@ _AGENT_ID = "507f1f77bcf86cd799439012"
def mock_stream_processor():
"""Create a mock StreamProcessor for stream tests."""
with patch(
"application.api.answer.routes.stream.StreamProcessor"
"docsgpt.api.answer.routes.stream.StreamProcessor"
) as MockProcessor:
processor = MagicMock()
processor.decoded_token = {"sub": "test_user"}
@@ -35,7 +35,7 @@ def stream_client(mock_mongo_db, flask_app):
"""Create a test client with the stream route registered."""
from flask_restx import Api
from application.api.answer.routes.stream import answer_ns
from docsgpt.api.answer.routes.stream import answer_ns
api = Api(flask_app)
api.add_namespace(answer_ns)
@@ -59,13 +59,13 @@ class TestStreamResourcePost:
yield f'data: {json.dumps({"type": "end"})}\n\n'
with patch(
"application.api.answer.routes.stream.StreamResource.validate_request",
"docsgpt.api.answer.routes.stream.StreamResource.validate_request",
return_value=None,
), patch(
"application.api.answer.routes.stream.StreamResource.check_usage",
"docsgpt.api.answer.routes.stream.StreamResource.check_usage",
return_value=None,
), patch(
"application.api.answer.routes.stream.StreamResource.complete_stream",
"docsgpt.api.answer.routes.stream.StreamResource.complete_stream",
side_effect=fake_stream,
):
resp = stream_client.post(
@@ -84,7 +84,7 @@ class TestStreamResourcePost:
):
mock_stream_processor.decoded_token = None
with patch(
"application.api.answer.routes.stream.StreamResource.validate_request",
"docsgpt.api.answer.routes.stream.StreamResource.validate_request",
return_value=None,
):
resp = stream_client.post(
@@ -101,10 +101,10 @@ class TestStreamResourcePost:
self, stream_client, mock_stream_processor
):
with patch(
"application.api.answer.routes.stream.StreamResource.validate_request",
"docsgpt.api.answer.routes.stream.StreamResource.validate_request",
return_value=None,
), patch(
"application.api.answer.routes.stream.StreamResource.check_usage",
"docsgpt.api.answer.routes.stream.StreamResource.check_usage",
) as mock_check:
mock_check.return_value = ({"error": "Usage limit exceeded"}, 429)
resp = stream_client.post(
@@ -119,7 +119,7 @@ class TestStreamResourcePost:
):
mock_stream_processor.build_agent.side_effect = ValueError("bad data")
with patch(
"application.api.answer.routes.stream.StreamResource.validate_request",
"docsgpt.api.answer.routes.stream.StreamResource.validate_request",
return_value=None,
):
resp = stream_client.post(
@@ -137,7 +137,7 @@ class TestStreamResourcePost:
):
mock_stream_processor.build_agent.side_effect = RuntimeError("crash")
with patch(
"application.api.answer.routes.stream.StreamResource.validate_request",
"docsgpt.api.answer.routes.stream.StreamResource.validate_request",
return_value=None,
):
resp = stream_client.post(
@@ -172,13 +172,13 @@ class TestStreamResourcePost:
conv_id = str(uuid.uuid4())
with patch(
"application.api.answer.routes.stream.StreamResource.validate_request",
"docsgpt.api.answer.routes.stream.StreamResource.validate_request",
return_value=None,
), patch(
"application.api.answer.routes.stream.StreamResource.check_usage",
"docsgpt.api.answer.routes.stream.StreamResource.check_usage",
return_value=None,
), patch(
"application.api.answer.routes.stream.StreamResource.complete_stream",
"docsgpt.api.answer.routes.stream.StreamResource.complete_stream",
side_effect=fake_stream,
) as mock_complete:
resp = stream_client.post(
@@ -14,16 +14,16 @@ from unittest.mock import MagicMock, patch
import pytest
from application.api.answer.services.compression import CompressionService
from application.api.answer.services.compression.orchestrator import (
from docsgpt.api.answer.services.compression import CompressionService
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,
)
from application.api.answer.services.compression.token_counter import TokenCounter
from application.api.answer.services.compression.types import CompressionResult
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.compression.token_counter import TokenCounter
from docsgpt.api.answer.services.compression.types import CompressionResult
from docsgpt.api.answer.services.conversation_service import (
COMPRESSION_SUMMARY_PROMPT,
)
@@ -106,16 +106,16 @@ class TestTurnStartReuse:
assert result.last_compression_at is None
@patch(
"application.api.answer.services.compression.orchestrator.get_provider_from_model_id",
"docsgpt.api.answer.services.compression.orchestrator.get_provider_from_model_id",
return_value="openai",
)
@patch(
"application.api.answer.services.compression.orchestrator.get_api_key_for_provider",
"docsgpt.api.answer.services.compression.orchestrator.get_api_key_for_provider",
return_value="sk",
)
@patch("application.api.answer.services.compression.orchestrator.LLMCreator")
@patch("application.api.answer.services.compression.orchestrator.CompressionService")
@patch("application.api.answer.services.compression.orchestrator.settings")
@patch("docsgpt.api.answer.services.compression.orchestrator.LLMCreator")
@patch("docsgpt.api.answer.services.compression.orchestrator.CompressionService")
@patch("docsgpt.api.answer.services.compression.orchestrator.settings")
def test_over_threshold_compresses_only_the_tail(
self,
mock_settings,
@@ -172,7 +172,7 @@ class TestEffectiveTokenCount:
)
@patch(
"application.api.answer.services.compression.threshold_checker.get_token_limit",
"docsgpt.api.answer.services.compression.threshold_checker.get_token_limit",
return_value=1000,
)
def test_should_compress_uses_effective_count(self, _limit):
@@ -252,7 +252,7 @@ class TestSummaryRowMarker:
who types the label text as a question keeps that turn."""
def test_marked_row_is_a_summary_row(self):
from application.api.answer.services.compression.types import (
from docsgpt.api.answer.services.compression.types import (
COMPRESSION_SUMMARY_MARKER,
is_compression_summary_row,
)
@@ -261,7 +261,7 @@ class TestSummaryRowMarker:
assert is_compression_summary_row(row) is True
def test_legacy_row_without_metadata_is_a_summary_row(self):
from application.api.answer.services.compression.types import is_compression_summary_row
from docsgpt.api.answer.services.compression.types import is_compression_summary_row
assert is_compression_summary_row({"prompt": COMPRESSION_SUMMARY_PROMPT, "response": "S"}) is True
assert is_compression_summary_row(
@@ -269,7 +269,7 @@ class TestSummaryRowMarker:
) is True
def test_user_turn_with_the_label_text_is_kept(self):
from application.api.answer.services.compression.types import is_compression_summary_row
from docsgpt.api.answer.services.compression.types import is_compression_summary_row
real_turn = {
"prompt": COMPRESSION_SUMMARY_PROMPT,
@@ -309,16 +309,16 @@ class TestIncrementalTailExcludesSummaryRows:
svc.compress_conversation(conv, compress_up_to_index=2, start_index=2)
@patch(
"application.api.answer.services.compression.orchestrator.get_provider_from_model_id",
"docsgpt.api.answer.services.compression.orchestrator.get_provider_from_model_id",
return_value="openai",
)
@patch(
"application.api.answer.services.compression.orchestrator.get_api_key_for_provider",
"docsgpt.api.answer.services.compression.orchestrator.get_api_key_for_provider",
return_value="sk",
)
@patch("application.api.answer.services.compression.orchestrator.LLMCreator")
@patch("application.api.answer.services.compression.orchestrator.CompressionService")
@patch("application.api.answer.services.compression.orchestrator.settings")
@patch("docsgpt.api.answer.services.compression.orchestrator.LLMCreator")
@patch("docsgpt.api.answer.services.compression.orchestrator.CompressionService")
@patch("docsgpt.api.answer.services.compression.orchestrator.settings")
def test_orchestrator_reuses_summary_when_only_summary_rows_follow_the_point(
self, mock_settings, MockCompressionService, MockLLMCreator, _key, _provider,
orchestrator, conversation_service, threshold_checker,
@@ -355,16 +355,16 @@ class TestAbsolutePersistIndex:
assert metadata.query_index == 19
@patch(
"application.api.answer.services.compression.orchestrator.get_provider_from_model_id",
"docsgpt.api.answer.services.compression.orchestrator.get_provider_from_model_id",
return_value="openai",
)
@patch(
"application.api.answer.services.compression.orchestrator.get_api_key_for_provider",
"docsgpt.api.answer.services.compression.orchestrator.get_api_key_for_provider",
return_value="sk",
)
@patch("application.api.answer.services.compression.orchestrator.LLMCreator")
@patch("application.api.answer.services.compression.orchestrator.CompressionService")
@patch("application.api.answer.services.compression.orchestrator.settings")
@patch("docsgpt.api.answer.services.compression.orchestrator.LLMCreator")
@patch("docsgpt.api.answer.services.compression.orchestrator.CompressionService")
@patch("docsgpt.api.answer.services.compression.orchestrator.settings")
def test_mid_execution_builds_on_the_carried_summary_and_persists_the_absolute_index(
self, mock_settings, MockCompressionService, MockLLMCreator, _key, _provider,
orchestrator, conversation_service,
@@ -461,16 +461,16 @@ class TestUnusableSavedPoints:
assert [q["prompt"] for q in recent] == ["q2", "q3", "q4"]
@patch(
"application.api.answer.services.compression.orchestrator.get_provider_from_model_id",
"docsgpt.api.answer.services.compression.orchestrator.get_provider_from_model_id",
return_value="openai",
)
@patch(
"application.api.answer.services.compression.orchestrator.get_api_key_for_provider",
"docsgpt.api.answer.services.compression.orchestrator.get_api_key_for_provider",
return_value="sk",
)
@patch("application.api.answer.services.compression.orchestrator.LLMCreator")
@patch("application.api.answer.services.compression.orchestrator.CompressionService")
@patch("application.api.answer.services.compression.orchestrator.settings")
@patch("docsgpt.api.answer.services.compression.orchestrator.LLMCreator")
@patch("docsgpt.api.answer.services.compression.orchestrator.CompressionService")
@patch("docsgpt.api.answer.services.compression.orchestrator.settings")
def test_recompression_starts_after_the_latest_usable_point(
self, mock_settings, MockCompressionService, MockLLMCreator, _key, _provider,
orchestrator, conversation_service, threshold_checker,
@@ -1,9 +1,9 @@
"""Tests for application/api/answer/services/compression/message_builder.py"""
"""Tests for docsgpt/api/answer/services/compression/message_builder.py"""
import pytest
from application.api.answer.services.compression.message_builder import MessageBuilder
from docsgpt.api.answer.services.compression.message_builder import MessageBuilder
@pytest.mark.unit
@@ -1,13 +1,13 @@
"""Tests for application/api/answer/services/compression/orchestrator.py"""
"""Tests for docsgpt/api/answer/services/compression/orchestrator.py"""
from unittest.mock import MagicMock, patch
import pytest
from application.api.answer.services.compression.orchestrator import (
from docsgpt.api.answer.services.compression.orchestrator import (
CompressionOrchestrator,
)
from application.api.answer.services.compression.types import (
from docsgpt.api.answer.services.compression.types import (
CompressionMetadata,
CompressionResult,
)
@@ -172,14 +172,14 @@ class TestCompressIfNeeded:
@pytest.mark.unit
class TestPerformCompression:
@patch(
"application.api.answer.services.compression.orchestrator.get_provider_from_model_id"
"docsgpt.api.answer.services.compression.orchestrator.get_provider_from_model_id"
)
@patch(
"application.api.answer.services.compression.orchestrator.get_api_key_for_provider"
"docsgpt.api.answer.services.compression.orchestrator.get_api_key_for_provider"
)
@patch("application.api.answer.services.compression.orchestrator.LLMCreator")
@patch("application.api.answer.services.compression.orchestrator.CompressionService")
@patch("application.api.answer.services.compression.orchestrator.settings")
@patch("docsgpt.api.answer.services.compression.orchestrator.LLMCreator")
@patch("docsgpt.api.answer.services.compression.orchestrator.CompressionService")
@patch("docsgpt.api.answer.services.compression.orchestrator.settings")
def test_successful_compression(
self,
mock_settings,
@@ -228,13 +228,13 @@ class TestPerformCompression:
mock_svc_instance.compress_and_save.assert_called_once()
@patch(
"application.api.answer.services.compression.orchestrator.get_provider_from_model_id"
"docsgpt.api.answer.services.compression.orchestrator.get_provider_from_model_id"
)
@patch(
"application.api.answer.services.compression.orchestrator.get_api_key_for_provider"
"docsgpt.api.answer.services.compression.orchestrator.get_api_key_for_provider"
)
@patch("application.api.answer.services.compression.orchestrator.LLMCreator")
@patch("application.api.answer.services.compression.orchestrator.settings")
@patch("docsgpt.api.answer.services.compression.orchestrator.LLMCreator")
@patch("docsgpt.api.answer.services.compression.orchestrator.settings")
def test_uses_compression_model_override(
self,
mock_settings,
@@ -253,7 +253,7 @@ class TestPerformCompression:
conversation = {"queries": [{"prompt": "q", "response": "r"}], "agent_id": "a"}
with patch(
"application.api.answer.services.compression.orchestrator.CompressionService"
"docsgpt.api.answer.services.compression.orchestrator.CompressionService"
) as MockCS:
mock_svc = MagicMock()
mock_svc.compress_and_save.return_value = MagicMock(
@@ -280,14 +280,14 @@ class TestPerformCompression:
)
@patch(
"application.api.answer.services.compression.orchestrator.get_provider_from_model_id"
"docsgpt.api.answer.services.compression.orchestrator.get_provider_from_model_id"
)
@patch(
"application.api.answer.services.compression.orchestrator.get_api_key_for_provider"
"docsgpt.api.answer.services.compression.orchestrator.get_api_key_for_provider"
)
@patch("application.api.answer.services.compression.orchestrator.LLMCreator")
@patch("application.api.answer.services.compression.orchestrator.CompressionService")
@patch("application.api.answer.services.compression.orchestrator.settings")
@patch("docsgpt.api.answer.services.compression.orchestrator.LLMCreator")
@patch("docsgpt.api.answer.services.compression.orchestrator.CompressionService")
@patch("docsgpt.api.answer.services.compression.orchestrator.settings")
def test_no_queries_returns_no_compression(
self,
mock_settings,
@@ -327,9 +327,9 @@ class TestPerformCompression:
}
with patch(
"application.api.answer.services.compression.orchestrator.settings"
"docsgpt.api.answer.services.compression.orchestrator.settings"
) as mock_settings, patch(
"application.api.answer.services.compression.orchestrator.get_provider_from_model_id",
"docsgpt.api.answer.services.compression.orchestrator.get_provider_from_model_id",
side_effect=RuntimeError("provider error"),
):
mock_settings.COMPRESSION_MODEL_OVERRIDE = None
@@ -1,11 +1,11 @@
"""Tests for application/api/answer/services/compression/service.py"""
"""Tests for docsgpt/api/answer/services/compression/service.py"""
from unittest.mock import MagicMock, patch
import pytest
from application.api.answer.services.compression.service import CompressionService
from application.api.answer.services.compression.types import CompressionMetadata
from docsgpt.api.answer.services.compression.service import CompressionService
from docsgpt.api.answer.services.compression.types import CompressionMetadata
@pytest.fixture
@@ -48,11 +48,11 @@ def sample_conversation():
@pytest.mark.unit
class TestCompressionServiceInit:
@patch("application.api.answer.services.compression.service.settings")
@patch("docsgpt.api.answer.services.compression.service.settings")
def test_default_prompt_builder(self, mock_settings, mock_llm):
mock_settings.COMPRESSION_PROMPT_VERSION = "v1.0"
with patch(
"application.api.answer.services.compression.service.CompressionPromptBuilder"
"docsgpt.api.answer.services.compression.service.CompressionPromptBuilder"
):
svc = CompressionService(llm=mock_llm, model_id="gpt-4")
assert svc.llm is mock_llm
@@ -81,7 +81,7 @@ class TestCompressConversation:
)
with patch(
"application.api.answer.services.compression.service.TokenCounter"
"docsgpt.api.answer.services.compression.service.TokenCounter"
) as MockTC:
MockTC.count_query_tokens.return_value = 1000
MockTC.count_message_tokens.return_value = 100
@@ -144,7 +144,7 @@ class TestCompressConversation:
)
with patch(
"application.api.answer.services.compression.service.TokenCounter"
"docsgpt.api.answer.services.compression.service.TokenCounter"
) as MockTC:
MockTC.count_query_tokens.return_value = 500
MockTC.count_message_tokens.return_value = 50
@@ -170,7 +170,7 @@ class TestCompressConversation:
)
with patch(
"application.api.answer.services.compression.service.TokenCounter"
"docsgpt.api.answer.services.compression.service.TokenCounter"
) as MockTC:
MockTC.count_query_tokens.return_value = 1000
MockTC.count_message_tokens.return_value = 0
@@ -195,7 +195,7 @@ class TestCompressConversation:
)
with patch(
"application.api.answer.services.compression.service.TokenCounter"
"docsgpt.api.answer.services.compression.service.TokenCounter"
) as MockTC:
MockTC.count_query_tokens.return_value = 100
with pytest.raises(RuntimeError, match="LLM error"):
@@ -222,7 +222,7 @@ class TestCompressAndSave:
)
with patch(
"application.api.answer.services.compression.service.TokenCounter"
"docsgpt.api.answer.services.compression.service.TokenCounter"
) as MockTC:
MockTC.count_query_tokens.return_value = 500
MockTC.count_message_tokens.return_value = 50
@@ -487,7 +487,7 @@ class TestBoundRecentQueries:
self, mock_llm, monkeypatch
):
monkeypatch.setattr(
"application.api.answer.services.compression.service.settings.COMPRESSION_RECENT_FIELD_MAX_TOKENS",
"docsgpt.api.answer.services.compression.service.settings.COMPRESSION_RECENT_FIELD_MAX_TOKENS",
50,
raising=False,
)
@@ -507,7 +507,7 @@ class TestBoundRecentQueries:
def test_oversized_tool_result_is_trimmed(self, mock_llm, monkeypatch):
monkeypatch.setattr(
"application.api.answer.services.compression.service.settings.COMPRESSION_RECENT_FIELD_MAX_TOKENS",
"docsgpt.api.answer.services.compression.service.settings.COMPRESSION_RECENT_FIELD_MAX_TOKENS",
50,
raising=False,
)
@@ -536,7 +536,7 @@ class TestBoundRecentQueries:
def test_zero_cap_disables_bounding(self, mock_llm, monkeypatch):
monkeypatch.setattr(
"application.api.answer.services.compression.service.settings.COMPRESSION_RECENT_FIELD_MAX_TOKENS",
"docsgpt.api.answer.services.compression.service.settings.COMPRESSION_RECENT_FIELD_MAX_TOKENS",
0,
raising=False,
)
@@ -7,18 +7,18 @@ import pytest
class TestCompressionThresholdChecker:
def _make_checker(self, pct=0.7):
from application.api.answer.services.compression.threshold_checker import (
from docsgpt.api.answer.services.compression.threshold_checker import (
CompressionThresholdChecker,
)
return CompressionThresholdChecker(threshold_percentage=pct)
@patch(
"application.api.answer.services.compression.threshold_checker.get_token_limit",
"docsgpt.api.answer.services.compression.threshold_checker.get_token_limit",
return_value=8000,
)
@patch(
"application.api.answer.services.compression.threshold_checker.TokenCounter.count_message_tokens",
"docsgpt.api.answer.services.compression.threshold_checker.TokenCounter.count_message_tokens",
return_value=6000,
)
def test_check_message_tokens_above_threshold(self, mock_count, mock_limit):
@@ -26,11 +26,11 @@ class TestCompressionThresholdChecker:
assert checker.check_message_tokens([{"role": "user"}], "gpt-4") is True
@patch(
"application.api.answer.services.compression.threshold_checker.get_token_limit",
"docsgpt.api.answer.services.compression.threshold_checker.get_token_limit",
return_value=8000,
)
@patch(
"application.api.answer.services.compression.threshold_checker.TokenCounter.count_message_tokens",
"docsgpt.api.answer.services.compression.threshold_checker.TokenCounter.count_message_tokens",
return_value=1000,
)
def test_check_message_tokens_below_threshold(self, mock_count, mock_limit):
@@ -38,7 +38,7 @@ class TestCompressionThresholdChecker:
assert checker.check_message_tokens([{"role": "user"}], "gpt-4") is False
@patch(
"application.api.answer.services.compression.threshold_checker.TokenCounter.count_message_tokens",
"docsgpt.api.answer.services.compression.threshold_checker.TokenCounter.count_message_tokens",
side_effect=Exception("Token error"),
)
def test_check_message_tokens_exception_returns_false(self, mock_count):
@@ -1,10 +1,10 @@
"""Tests for application/api/answer/services/compression/types.py"""
"""Tests for docsgpt/api/answer/services/compression/types.py"""
from datetime import datetime, timezone
import pytest
from application.api.answer.services.compression.types import (
from docsgpt.api.answer.services.compression.types import (
CompressionMetadata,
CompressionResult,
)
@@ -1,4 +1,4 @@
"""Tests for application/api/answer/services/continuation_service.py using pg_conn."""
"""Tests for docsgpt/api/answer/services/continuation_service.py using pg_conn."""
from contextlib import contextmanager
from unittest.mock import patch
@@ -15,10 +15,10 @@ def _patch_db(conn):
yield conn
with patch(
"application.api.answer.services.continuation_service.db_readonly",
"docsgpt.api.answer.services.continuation_service.db_readonly",
_yield,
), patch(
"application.api.answer.services.continuation_service.db_session",
"docsgpt.api.answer.services.continuation_service.db_session",
_yield,
):
yield
@@ -26,21 +26,21 @@ def _patch_db(conn):
class TestMakeSerializable:
def test_uuid_becomes_string(self):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
_make_serializable,
)
u = uuid4()
assert _make_serializable(u) == str(u)
def test_dict_keys_stringified(self):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
_make_serializable,
)
got = _make_serializable({42: "a", "b": 2})
assert got == {"42": "a", "b": 2}
def test_list_elements_recursively_serialized(self):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
_make_serializable,
)
u = uuid4()
@@ -52,7 +52,7 @@ class TestMakeSerializable:
# the shared serialization module — base64 is lossless and round-
# trippable (UTF-8-replace silently corrupted binary payloads).
import base64
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
_make_serializable,
)
got = _make_serializable(b"hello")
@@ -60,7 +60,7 @@ class TestMakeSerializable:
def test_bytes_arbitrary_binary_roundtrips(self):
import base64
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
_make_serializable,
)
raw = b"\xff\xfe\x00\x10"
@@ -69,7 +69,7 @@ class TestMakeSerializable:
assert base64.b64decode(got) == raw
def test_passes_through_primitives(self):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
_make_serializable,
)
assert _make_serializable("hello") == "hello"
@@ -83,7 +83,7 @@ class TestMakeSerializable:
# rows, which would otherwise blow up json.dumps in pending_tool_state.
import json
from datetime import datetime, timezone
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
_make_serializable,
)
@@ -97,7 +97,7 @@ class TestMakeSerializable:
# where each tool row has timestamp fields buried under string keys.
import json
from datetime import datetime, timezone
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
_make_serializable,
)
@@ -116,7 +116,7 @@ class TestMakeSerializable:
def test_date_becomes_iso_string(self):
from datetime import date
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
_make_serializable,
)
assert _make_serializable(date(2026, 5, 2)) == "2026-05-02"
@@ -124,10 +124,10 @@ class TestMakeSerializable:
class TestContinuationServiceSaveLoad:
def test_save_and_load_state(self, pg_conn):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ContinuationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -152,7 +152,7 @@ class TestContinuationServiceSaveLoad:
assert loaded["messages"] == [{"role": "user", "content": "hi"}]
def test_load_state_returns_none_when_not_found(self, pg_conn):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ContinuationService,
)
@@ -164,11 +164,11 @@ class TestContinuationServiceSaveLoad:
assert got is None
def test_claim_state_is_atomic_and_duplicate_is_conflict(self, pg_conn):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ContinuationService,
ResumeInProgressError,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -193,10 +193,10 @@ class TestContinuationServiceSaveLoad:
service.claim_state(conv_id, user)
def test_expired_state_is_neither_loaded_nor_claimed(self, pg_conn):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ContinuationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -226,10 +226,10 @@ class TestContinuationServiceSaveLoad:
assert service.claim_state(conv_id, user) is None
def test_save_state_no_client_tools(self, pg_conn):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ContinuationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -253,10 +253,10 @@ class TestContinuationServiceSaveLoad:
assert loaded is not None
def test_delete_state_returns_true_when_deleted(self, pg_conn):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ContinuationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -279,7 +279,7 @@ class TestContinuationServiceSaveLoad:
assert got is True
def test_delete_state_returns_false_when_missing(self, pg_conn):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ContinuationService,
)
@@ -301,7 +301,7 @@ class TestContinuationServiceLegacyIdHandling:
LEGACY_OBJECTID = "507f1f77bcf86cd799439011"
def test_load_state_unresolvable_legacy_id_returns_none(self, pg_conn):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ContinuationService,
)
@@ -311,7 +311,7 @@ class TestContinuationServiceLegacyIdHandling:
assert got is None
def test_delete_state_unresolvable_legacy_id_returns_false(self, pg_conn):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ContinuationService,
)
@@ -323,7 +323,7 @@ class TestContinuationServiceLegacyIdHandling:
def test_save_state_unresolvable_legacy_id_raises(self, pg_conn):
import pytest
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ContinuationService,
)
@@ -341,10 +341,10 @@ class TestContinuationServiceLegacyIdHandling:
)
def test_load_state_resolves_backfilled_legacy_id(self, pg_conn):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ContinuationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -1,4 +1,4 @@
"""Tests for application/api/answer/services/conversation_service.py."""
"""Tests for docsgpt/api/answer/services/conversation_service.py."""
from contextlib import contextmanager
from datetime import datetime, timezone
@@ -14,10 +14,10 @@ def _patch_db(conn):
yield conn
with patch(
"application.api.answer.services.conversation_service.db_session",
"docsgpt.api.answer.services.conversation_service.db_session",
_yield,
), patch(
"application.api.answer.services.conversation_service.db_readonly",
"docsgpt.api.answer.services.conversation_service.db_readonly",
_yield,
):
yield
@@ -25,13 +25,13 @@ def _patch_db(conn):
class TestConversationServiceGet:
def test_returns_none_when_no_conversation_id(self):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
assert ConversationService().get_conversation("", "u") is None
def test_returns_none_when_no_user_id(self):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
assert (
@@ -42,7 +42,7 @@ class TestConversationServiceGet:
)
def test_returns_none_when_not_found(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
with _patch_db(pg_conn):
@@ -52,10 +52,10 @@ class TestConversationServiceGet:
assert got is None
def test_returns_conversation_with_messages(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -74,7 +74,7 @@ class TestConversationServiceGet:
assert len(got["queries"]) == 1
def test_handles_exception_returns_none(self):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
@@ -84,7 +84,7 @@ class TestConversationServiceGet:
yield
with patch(
"application.api.answer.services.conversation_service.db_readonly",
"docsgpt.api.answer.services.conversation_service.db_readonly",
_broken,
):
got = ConversationService().get_conversation("abc", "u")
@@ -93,7 +93,7 @@ class TestConversationServiceGet:
class TestConversationServiceSave:
def test_raises_for_none_token(self):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
with pytest.raises(ValueError):
@@ -105,7 +105,7 @@ class TestConversationServiceSave:
)
def test_raises_when_no_user_in_token(self):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
with pytest.raises(ValueError):
@@ -117,10 +117,10 @@ class TestConversationServiceSave:
)
def test_creates_new_conversation(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -141,10 +141,10 @@ class TestConversationServiceSave:
assert got["name"] == "Title"
def test_appends_message_to_existing(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -166,10 +166,10 @@ class TestConversationServiceSave:
assert any(m["prompt"] == "q-new" for m in messages)
def test_updates_message_at_index(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -193,7 +193,7 @@ class TestConversationServiceSave:
assert messages[0]["prompt"] == "updated"
def test_raises_when_conversation_missing(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
with _patch_db(pg_conn), pytest.raises(ValueError):
@@ -206,10 +206,10 @@ class TestConversationServiceSave:
)
def test_save_with_empty_llm_title_falls_back(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -231,11 +231,11 @@ class TestConversationServiceSave:
class TestSaveUserQuestion:
def test_creates_conversation_and_reserves_message(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
TERMINATED_RESPONSE_PLACEHOLDER,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -261,10 +261,10 @@ class TestSaveUserQuestion:
assert messages[0]["request_id"] == result["request_id"]
def test_appends_to_existing_conversation(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -285,7 +285,7 @@ class TestSaveUserQuestion:
assert msgs[0]["prompt"] == "follow-up"
def test_raises_when_token_missing(self):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
with pytest.raises(ValueError):
@@ -302,10 +302,10 @@ class TestSaveUserQuestion:
the end. Pre-fix the WAL path appended unconditionally and the
old answer survived alongside the regenerated one.
"""
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -356,10 +356,10 @@ class TestSaveUserQuestion:
"""``index=0`` is a valid edge: it should drop every prior
message and reseat the placeholder at position 0.
"""
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -398,10 +398,10 @@ class TestSaveUserQuestion:
the create-then-reserve path silently treats it as a no-op
rather than truncating a freshly-created conversation.
"""
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -421,7 +421,7 @@ class TestSaveUserQuestion:
assert msgs[0]["prompt"] == "brand new q"
def test_raises_when_conversation_unauthorized(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
with _patch_db(pg_conn), pytest.raises(ValueError):
@@ -434,10 +434,10 @@ class TestSaveUserQuestion:
class TestFinalizeMessage:
def test_finalizes_complete(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -449,7 +449,7 @@ class TestFinalizeMessage:
question="q",
decoded_token={"sub": user},
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
MessageUpdateOutcome,
)
assert svc.finalize_message(
@@ -477,10 +477,10 @@ class TestFinalizeMessage:
def test_finalize_strips_null_bytes(self, pg_conn):
"""A NUL-laden tool result (07-17 PDF incident) must not kill the
conversation save — Postgres rejects \\x00 in text and jsonb."""
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
MessageUpdateOutcome,
)
@@ -515,10 +515,10 @@ class TestFinalizeMessage:
assert msgs[0]["metadata"] == {"key": "val"}
def test_finalizes_failed_records_error(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -531,7 +531,7 @@ class TestFinalizeMessage:
decoded_token={"sub": user},
)
err = RuntimeError("provider down")
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
MessageUpdateOutcome,
)
assert svc.finalize_message(
@@ -552,7 +552,7 @@ class TestFinalizeMessage:
rows as 'confirmed' for the same message_id."""
from sqlalchemy import text as sql_text
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
@@ -572,7 +572,7 @@ class TestFinalizeMessage:
),
{"cid": "c1", "mid": res["message_id"]},
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
MessageUpdateOutcome,
)
assert svc.finalize_message(
@@ -586,10 +586,10 @@ class TestFinalizeMessage:
assert status == "confirmed"
def test_finalize_returns_not_found_for_unknown_message(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
MessageUpdateOutcome,
)
with _patch_db(pg_conn):
@@ -614,10 +614,10 @@ class TestFinalizeMessage:
from sqlalchemy import text as sql_text
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories import conversations as conv_module
from docsgpt.storage.db.repositories import conversations as conv_module
user = "u-fin-rollback"
@@ -632,10 +632,10 @@ class TestFinalizeMessage:
raise
with patch(
"application.api.answer.services.conversation_service.db_session",
"docsgpt.api.answer.services.conversation_service.db_session",
_savepoint_session,
), patch(
"application.api.answer.services.conversation_service.db_readonly",
"docsgpt.api.answer.services.conversation_service.db_readonly",
_savepoint_session,
):
svc = ConversationService()
@@ -686,10 +686,10 @@ class TestFinalizeMessage:
assert msg_status == "pending"
def test_finalize_generates_title_when_provided(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -703,7 +703,7 @@ class TestFinalizeMessage:
question="long question that becomes the fallback name",
decoded_token={"sub": user},
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
MessageUpdateOutcome,
)
assert svc.finalize_message(
@@ -730,10 +730,10 @@ class TestSaveUserQuestionFinalizeFailedFlow:
"""LLM fails immediately; question stays queryable with status='failed' + error metadata."""
def test_failed_llm_leaves_question_persisted(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -769,10 +769,10 @@ class TestSaveUserQuestionFinalizeFailedFlow:
class TestCompressionMetadata:
def test_update_compression_metadata(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -792,7 +792,7 @@ class TestCompressionMetadata:
)
def test_update_compression_raises_on_error(self):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
@@ -802,13 +802,13 @@ class TestCompressionMetadata:
yield
with patch(
"application.api.answer.services.conversation_service.db_session",
"docsgpt.api.answer.services.conversation_service.db_session",
_broken,
), pytest.raises(RuntimeError):
ConversationService().update_compression_metadata("abc", {})
def test_append_compression_message_skips_empty_summary(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
@@ -818,10 +818,10 @@ class TestCompressionMetadata:
)
def test_append_compression_message_appends_summary(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -844,7 +844,7 @@ class TestCompressionMetadata:
assert any(m["response"] == "A summary" for m in messages)
def test_append_compression_message_swallows_error(self):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
@@ -854,7 +854,7 @@ class TestCompressionMetadata:
yield
with patch(
"application.api.answer.services.conversation_service.db_session",
"docsgpt.api.answer.services.conversation_service.db_session",
_broken,
):
ConversationService().append_compression_message(
@@ -864,7 +864,7 @@ class TestCompressionMetadata:
def test_get_compression_metadata_returns_none_for_missing(
self, pg_conn,
):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
with _patch_db(pg_conn):
@@ -881,7 +881,7 @@ class TestCompressionMetadata:
would raise and pollute logs with a stack trace every call."""
import logging
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
@@ -896,7 +896,7 @@ class TestCompressionMetadata:
)
def test_get_compression_metadata_handles_exception(self):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
@@ -906,7 +906,7 @@ class TestCompressionMetadata:
yield
with patch(
"application.api.answer.services.conversation_service.db_readonly",
"docsgpt.api.answer.services.conversation_service.db_readonly",
_broken,
):
got = ConversationService().get_compression_metadata("abc")
@@ -2,7 +2,7 @@
import pytest
from application.api.answer.services.persistence_policy import (
from docsgpt.api.answer.services.persistence_policy import (
VISIBILITY_HIDDEN,
VISIBILITY_LISTED,
resolve_persistence,
@@ -5,7 +5,7 @@ import pytest
class TestTemplateEngine:
def test_render_simple_template(self):
from application.templates.template_engine import TemplateEngine
from docsgpt.templates.template_engine import TemplateEngine
engine = TemplateEngine()
result = engine.render("Hello {{ name }}", {"name": "World"})
@@ -13,7 +13,7 @@ class TestTemplateEngine:
assert result == "Hello World"
def test_render_with_namespace(self):
from application.templates.template_engine import TemplateEngine
from docsgpt.templates.template_engine import TemplateEngine
engine = TemplateEngine()
context = {
@@ -27,7 +27,7 @@ class TestTemplateEngine:
assert result == "Alice is a admin on 2025-10-22"
def test_render_empty_template(self):
from application.templates.template_engine import TemplateEngine
from docsgpt.templates.template_engine import TemplateEngine
engine = TemplateEngine()
result = engine.render("", {"key": "value"})
@@ -35,7 +35,7 @@ class TestTemplateEngine:
assert result == ""
def test_render_template_without_variables(self):
from application.templates.template_engine import TemplateEngine
from docsgpt.templates.template_engine import TemplateEngine
engine = TemplateEngine()
result = engine.render("Just plain text", {})
@@ -43,7 +43,7 @@ class TestTemplateEngine:
assert result == "Just plain text"
def test_render_undefined_variable_returns_empty_string(self):
from application.templates.template_engine import TemplateEngine
from docsgpt.templates.template_engine import TemplateEngine
engine = TemplateEngine()
@@ -51,7 +51,7 @@ class TestTemplateEngine:
assert result == "Hello "
def test_render_syntax_error_raises_error(self):
from application.templates.template_engine import (
from docsgpt.templates.template_engine import (
TemplateEngine,
TemplateRenderError,
)
@@ -62,7 +62,7 @@ class TestTemplateEngine:
engine.render("Hello {{ name", {"name": "World"})
def test_render_blocks_unsafe_attribute_access(self):
from application.templates.template_engine import (
from docsgpt.templates.template_engine import (
TemplateEngine,
TemplateRenderError,
)
@@ -74,25 +74,25 @@ class TestTemplateEngine:
engine.render(payload, {})
def test_validate_template_valid(self):
from application.templates.template_engine import TemplateEngine
from docsgpt.templates.template_engine import TemplateEngine
engine = TemplateEngine()
assert engine.validate_template("Valid {{ variable }}") is True
def test_validate_template_invalid(self):
from application.templates.template_engine import TemplateEngine
from docsgpt.templates.template_engine import TemplateEngine
engine = TemplateEngine()
assert engine.validate_template("Invalid {{ variable") is False
def test_validate_empty_template(self):
from application.templates.template_engine import TemplateEngine
from docsgpt.templates.template_engine import TemplateEngine
engine = TemplateEngine()
assert engine.validate_template("") is True
def test_extract_variables(self):
from application.templates.template_engine import TemplateEngine
from docsgpt.templates.template_engine import TemplateEngine
engine = TemplateEngine()
template = "{{ user.name }} and {{ user.email }}"
@@ -106,7 +106,7 @@ class TestTemplateEngine:
class TestSystemNamespace:
def test_system_namespace_build(self):
from application.templates.namespaces import SystemNamespace
from docsgpt.templates.namespaces import SystemNamespace
builder = SystemNamespace()
context = builder.build(
@@ -120,7 +120,7 @@ class TestSystemNamespace:
assert "timestamp" in context
def test_system_namespace_generates_request_id(self):
from application.templates.namespaces import SystemNamespace
from docsgpt.templates.namespaces import SystemNamespace
builder = SystemNamespace()
context = builder.build(user_id="user_123")
@@ -129,13 +129,13 @@ class TestSystemNamespace:
assert len(context["request_id"]) > 0
def test_system_namespace_name(self):
from application.templates.namespaces import SystemNamespace
from docsgpt.templates.namespaces import SystemNamespace
builder = SystemNamespace()
assert builder.namespace_name == "system"
def test_system_namespace_date_format(self):
from application.templates.namespaces import SystemNamespace
from docsgpt.templates.namespaces import SystemNamespace
builder = SystemNamespace()
context = builder.build()
@@ -150,7 +150,7 @@ class TestSystemNamespace:
class TestPassthroughNamespace:
def test_passthrough_namespace_build(self):
from application.templates.namespaces import PassthroughNamespace
from docsgpt.templates.namespaces import PassthroughNamespace
builder = PassthroughNamespace()
passthrough_data = {"company": "Acme", "user_name": "John", "count": 42}
@@ -162,7 +162,7 @@ class TestPassthroughNamespace:
assert context["count"] == 42
def test_passthrough_namespace_empty(self):
from application.templates.namespaces import PassthroughNamespace
from docsgpt.templates.namespaces import PassthroughNamespace
builder = PassthroughNamespace()
context = builder.build(passthrough_data=None)
@@ -170,7 +170,7 @@ class TestPassthroughNamespace:
assert context == {}
def test_passthrough_namespace_filters_unsafe_values(self):
from application.templates.namespaces import PassthroughNamespace
from docsgpt.templates.namespaces import PassthroughNamespace
builder = PassthroughNamespace()
passthrough_data = {
@@ -190,7 +190,7 @@ class TestPassthroughNamespace:
assert "unsafe_list" not in context
def test_passthrough_namespace_allows_none_values(self):
from application.templates.namespaces import PassthroughNamespace
from docsgpt.templates.namespaces import PassthroughNamespace
builder = PassthroughNamespace()
passthrough_data = {"nullable_field": None}
@@ -200,7 +200,7 @@ class TestPassthroughNamespace:
assert context["nullable_field"] is None
def test_passthrough_namespace_name(self):
from application.templates.namespaces import PassthroughNamespace
from docsgpt.templates.namespaces import PassthroughNamespace
builder = PassthroughNamespace()
assert builder.namespace_name == "passthrough"
@@ -210,7 +210,7 @@ class TestPassthroughNamespace:
class TestSourceNamespace:
def test_source_namespace_build_with_docs(self):
from application.templates.namespaces import SourceNamespace
from docsgpt.templates.namespaces import SourceNamespace
builder = SourceNamespace()
docs = [
@@ -227,7 +227,7 @@ class TestSourceNamespace:
assert context["summaries"] == docs_together
def test_source_namespace_build_empty(self):
from application.templates.namespaces import SourceNamespace
from docsgpt.templates.namespaces import SourceNamespace
builder = SourceNamespace()
context = builder.build(docs=None, docs_together=None)
@@ -235,7 +235,7 @@ class TestSourceNamespace:
assert context == {}
def test_source_namespace_build_docs_only(self):
from application.templates.namespaces import SourceNamespace
from docsgpt.templates.namespaces import SourceNamespace
builder = SourceNamespace()
docs = [{"text": "Doc 1"}]
@@ -247,7 +247,7 @@ class TestSourceNamespace:
assert "content" not in context
def test_source_namespace_build_docs_together_only(self):
from application.templates.namespaces import SourceNamespace
from docsgpt.templates.namespaces import SourceNamespace
builder = SourceNamespace()
docs_together = "Content here"
@@ -259,7 +259,7 @@ class TestSourceNamespace:
assert "documents" not in context
def test_source_namespace_name(self):
from application.templates.namespaces import SourceNamespace
from docsgpt.templates.namespaces import SourceNamespace
builder = SourceNamespace()
assert builder.namespace_name == "source"
@@ -269,7 +269,7 @@ class TestSourceNamespace:
class TestToolsNamespace:
def test_tools_namespace_build_with_memory_data(self):
from application.templates.namespaces import ToolsNamespace
from docsgpt.templates.namespaces import ToolsNamespace
builder = ToolsNamespace()
tools_data = {
@@ -282,7 +282,7 @@ class TestToolsNamespace:
assert context["memory"]["available"] is True
def test_tools_namespace_build_empty(self):
from application.templates.namespaces import ToolsNamespace
from docsgpt.templates.namespaces import ToolsNamespace
builder = ToolsNamespace()
context = builder.build(tools_data=None)
@@ -290,7 +290,7 @@ class TestToolsNamespace:
assert context == {}
def test_tools_namespace_build_multiple_tools(self):
from application.templates.namespaces import ToolsNamespace
from docsgpt.templates.namespaces import ToolsNamespace
builder = ToolsNamespace()
tools_data = {
@@ -309,7 +309,7 @@ class TestToolsNamespace:
assert context["api"]["status"] == "success"
def test_tools_namespace_filters_unsafe_values(self):
from application.templates.namespaces import ToolsNamespace
from docsgpt.templates.namespaces import ToolsNamespace
builder = ToolsNamespace()
@@ -324,13 +324,13 @@ class TestToolsNamespace:
assert "unsafe_tool" not in context
def test_tools_namespace_name(self):
from application.templates.namespaces import ToolsNamespace
from docsgpt.templates.namespaces import ToolsNamespace
builder = ToolsNamespace()
assert builder.namespace_name == "tools"
def test_tools_namespace_with_empty_dict(self):
from application.templates.namespaces import ToolsNamespace
from docsgpt.templates.namespaces import ToolsNamespace
builder = ToolsNamespace()
context = builder.build(tools_data={})
@@ -342,7 +342,7 @@ class TestToolsNamespace:
class TestNamespaceManagerWithTools:
def test_namespace_manager_includes_tools_in_context(self):
from application.templates.namespaces import NamespaceManager
from docsgpt.templates.namespaces import NamespaceManager
manager = NamespaceManager()
tools_data = {"memory": {"root": "content", "available": True}}
@@ -353,7 +353,7 @@ class TestNamespaceManagerWithTools:
assert context["tools"]["memory"]["root"] == "content"
def test_namespace_manager_build_context_all_namespaces(self):
from application.templates.namespaces import NamespaceManager
from docsgpt.templates.namespaces import NamespaceManager
manager = NamespaceManager()
context = manager.build_context(
@@ -371,7 +371,7 @@ class TestNamespaceManagerWithTools:
assert context["tools"]["memory"]["root"] == "notes"
def test_namespace_manager_build_context_partial_data(self):
from application.templates.namespaces import NamespaceManager
from docsgpt.templates.namespaces import NamespaceManager
manager = NamespaceManager()
context = manager.build_context(request_id="req_123")
@@ -386,13 +386,13 @@ class TestPromptRendererArtifactParent:
def test_render_resolves_artifact_by_id_with_conversation_parent(self):
from unittest.mock import patch
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
row = {"id": "art-1", "current_version": 1, "kind": "document", "title": "Report"}
version = {"mime_type": "text/html", "filename": "report.html", "size": 12}
with patch(
"application.storage.db.repositories.artifacts.ArtifactsRepository"
) as repo_cls, patch("application.storage.db.session.db_readonly"):
"docsgpt.storage.db.repositories.artifacts.ArtifactsRepository"
) as repo_cls, patch("docsgpt.storage.db.session.db_readonly"):
repo = repo_cls.return_value
repo.get_artifact_in_parent.return_value = row
repo.get_version.return_value = version
@@ -407,7 +407,7 @@ class TestPromptRendererArtifactParent:
assert kwargs.get("conversation_id") == "conv-1"
def test_render_without_parent_yields_empty_lookup(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
# No artifact_parent (e.g. a first-turn conversation_id is None) must not error;
# the lookup returns an empty mapping and the attribute renders blank.
@@ -418,7 +418,7 @@ class TestPromptRendererArtifactParent:
assert rendered == "[]"
def test_namespace_manager_get_builder(self):
from application.templates.namespaces import NamespaceManager, SystemNamespace
from docsgpt.templates.namespaces import NamespaceManager, SystemNamespace
manager = NamespaceManager()
builder = manager.get_builder("system")
@@ -426,7 +426,7 @@ class TestPromptRendererArtifactParent:
assert isinstance(builder, SystemNamespace)
def test_namespace_manager_get_builder_nonexistent(self):
from application.templates.namespaces import NamespaceManager
from docsgpt.templates.namespaces import NamespaceManager
manager = NamespaceManager()
builder = manager.get_builder("nonexistent")
@@ -436,7 +436,7 @@ class TestPromptRendererArtifactParent:
def test_namespace_manager_handles_builder_exceptions(self):
from unittest.mock import patch
from application.templates.namespaces import NamespaceManager
from docsgpt.templates.namespaces import NamespaceManager
manager = NamespaceManager()
@@ -456,7 +456,7 @@ class TestPromptRendererArtifactParent:
class TestPromptRenderer:
def test_render_prompt_with_template_syntax(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "Hello {{ system.user_id }}, today is {{ system.date }}"
@@ -467,7 +467,7 @@ class TestPromptRenderer:
assert "202" in result
def test_render_prompt_with_passthrough_data(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "Company: {{ passthrough.company }}\nUser: {{ passthrough.user_name }}"
@@ -479,7 +479,7 @@ class TestPromptRenderer:
assert "User: John" in result
def test_render_prompt_with_source_docs(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "Use this information:\n{{ source.content }}"
@@ -491,7 +491,7 @@ class TestPromptRenderer:
assert "Important document content" in result
def test_render_prompt_empty_content(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
result = renderer.render_prompt("")
@@ -499,7 +499,7 @@ class TestPromptRenderer:
assert result == ""
def test_render_prompt_legacy_format_with_summaries(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "Context: {summaries}\nQuestion: What is this?"
@@ -510,7 +510,7 @@ class TestPromptRenderer:
assert "Context: This is the document content" in result
def test_render_prompt_legacy_format_without_docs(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "Context: {summaries}\nQuestion: What is this?"
@@ -522,7 +522,7 @@ class TestPromptRenderer:
assert "Question: What is this?" in result
def test_render_prompt_combined_namespace_variables(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "User: {{ passthrough.user }}, Date: {{ system.date }}, Docs: {{ source.content }}"
@@ -540,7 +540,7 @@ class TestPromptRenderer:
assert "Doc content" in result
def test_render_prompt_with_tools_data(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "Memory contents:\n{{ tools.memory.root }}\n\nStatus: {{ tools.memory.available }}"
@@ -557,7 +557,7 @@ class TestPromptRenderer:
assert "Status: True" in result
def test_render_prompt_with_all_namespaces(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = """
@@ -583,7 +583,7 @@ Memory: {{ tools.memory.root }}
assert "Notes content" in result
def test_render_prompt_undefined_variable_returns_empty_string(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "Hello {{ undefined_var }}"
@@ -592,7 +592,7 @@ Memory: {{ tools.memory.root }}
assert result == "Hello "
def test_render_prompt_with_undefined_variable_in_template(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "Hello {{ undefined_name }}"
@@ -601,19 +601,19 @@ Memory: {{ tools.memory.root }}
assert result == "Hello "
def test_validate_template_valid(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
assert renderer.validate_template("Valid {{ variable }}") is True
def test_validate_template_invalid(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
assert renderer.validate_template("Invalid {{ variable") is False
def test_extract_variables(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
template = "{{ var1 }} and {{ var2 }}"
@@ -623,7 +623,7 @@ Memory: {{ tools.memory.root }}
assert isinstance(result, set)
def test_uses_template_syntax_detection(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
@@ -632,7 +632,7 @@ Memory: {{ tools.memory.root }}
assert renderer._uses_template_syntax("Plain text") is False
def test_apply_legacy_substitutions(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "Use {summaries} to answer"
@@ -643,7 +643,7 @@ Memory: {{ tools.memory.root }}
assert "Use Important info to answer" in result
def test_apply_legacy_substitutions_without_docs(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "Use {summaries} to answer"
@@ -658,7 +658,7 @@ Memory: {{ tools.memory.root }}
class TestPromptRendererIntegration:
def test_render_prompt_real_world_scenario(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "You are helping {{ passthrough.company }}.\n\nUser: {{ passthrough.user_name }}\n\nRequest ID: {{ system.request_id }}\n\nDate: {{ system.date }}\n\nReference Documents:\n\n{{ source.content }}\n\nPlease answer the question professionally."
@@ -681,7 +681,7 @@ class TestPromptRendererIntegration:
assert "professionally" in result
def test_render_prompt_multiple_doc_references(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = """Documents: {{ source.content }} \n\nAlso summaries: {{ source.summaries }}"""
@@ -700,7 +700,7 @@ class TestPromptRendererIntegration:
class TestStreamProcessorPromptRendering:
def test_stream_processor_pre_fetch_docs_none_doc_mode(self):
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
request_data = {"question": "Test question", "isNoneDoc": True}
processor = StreamProcessor(request_data, None)
@@ -711,8 +711,8 @@ class TestStreamProcessorPromptRendering:
assert docs_list is None
def test_pre_fetch_tools_disabled_globally(self, monkeypatch):
from application.api.answer.services.stream_processor import StreamProcessor
from application.core.settings import settings
from docsgpt.api.answer.services.stream_processor import StreamProcessor
from docsgpt.core.settings import settings
monkeypatch.setattr(settings, "ENABLE_TOOL_PREFETCH", False)
@@ -724,7 +724,7 @@ class TestStreamProcessorPromptRendering:
assert result is None
def test_pre_fetch_tools_disabled_per_request(self):
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
request_data = {"question": "test", "disable_tool_prefetch": True}
processor = StreamProcessor(request_data, {"sub": "user1"})
@@ -736,8 +736,8 @@ class TestStreamProcessorPromptRendering:
def test_pre_fetch_tools_skips_tool_with_no_actions(self, mock_mongo_db):
from unittest.mock import MagicMock, patch
from application.api.answer.services.stream_processor import StreamProcessor
from application.core.settings import settings
from docsgpt.api.answer.services.stream_processor import StreamProcessor
from docsgpt.core.settings import settings
db = mock_mongo_db[settings.MONGO_DB_NAME]
db["user_tools"].insert_one(
@@ -753,7 +753,7 @@ class TestStreamProcessorPromptRendering:
processor = StreamProcessor(request_data, {"sub": "user1"})
with patch(
"application.agents.tools.tool_manager.ToolManager"
"docsgpt.agents.tools.tool_manager.ToolManager"
) as mock_manager_class:
mock_manager = MagicMock()
mock_manager_class.return_value = mock_manager
@@ -773,8 +773,8 @@ class TestStreamProcessorPromptRendering:
def test_pre_fetch_tools_enabled_by_default(self, mock_mongo_db, monkeypatch):
from unittest.mock import MagicMock, patch
from application.api.answer.services.stream_processor import StreamProcessor
from application.core.settings import settings
from docsgpt.api.answer.services.stream_processor import StreamProcessor
from docsgpt.core.settings import settings
db = mock_mongo_db[settings.MONGO_DB_NAME]
db["user_tools"].insert_one(
@@ -790,7 +790,7 @@ class TestStreamProcessorPromptRendering:
processor = StreamProcessor(request_data, {"sub": "user1"})
with patch(
"application.agents.tools.tool_manager.ToolManager"
"docsgpt.agents.tools.tool_manager.ToolManager"
) as mock_manager_class:
mock_manager = MagicMock()
mock_manager_class.return_value = mock_manager
@@ -812,7 +812,7 @@ class TestStreamProcessorPromptRendering:
assert "memory_ls" in result["memory"]
def test_pre_fetch_tools_no_tools_configured(self):
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
request_data = {"question": "test"}
processor = StreamProcessor(request_data, {"sub": "user1"})
@@ -824,8 +824,8 @@ class TestStreamProcessorPromptRendering:
def test_pre_fetch_tools_memory_returns_error(self, mock_mongo_db):
from unittest.mock import MagicMock, patch
from application.api.answer.services.stream_processor import StreamProcessor
from application.core.settings import settings
from docsgpt.api.answer.services.stream_processor import StreamProcessor
from docsgpt.core.settings import settings
db = mock_mongo_db[settings.MONGO_DB_NAME]
db["user_tools"].insert_one(
@@ -841,7 +841,7 @@ class TestStreamProcessorPromptRendering:
processor = StreamProcessor(request_data, {"sub": "user1"})
with patch(
"application.agents.tools.tool_manager.ToolManager"
"docsgpt.agents.tools.tool_manager.ToolManager"
) as mock_manager_class:
mock_manager = MagicMock()
mock_manager_class.return_value = mock_manager
@@ -864,8 +864,8 @@ class TestStreamProcessorPromptRendering:
def test_pre_fetch_tools_memory_returns_empty(self, mock_mongo_db):
from unittest.mock import MagicMock, patch
from application.api.answer.services.stream_processor import StreamProcessor
from application.core.settings import settings
from docsgpt.api.answer.services.stream_processor import StreamProcessor
from docsgpt.core.settings import settings
db = mock_mongo_db[settings.MONGO_DB_NAME]
db["user_tools"].insert_one(
@@ -881,7 +881,7 @@ class TestStreamProcessorPromptRendering:
processor = StreamProcessor(request_data, {"sub": "user1"})
with patch(
"application.agents.tools.tool_manager.ToolManager"
"docsgpt.agents.tools.tool_manager.ToolManager"
) as mock_manager_class:
mock_manager = MagicMock()
mock_manager_class.return_value = mock_manager
@@ -1,4 +1,4 @@
"""Tests for application/api/answer/services/stream_processor.py.
"""Tests for docsgpt/api/answer/services/stream_processor.py.
The previous suite was tightly coupled to Mongo (mock_mongo_db fixture,
bson.ObjectId, bson.DBRef, find_one, etc.) which no longer exist after the
@@ -24,7 +24,7 @@ class TestStreamProcessorInitialization:
pass
def test_initializes_with_decoded_token(self):
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
conv_id = _STATIC_OID
request_data = {
@@ -41,7 +41,7 @@ class TestStreamProcessorInitialization:
assert processor.conversation_id == request_data["conversation_id"]
def test_initializes_without_token(self):
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
request_data = {"question": "Test question"}
@@ -52,7 +52,7 @@ class TestStreamProcessorInitialization:
assert processor.data == request_data
def test_initializes_default_attributes(self):
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
processor = StreamProcessor({"question": "Test"}, {"sub": "user_123"})
@@ -66,7 +66,7 @@ class TestStreamProcessorInitialization:
assert processor.shared_token is None
def test_extracts_conversation_id_from_request(self):
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
conv_id = _STATIC_OID
request_data = {"question": "Test", "conversation_id": conv_id}
@@ -81,7 +81,7 @@ class TestStreamProcessorHistoryLoading:
pass
def test_uses_request_history_when_no_conversation_id(self):
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
request_data = {
"question": "What is Python?",
@@ -98,7 +98,7 @@ class TestStreamProcessorAgentConfiguration:
pass
def test_uses_default_config_without_api_key(self):
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
request_data = {"question": "Test"}
@@ -110,7 +110,7 @@ class TestStreamProcessorAgentConfiguration:
def test_embedded_workflow_without_saved_id(self):
"""A preview run with no saved workflow id carries no ``workflow_id``."""
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
request_data = {
"question": "Test",
@@ -126,7 +126,7 @@ class TestStreamProcessorAgentConfiguration:
def test_embedded_workflow_with_saved_id_persists_run(self):
"""A saved workflow id alongside the embedded graph is captured so the
run can persist a ``workflow_runs`` row for artifact listing."""
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
request_data = {
"question": "Test",
@@ -152,7 +152,7 @@ class TestStreamProcessorDocPrefetch:
def test_prefetch_skipped_when_no_active_docs(self):
from unittest.mock import MagicMock
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
processor = StreamProcessor(
{"question": "Hi there"},
@@ -170,7 +170,7 @@ class TestStreamProcessorDocPrefetch:
def test_prefetch_skipped_when_active_docs_is_default(self):
from unittest.mock import MagicMock
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
processor = StreamProcessor(
{"question": "Hi", "active_docs": "default"},
@@ -194,7 +194,7 @@ class TestStreamProcessorAttachments:
pass
def test_handles_empty_attachments(self):
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
request_data = {"question": "Simple question"}
@@ -225,7 +225,7 @@ class TestBuildContinuationFromMessages:
def _make_processor():
from unittest.mock import MagicMock
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
processor = StreamProcessor({"question": ""}, {"sub": "user_123"})
fake_agent = MagicMock()
@@ -1,4 +1,4 @@
"""Targeted tests for application/api/answer/services/stream_processor.py.
"""Targeted tests for docsgpt/api/answer/services/stream_processor.py.
Tests the ``get_prompt`` helper and simpler StreamProcessor methods against
real ephemeral Postgres.
@@ -9,7 +9,7 @@ from unittest.mock import patch
import pytest
import application.api.answer.services.stream_processor as sp_mod
import docsgpt.api.answer.services.stream_processor as sp_mod
@contextmanager
@@ -19,46 +19,46 @@ def _patch_db(conn):
yield conn
with patch(
"application.api.answer.services.stream_processor.db_readonly", _yield
"docsgpt.api.answer.services.stream_processor.db_readonly", _yield
), patch(
"application.api.answer.services.stream_processor.db_session", _yield
"docsgpt.api.answer.services.stream_processor.db_session", _yield
):
yield
class TestGetPrompt:
def test_default_preset(self):
from application.api.answer.services.stream_processor import get_prompt
from docsgpt.api.answer.services.stream_processor import get_prompt
got = get_prompt("default")
assert isinstance(got, str)
assert len(got) > 0
def test_creative_preset(self):
from application.api.answer.services.stream_processor import get_prompt
from docsgpt.api.answer.services.stream_processor import get_prompt
got = get_prompt("creative")
assert isinstance(got, str) and len(got) > 0
def test_strict_preset(self):
from application.api.answer.services.stream_processor import get_prompt
from docsgpt.api.answer.services.stream_processor import get_prompt
got = get_prompt("strict")
assert isinstance(got, str) and len(got) > 0
def test_agentic_default_preset(self):
from application.api.answer.services.stream_processor import get_prompt
from docsgpt.api.answer.services.stream_processor import get_prompt
got = get_prompt("agentic_default")
assert isinstance(got, str) and len(got) > 0
def test_none_defaults_to_default(self):
from application.api.answer.services.stream_processor import get_prompt
from docsgpt.api.answer.services.stream_processor import get_prompt
got = get_prompt(None)
assert isinstance(got, str) and len(got) > 0
def test_empty_string_defaults_to_default(self):
from application.api.answer.services.stream_processor import get_prompt
from docsgpt.api.answer.services.stream_processor import get_prompt
assert get_prompt("") == get_prompt("default")
def test_non_string_id_converted(self):
from application.api.answer.services.stream_processor import get_prompt
from docsgpt.api.answer.services.stream_processor import get_prompt
# A UUID object would be stringified; use an int to test the branch
with pytest.raises(ValueError):
# Int converts to str "42" which isn't a preset, and will
@@ -66,18 +66,18 @@ class TestGetPrompt:
get_prompt(42)
def test_unknown_prompt_id_raises(self, pg_conn):
from application.api.answer.services.stream_processor import get_prompt
from docsgpt.api.answer.services.stream_processor import get_prompt
with _patch_db(pg_conn), pytest.raises(ValueError):
get_prompt("00000000-0000-0000-0000-000000000000")
def test_legacy_id_unknown_raises(self, pg_conn):
from application.api.answer.services.stream_processor import get_prompt
from docsgpt.api.answer.services.stream_processor import get_prompt
with _patch_db(pg_conn), pytest.raises(ValueError):
get_prompt("507f1f77bcf86cd799439011")
def test_uuid_lookup_returns_content(self, pg_conn):
from application.api.answer.services.stream_processor import get_prompt
from application.storage.db.repositories.prompts import (
from docsgpt.api.answer.services.stream_processor import get_prompt
from docsgpt.storage.db.repositories.prompts import (
PromptsRepository,
)
@@ -91,7 +91,7 @@ class TestGetPrompt:
class TestStreamProcessorInit:
def test_basic_init(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
data = {"question": "hi", "conversation_id": "conv-1"}
@@ -103,7 +103,7 @@ class TestStreamProcessorInit:
assert sp.conversation_id == "conv-1"
def test_init_no_token(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"question": "hi"}, None)
@@ -112,7 +112,7 @@ class TestStreamProcessorInit:
assert sp.conversation_id is None
def test_init_sets_agent_id_from_data(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
data = {"question": "hi", "agent_id": "agent-xyz"}
@@ -122,7 +122,7 @@ class TestStreamProcessorInit:
class TestLoadConversationHistory:
def test_no_conversation_id_uses_request_history(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
import json as _json
@@ -136,10 +136,10 @@ class TestLoadConversationHistory:
assert len(sp.history) == 1
def test_loads_existing_conversation_history(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.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -162,7 +162,7 @@ class TestLoadConversationHistory:
)
# Also patch conversation_service.get_conversation's DB accessor
with _patch_db(pg_conn), patch(
"application.api.answer.services.conversation_service.db_readonly",
"docsgpt.api.answer.services.conversation_service.db_readonly",
) as mock_readonly:
@contextmanager
def _yield():
@@ -172,10 +172,10 @@ class TestLoadConversationHistory:
assert len(sp.history) == 2
def test_unauthorized_conversation_raises(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.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -186,7 +186,7 @@ class TestLoadConversationHistory:
{"sub": "hacker"},
)
with _patch_db(pg_conn), patch(
"application.api.answer.services.conversation_service.db_readonly"
"docsgpt.api.answer.services.conversation_service.db_readonly"
) as mock_readonly:
@contextmanager
def _yield():
@@ -198,7 +198,7 @@ class TestLoadConversationHistory:
class TestHasActiveDocs:
def test_false_when_no_active_docs(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"question": "q"}, {"sub": "u"})
@@ -207,7 +207,7 @@ class TestHasActiveDocs:
assert sp._has_active_docs() is False
def test_true_when_source_active(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"question": "q"}, {"sub": "u"})
@@ -215,7 +215,7 @@ class TestHasActiveDocs:
assert sp._has_active_docs() is True
def test_false_when_source_empty(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"question": "q"}, {"sub": "u"})
@@ -223,7 +223,7 @@ class TestHasActiveDocs:
assert sp._has_active_docs() is False
def test_default_returns_false(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"question": "q"}, {"sub": "u"})
@@ -234,7 +234,7 @@ class TestHasActiveDocs:
class TestProcessAttachments:
def test_no_attachments(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"question": "q"}, {"sub": "u"})
@@ -244,10 +244,10 @@ class TestProcessAttachments:
assert sp.attachments == []
def test_retrieves_attachments_by_id(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.attachments import (
from docsgpt.storage.db.repositories.attachments import (
AttachmentsRepository,
)
@@ -269,7 +269,7 @@ class TestProcessAttachments:
class TestGetAttachmentsContent:
def test_empty_list(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"question": "q"}, {"sub": "u"})
@@ -278,7 +278,7 @@ class TestGetAttachmentsContent:
assert got == []
def test_skips_missing_attachments(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"question": "q"}, {"sub": "u"})
@@ -291,14 +291,14 @@ class TestGetAttachmentsContent:
class TestResolveAgentId:
def test_returns_agent_id_from_request(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"agent_id": "req-agent"}, {"sub": "u"})
assert sp._resolve_agent_id() == "req-agent"
def test_returns_none_if_not_set(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -307,7 +307,7 @@ class TestResolveAgentId:
class TestGetAgentKey:
def test_returns_tuple_for_none_agent_id(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -315,7 +315,7 @@ class TestGetAgentKey:
assert key is None and is_shared is False and tok is None
def test_raises_for_missing_agent(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -325,10 +325,10 @@ class TestGetAgentKey:
)
def test_returns_key_for_owned_agent(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.agents import AgentsRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
agent = AgentsRepository(pg_conn).create(
"owner", "a", "published", key="the-key",
@@ -340,10 +340,10 @@ class TestGetAgentKey:
assert shared is False
def test_raises_on_unauthorized_access(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.agents import AgentsRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
agent = AgentsRepository(pg_conn).create(
"owner", "a", "published", key="k", shared=False,
@@ -372,7 +372,7 @@ def _stub_db_readonly(monkeypatch, sp_mod):
class TestConfigureSource:
def test_agent_data_with_sources_list(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -386,7 +386,7 @@ class TestConfigureSource:
assert sp.source == {"active_docs": ["s1"]}
def test_agent_data_with_single_source(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -395,7 +395,7 @@ class TestConfigureSource:
assert sp.source == {"active_docs": "src-1"}
def test_agent_data_default_source(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -449,7 +449,7 @@ class TestConfigureSource:
assert sp.source == {}
def test_request_active_docs_default(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"active_docs": "default"}, {"sub": "u"})
@@ -457,7 +457,7 @@ class TestConfigureSource:
assert sp.source == {}
def test_no_data_empty_source(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -467,7 +467,7 @@ class TestConfigureSource:
class TestConfigureRetriever:
def test_defaults(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -476,7 +476,7 @@ class TestConfigureRetriever:
assert sp.retriever_config["chunks"] == 2
def test_agent_overrides(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -486,7 +486,7 @@ class TestConfigureRetriever:
assert sp.retriever_config["chunks"] == 5
def test_agent_wins_over_request_on_agent_bound(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor(
@@ -498,7 +498,7 @@ class TestConfigureRetriever:
assert sp.retriever_config["chunks"] == 5
def test_body_wins_on_agentless(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor(
@@ -510,7 +510,7 @@ class TestConfigureRetriever:
def test_agent_bound_drops_body_chunks_and_retriever(self):
# Missing agent values fall back to system defaults, not body's.
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor(
@@ -522,7 +522,7 @@ class TestConfigureRetriever:
assert sp.retriever_config["chunks"] == 2
def test_invalid_agent_chunks_falls_back(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -531,7 +531,7 @@ class TestConfigureRetriever:
assert sp.retriever_config["chunks"] == 2
def test_invalid_request_chunks_falls_back(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"chunks": "abc"}, {"sub": "u"})
@@ -539,7 +539,7 @@ class TestConfigureRetriever:
assert sp.retriever_config["chunks"] == 2
def test_isnonedoc_without_api_key_sets_chunks_to_0(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"isNoneDoc": True}, {"sub": "u"})
@@ -550,10 +550,10 @@ class TestConfigureRetriever:
class TestGetPromptContent:
def test_gets_from_agent_config(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.prompts import (
from docsgpt.storage.db.repositories.prompts import (
PromptsRepository,
)
@@ -573,10 +573,10 @@ class TestGetPromptContent:
assert "{{ system.persona }}" in content
def test_templated_custom_prompt_is_left_alone(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.prompts import (
from docsgpt.storage.db.repositories.prompts import (
PromptsRepository,
)
@@ -592,7 +592,7 @@ class TestGetPromptContent:
assert sp._persona is None
def test_returns_none_on_missing(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
@@ -605,7 +605,7 @@ class TestGetPromptContent:
assert content is None
def test_caches_prompt_content(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -614,7 +614,7 @@ class TestGetPromptContent:
assert sp._get_prompt_content() == "cached"
def test_agentic_agent_gets_agentic_preset(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -624,7 +624,7 @@ class TestGetPromptContent:
assert "source.summaries" not in content
def test_research_agent_gets_agentic_preset(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -633,7 +633,7 @@ class TestGetPromptContent:
assert "`search` tool" in content
def test_classic_agent_gets_classic_preset(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -644,7 +644,7 @@ class TestGetPromptContent:
assert "documents are provided with the question" in content
def test_null_prompt_id_agentic_agent_gets_agentic_preset(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
# PG ``agents.prompt_id`` is NULL for agents that never chose a
@@ -657,7 +657,7 @@ class TestGetPromptContent:
assert "source.summaries" not in content
def test_null_prompt_id_classic_agent_gets_default_preset(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -671,7 +671,7 @@ class TestGetPromptContent:
class TestPreFetchDocs:
def test_skips_when_no_active_docs(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -680,7 +680,7 @@ class TestPreFetchDocs:
assert docs is None and raw is None
def test_skips_when_isnonedoc_no_agent(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"isNoneDoc": True}, {"sub": "u"})
@@ -690,13 +690,13 @@ class TestPreFetchDocs:
assert docs is None and raw is None
def test_handles_retriever_exception(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
sp.source = {"active_docs": "src"}
with patch(
"application.api.answer.services.stream_processor.StreamProcessor.create_retriever",
"docsgpt.api.answer.services.stream_processor.StreamProcessor.create_retriever",
side_effect=RuntimeError("boom"),
):
docs, raw = sp.pre_fetch_docs("q")
@@ -705,48 +705,48 @@ class TestPreFetchDocs:
class TestPreFetchTools:
def test_disabled_globally_returns_none(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
with patch(
"application.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
"docsgpt.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
False,
):
got = sp.pre_fetch_tools()
assert got is None
def test_disabled_per_request(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"disable_tool_prefetch": True}, {"sub": "u"})
with patch(
"application.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
"docsgpt.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
True,
):
got = sp.pre_fetch_tools()
assert got is None
def test_no_template_skips_default_tool_prefetch(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "no-tools-user"})
sp._prompt_content = "No template syntax here"
with _patch_db(pg_conn), patch(
"application.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
"docsgpt.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
True,
):
got = sp.pre_fetch_tools()
assert got is None
def test_unresolvable_prompt_prefetches_only_explicit_rows(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_tools import (
from docsgpt.storage.db.repositories.user_tools import (
UserToolsRepository,
)
@@ -766,7 +766,7 @@ class TestPreFetchTools:
return {"ok": True}
with _patch_db(pg_conn), patch(
"application.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
"docsgpt.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
True,
), patch.object(sp, "_fetch_tool_data", _fake_fetch):
got = sp.pre_fetch_tools()
@@ -778,8 +778,8 @@ class TestPreFetchTools:
def test_default_tool_prefetched_when_template_references_it(
self, pg_conn
):
from application.agents.default_tools import default_tool_id
from application.api.answer.services.stream_processor import (
from docsgpt.agents.default_tools import default_tool_id
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
@@ -792,7 +792,7 @@ class TestPreFetchTools:
return {"ok": True}
with _patch_db(pg_conn), patch(
"application.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
"docsgpt.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
True,
), patch.object(sp, "_fetch_tool_data", _fake_fetch):
got = sp.pre_fetch_tools()
@@ -807,11 +807,11 @@ class TestPreFetchTools:
assert got.get("read_webpage") == {"ok": True}
def test_explicit_row_keeps_name_key_over_default(self, pg_conn):
from application.agents.default_tools import default_tool_id
from application.api.answer.services.stream_processor import (
from docsgpt.agents.default_tools import default_tool_id
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
from application.storage.db.repositories.user_tools import (
from docsgpt.storage.db.repositories.user_tools import (
UserToolsRepository,
)
@@ -825,7 +825,7 @@ class TestPreFetchTools:
return {"is_default": bool(tool_doc.get("default"))}
with _patch_db(pg_conn), patch(
"application.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
"docsgpt.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
True,
), patch.object(sp, "_fetch_tool_data", _fake_fetch):
got = sp.pre_fetch_tools()
@@ -838,8 +838,8 @@ class TestPreFetchTools:
def test_fetch_tool_data_executes_referenced_memory_view(self, pg_conn):
from unittest.mock import MagicMock
from application.agents.default_tools import synthesize_default_tool
from application.api.answer.services.stream_processor import (
from docsgpt.agents.default_tools import synthesize_default_tool
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
@@ -853,7 +853,7 @@ class TestPreFetchTools:
mock_manager.load_tool.return_value = mock_tool
with patch(
"application.agents.tools.tool_manager.ToolManager",
"docsgpt.agents.tools.tool_manager.ToolManager",
return_value=mock_manager,
):
got = sp._fetch_tool_data(tool_doc, {"memory_view"})
@@ -864,14 +864,14 @@ class TestPreFetchTools:
mock_tool.execute_action.assert_called_once_with("memory_view")
def test_agent_bound_invocation_omits_default_tool_prefetch(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"agent_id": "agent-xyz"}, {"sub": "u-ag"})
sp._required_tool_actions = {"read_webpage": {None}}
with _patch_db(pg_conn), patch(
"application.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
"docsgpt.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
True,
):
got = sp.pre_fetch_tools()
@@ -881,11 +881,11 @@ class TestPreFetchTools:
"""An explicit row and the synthesized default of the same name
coexist: name key stays on the explicit, default reachable by
synthetic id only."""
from application.agents.default_tools import default_tool_id
from application.api.answer.services.stream_processor import (
from docsgpt.agents.default_tools import default_tool_id
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
from application.storage.db.repositories.user_tools import (
from docsgpt.storage.db.repositories.user_tools import (
UserToolsRepository,
)
@@ -906,7 +906,7 @@ class TestPreFetchTools:
}
with _patch_db(pg_conn), patch(
"application.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
"docsgpt.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
True,
), patch.object(sp, "_fetch_tool_data", _fake_fetch):
got = sp.pre_fetch_tools()
@@ -921,7 +921,7 @@ class TestValidateAndSetModelAgentAuthority:
"""Agent-bound chats: agent's ``default_model_id`` is authoritative."""
def test_agent_bound_ignores_body_model_id(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"model_id": "body-model"}, {"sub": "caller"})
@@ -937,10 +937,10 @@ class TestValidateAndSetModelAgentAuthority:
return True
with patch(
"application.api.answer.services.stream_processor.validate_model_id",
"docsgpt.api.answer.services.stream_processor.validate_model_id",
side_effect=_fake_validate,
), patch(
"application.api.answer.services.stream_processor.get_default_model_id",
"docsgpt.api.answer.services.stream_processor.get_default_model_id",
return_value="global-default",
):
sp._validate_and_set_model()
@@ -950,17 +950,17 @@ class TestValidateAndSetModelAgentAuthority:
assert ("agent-model", "owner") in captured
def test_agent_bound_no_default_falls_back_to_system(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"model_id": "body-model"}, {"sub": "u"})
sp._agent_data = {"user": "u"}
sp.agent_config = {"default_model_id": "", "user_id": "u"}
with patch(
"application.api.answer.services.stream_processor.validate_model_id",
"docsgpt.api.answer.services.stream_processor.validate_model_id",
return_value=False,
), patch(
"application.api.answer.services.stream_processor.get_default_model_id",
"docsgpt.api.answer.services.stream_processor.get_default_model_id",
return_value="global-default",
):
sp._validate_and_set_model()
@@ -968,13 +968,13 @@ class TestValidateAndSetModelAgentAuthority:
assert sp.model_user_id is None
def test_agentless_body_model_still_wins(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"model_id": "body-model"}, {"sub": "u"})
sp._agent_data = None
with patch(
"application.api.answer.services.stream_processor.validate_model_id",
"docsgpt.api.answer.services.stream_processor.validate_model_id",
return_value=True,
):
sp._validate_and_set_model()
@@ -986,14 +986,14 @@ class TestGetDataFromApiKeySourceUnion:
"""`_get_data_from_api_key`: primary ∪ extras, deduplicated, primary first."""
def _make_sp(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
return StreamProcessor({}, {"sub": "u"})
def test_union_primary_and_extras(self, pg_conn):
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.sources import SourcesRepository
owner = "u-merge-both"
sources_repo = SourcesRepository(pg_conn)
@@ -1023,8 +1023,8 @@ class TestGetDataFromApiKeySourceUnion:
assert data["source"] == str(primary["id"])
def test_only_primary(self, pg_conn):
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.sources import SourcesRepository
owner = "u-merge-primary-only"
primary = SourcesRepository(pg_conn).create(
@@ -1045,8 +1045,8 @@ class TestGetDataFromApiKeySourceUnion:
assert data["source"] == str(primary["id"])
def test_only_extras(self, pg_conn):
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.sources import SourcesRepository
owner = "u-merge-extras-only"
e1 = SourcesRepository(pg_conn).create(name="e1", user_id=owner)
@@ -1067,8 +1067,8 @@ class TestGetDataFromApiKeySourceUnion:
assert data["source"] is None
def test_dedupe_primary_repeated_in_extras(self, pg_conn):
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.sources import SourcesRepository
owner = "u-merge-dedupe"
primary = SourcesRepository(pg_conn).create(
@@ -1096,11 +1096,11 @@ class TestAgentBoundFieldsAuthoritative:
"""End-to-end regression: agent's source/model/chunks/retriever win."""
def test_agent_values_win_over_body(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.agents import AgentsRepository
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.sources import SourcesRepository
owner = "u-regr-agent-authority"
primary = SourcesRepository(pg_conn).create(
@@ -1130,10 +1130,10 @@ class TestAgentBoundFieldsAuthoritative:
sp = StreamProcessor(body, {"sub": owner})
with _patch_db(pg_conn), patch(
"application.api.answer.services.stream_processor.validate_model_id",
"docsgpt.api.answer.services.stream_processor.validate_model_id",
return_value=True,
), patch(
"application.api.answer.services.stream_processor.get_default_model_id",
"docsgpt.api.answer.services.stream_processor.get_default_model_id",
return_value="system-default",
):
sp._configure_agent()
@@ -1,6 +1,6 @@
"""Token-usage attribution tests for the always-on inline-persist model.
Persistence is owned by the per-call decorator in ``application.usage``.
Persistence is owned by the per-call decorator in ``docsgpt.usage``.
``finalize_message`` no longer writes ``token_usage`` rows. These tests
exercise the decorator path through ``stream_token_usage`` /
``gen_token_usage``:
@@ -63,7 +63,7 @@ class TestDecoratorAlwaysPersists:
"""Per-call inline persistence — no opt-in flag."""
def test_primary_stream_writes_agent_stream_row(self, pg_conn):
from application.usage import stream_token_usage
from docsgpt.usage import stream_token_usage
user_id = _seed_user(pg_conn)
@@ -81,7 +81,7 @@ class TestDecoratorAlwaysPersists:
yield "chunk-b"
llm = _PrimaryLLM()
with _patch_db_session_for(("application.usage",), pg_conn):
with _patch_db_session_for(("docsgpt.usage",), pg_conn):
for _ in llm._raw(
"m", [{"role": "user", "content": "hi"}], True, None,
):
@@ -102,7 +102,7 @@ class TestDecoratorAlwaysPersists:
def test_side_channel_source_flows_to_row(self, pg_conn):
"""``_token_usage_source`` overrides the default ``agent_stream``."""
from application.usage import stream_token_usage
from docsgpt.usage import stream_token_usage
user_id = _seed_user(pg_conn)
@@ -120,7 +120,7 @@ class TestDecoratorAlwaysPersists:
yield "chunk"
llm = _RagLLM()
with _patch_db_session_for(("application.usage",), pg_conn):
with _patch_db_session_for(("docsgpt.usage",), pg_conn):
for _ in llm._raw("m", [{"role": "user", "content": "q"}], True, None):
pass
@@ -137,7 +137,7 @@ class TestDecoratorAlwaysPersists:
"""``_request_id`` on the LLM (stamped by the route) lands in
``token_usage.request_id`` so ``count_in_range`` can DISTINCT it.
"""
from application.usage import stream_token_usage
from docsgpt.usage import stream_token_usage
user_id = _seed_user(pg_conn)
request_id = f"req-{uuid.uuid4().hex[:12]}"
@@ -156,7 +156,7 @@ class TestDecoratorAlwaysPersists:
yield "chunk"
llm = _PrimaryLLM()
with _patch_db_session_for(("application.usage",), pg_conn):
with _patch_db_session_for(("docsgpt.usage",), pg_conn):
# Call twice — the route invokes the LLM once per tool round.
for _ in llm._raw("m", [{"role": "user", "content": "q"}], True, None):
pass
@@ -173,7 +173,7 @@ class TestDecoratorAlwaysPersists:
assert all(r[0] == request_id for r in rows)
def test_zero_count_call_is_skipped(self, pg_conn):
from application.usage import gen_token_usage
from docsgpt.usage import gen_token_usage
user_id = _seed_user(pg_conn)
@@ -190,7 +190,7 @@ class TestDecoratorAlwaysPersists:
return None # empty result → 0 generated tokens, 0 prompt tokens
llm = _EmptyLLM()
with _patch_db_session_for(("application.usage",), pg_conn):
with _patch_db_session_for(("docsgpt.usage",), pg_conn):
llm._raw("m", [], False, None)
n = pg_conn.execute(
@@ -206,7 +206,7 @@ class TestDecoratorAlwaysPersists:
constraint; the decorator skips before that to keep the stream
running.
"""
from application.usage import stream_token_usage
from docsgpt.usage import stream_token_usage
class _OrphanLLM:
decoded_token = None
@@ -222,8 +222,8 @@ class TestDecoratorAlwaysPersists:
llm = _OrphanLLM()
with _patch_db_session_for(
("application.usage",), pg_conn,
), caplog.at_level(logging.WARNING, logger="application.usage"):
("docsgpt.usage",), pg_conn,
), caplog.at_level(logging.WARNING, logger="docsgpt.usage"):
for _ in llm._raw("m", [{"role": "user", "content": "q"}], True, None):
pass
+59 -59
View File
@@ -1,4 +1,4 @@
"""Unit tests for application/api/answer/routes/base.py — BaseAnswerResource.
"""Unit tests for docsgpt/api/answer/routes/base.py — BaseAnswerResource.
Additional coverage beyond tests/api/answer/routes/test_base.py:
- _prepare_tool_calls_for_logging: truncation, non-dict items
@@ -22,21 +22,21 @@ class TestPrepareToolCallsForLogging:
pass
def test_empty_list(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
assert resource._prepare_tool_calls_for_logging([]) == []
def test_none_returns_empty(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
assert resource._prepare_tool_calls_for_logging(None) == []
def test_truncates_long_result(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -45,7 +45,7 @@ class TestPrepareToolCallsForLogging:
assert len(prepared[0]["result"]) == 100
def test_truncates_result_full(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -54,7 +54,7 @@ class TestPrepareToolCallsForLogging:
assert len(prepared[0]["result_full"]) == 50
def test_non_dict_items_wrapped(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -64,7 +64,7 @@ class TestPrepareToolCallsForLogging:
assert prepared[1] == {"result": "42"}
def test_preserves_short_results(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -79,7 +79,7 @@ class TestCompleteStreamToolCalls:
pass
def test_streams_tool_calls(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -105,7 +105,7 @@ class TestCompleteStreamToolCalls:
assert len(tool_chunks) == 1
def test_streams_thought_events(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -137,7 +137,7 @@ class TestCompleteStreamStructuredOutput:
pass
def test_streams_structured_answer(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -173,7 +173,7 @@ class TestCompleteStreamMetadata:
pass
def test_metadata_collected(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -205,7 +205,7 @@ class TestCompleteStreamIsNoneDoc:
pass
def test_isNoneDoc_sets_source_to_none(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -238,7 +238,7 @@ class TestCompleteStreamErrorType:
pass
def test_error_type_event_sanitized(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -263,7 +263,7 @@ class TestCompleteStreamErrorType:
assert len(error_chunks) == 1
def test_non_error_type_event_passed_through(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -293,7 +293,7 @@ class TestProcessResponseStreamExtended:
pass
def test_handles_structured_answer(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -307,7 +307,7 @@ class TestProcessResponseStreamExtended:
assert result.get("extra", {}).get("structured") is True
def test_handles_tool_calls_event(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -321,7 +321,7 @@ class TestProcessResponseStreamExtended:
assert result["tool_calls"] == [{"name": "t1"}]
def test_incomplete_stream(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -332,7 +332,7 @@ class TestProcessResponseStreamExtended:
assert result["error"] == "Stream ended unexpectedly"
def test_handles_thought_event(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -350,7 +350,7 @@ class TestProcessResponseStreamExtended:
informational ``message_id`` event) without breaking JSON
decoding.
"""
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -372,7 +372,7 @@ class TestProcessResponseStreamExtended:
assert result["conversation_id"] == "conv-1"
def test_skips_keepalive_comment_lines(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -397,7 +397,7 @@ class TestCompleteStreamCompressionMetadata:
"""Cover lines 307-319 (compression metadata persistence in complete_stream)."""
def test_compression_metadata_persisted(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -442,7 +442,7 @@ class TestCompleteStreamCompressionMetadata:
def test_compression_metadata_error_handled(self, mock_mongo_db, flask_app):
"""Cover lines 318-322: compression metadata persistence error."""
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -485,7 +485,7 @@ class TestCompleteStreamLogTruncation:
"""Cover line 354: log data truncation for long values."""
def test_long_response_truncated_in_log(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -514,7 +514,7 @@ class TestCompleteStreamGeneratorExit:
"""Cover lines 360-416 (GeneratorExit handling in complete_stream)."""
def test_generator_exit_saves_partial_response(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -558,7 +558,7 @@ class TestCompleteStreamGeneratorExit:
def test_generator_exit_with_compression_metadata(self, mock_mongo_db, flask_app):
"""Cover lines 393-411: GeneratorExit with compression metadata."""
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -598,7 +598,7 @@ class TestCompleteStreamGeneratorExit:
def test_generator_exit_save_error_handled(self, mock_mongo_db, flask_app):
"""Cover lines 412-415: exception during partial save."""
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -647,7 +647,7 @@ class TestCompleteStreamGeneratorExit:
``end`` for a row whose DB status is still non-terminal and the
UI parks on a blank successful answer.
"""
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -679,7 +679,7 @@ class TestCompleteStreamGeneratorExit:
return True
with patch(
"application.api.answer.routes.base.record_event",
"docsgpt.api.answer.routes.base.record_event",
side_effect=_capture_record,
):
gen = resource.complete_stream(
@@ -727,8 +727,8 @@ class TestCompleteStreamGeneratorExit:
``end`` (the row matches ``complete``). Only the empty-response
branch flips to ``error``.
"""
from application.api.answer.routes.base import BaseAnswerResource
from application.storage.db.repositories.conversations import (
from docsgpt.api.answer.routes.base import BaseAnswerResource
from docsgpt.storage.db.repositories.conversations import (
MessageUpdateOutcome,
)
@@ -761,7 +761,7 @@ class TestCompleteStreamGeneratorExit:
return True
with patch(
"application.api.answer.routes.base.record_event",
"docsgpt.api.answer.routes.base.record_event",
side_effect=_capture_record,
):
gen = resource.complete_stream(
@@ -804,8 +804,8 @@ class TestCompleteStreamGeneratorExit:
as a failure and journaled ``error`` — a reconnect would then
replay a successful completion as a failed answer.
"""
from application.api.answer.routes.base import BaseAnswerResource
from application.storage.db.repositories.conversations import (
from docsgpt.api.answer.routes.base import BaseAnswerResource
from docsgpt.storage.db.repositories.conversations import (
MessageUpdateOutcome,
)
@@ -841,7 +841,7 @@ class TestCompleteStreamGeneratorExit:
return True
with patch(
"application.api.answer.routes.base.record_event",
"docsgpt.api.answer.routes.base.record_event",
side_effect=_capture_record,
):
gen = resource.complete_stream(
@@ -890,24 +890,24 @@ def _patch_db_session(conn):
yield conn
with patch(
"application.api.answer.services.conversation_service.db_session",
"docsgpt.api.answer.services.conversation_service.db_session",
_yield,
), patch(
"application.api.answer.services.conversation_service.db_readonly",
"docsgpt.api.answer.services.conversation_service.db_readonly",
_yield,
), patch(
# ``record_event`` opens its own short-lived ``db_session`` for
# cross-connection visibility. In tests we route it back to the
# same ``pg_conn`` so the journal write can see the message row
# the conversation_service just wrote in this transaction.
"application.streaming.message_journal.db_session",
"docsgpt.streaming.message_journal.db_session",
_yield,
), patch(
# ``complete_stream`` reads ``latest_sequence_no`` via
# ``db_readonly`` to seed continuation runs. Same patch reason
# as the journal — keep the read on the same pg_conn so it sees
# uncommitted writes from this transaction.
"application.api.answer.routes.base.db_readonly",
"docsgpt.api.answer.routes.base.db_readonly",
_yield,
), patch(
# The terminal ``stream_answer`` user_logs write opens its own
@@ -916,7 +916,7 @@ def _patch_db_session(conn):
# inserted (via the ``ensure_user_exists`` trigger) until the
# statement timeout fires — ~30s per test, swallowed by the
# caller's except, so it only ever showed up as slowness.
"application.api.answer.routes.base.db_session",
"docsgpt.api.answer.routes.base.db_session",
_yield,
):
yield
@@ -941,8 +941,8 @@ class TestCompleteStreamWalAcceptance:
def test_failed_llm_persists_question_with_failed_status(
self, pg_conn, flask_app,
):
from application.api.answer.routes.base import BaseAnswerResource
from application.storage.db.repositories.conversations import (
from docsgpt.api.answer.routes.base import BaseAnswerResource
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -993,8 +993,8 @@ class TestCompleteStreamWalAcceptance:
answer with no error and no retry affordance — the user saw a blank
message and re-sent the prompt. Reproduces the 2026-08-01 report.
"""
from application.api.answer.routes.base import BaseAnswerResource
from application.storage.db.repositories.conversations import (
from docsgpt.api.answer.routes.base import BaseAnswerResource
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -1044,8 +1044,8 @@ class TestCompleteStreamWalAcceptance:
then a downstream node failed, the row stays ``complete`` so the
partial answer still renders; the error was already surfaced live.
"""
from application.api.answer.routes.base import BaseAnswerResource
from application.storage.db.repositories.conversations import (
from docsgpt.api.answer.routes.base import BaseAnswerResource
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -1095,8 +1095,8 @@ class TestCompleteStreamWalAcceptance:
column default ``complete`` — leaving exactly the blank bubble this
changeset removes on the other branch.
"""
from application.api.answer.routes.base import BaseAnswerResource
from application.storage.db.repositories.conversations import (
from docsgpt.api.answer.routes.base import BaseAnswerResource
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -1143,7 +1143,7 @@ class TestCompleteStreamWalAcceptance:
``pending_tool_state`` row would deep-link the user to a 404.
Gate the publish on save_state actually committing.
"""
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -1173,10 +1173,10 @@ class TestCompleteStreamWalAcceptance:
published.append(args)
with _patch_db_session(pg_conn), patch(
"application.api.answer.routes.base.publish_user_event",
"docsgpt.api.answer.routes.base.publish_user_event",
side_effect=_capture,
), patch(
"application.api.answer.services.continuation_service."
"docsgpt.api.answer.services.continuation_service."
"ContinuationService.save_state",
side_effect=RuntimeError("PG outage"),
):
@@ -1213,8 +1213,8 @@ class TestCompleteStreamWalAcceptance:
from sqlalchemy import text as sql_text
from application.api.answer.routes.base import BaseAnswerResource
from application.storage.db.repositories.message_events import (
from docsgpt.api.answer.routes.base import BaseAnswerResource
from docsgpt.storage.db.repositories.message_events import (
MessageEventsRepository,
)
@@ -1296,8 +1296,8 @@ class TestCompleteStreamWalAcceptance:
row, so client-side correlation, ``token_usage`` joins, and
``count_in_range``'s DISTINCT all line up.
"""
from application.api.answer.routes.base import BaseAnswerResource
from application.storage.db.repositories.conversations import (
from docsgpt.api.answer.routes.base import BaseAnswerResource
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -1357,7 +1357,7 @@ class TestStreamingHeartbeatSeed:
def test_heartbeat_seeded_at_generation_start_and_on_first_chunk(
self, mock_mongo_db, flask_app,
):
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -1417,7 +1417,7 @@ class TestStreamingHeartbeatSeed:
self, mock_mongo_db, flask_app,
):
"""No DB-backed message row → no heartbeat call (and no error)."""
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -1471,11 +1471,11 @@ class TestCompleteStreamSupersededLookupIsGuarded:
):
import logging
from application.api.answer.routes.base import BaseAnswerResource
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
MessageUpdateOutcome,
)
@@ -1,4 +1,4 @@
"""Unit tests for application/api/answer/services/conversation_service.py.
"""Unit tests for docsgpt/api/answer/services/conversation_service.py.
Additional coverage beyond tests/api/answer/services/test_conversation_service.py:
- save_conversation: index-based update, metadata persistence, agent key tracking
@@ -18,7 +18,7 @@ class TestConversationServiceGetExtended:
pass
def test_handles_exception_gracefully(self, mock_mongo_db):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
@@ -33,7 +33,7 @@ class TestSaveConversationExtended:
pass
def test_raises_for_none_token(self, mock_mongo_db):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
@@ -71,7 +71,7 @@ class TestGetCompressionMetadata:
pass
def test_returns_none_for_missing_conversation(self, mock_mongo_db):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
@@ -80,7 +80,7 @@ class TestGetCompressionMetadata:
assert result is None
def test_handles_invalid_id(self, mock_mongo_db):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
@@ -100,7 +100,7 @@ class TestConversationServiceGaps:
def test_append_compression_message_empty_summary_skips(self, mock_mongo_db):
"""Cover: empty summary does not insert."""
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
@@ -12,12 +12,12 @@ from unittest.mock import MagicMock, patch
import pytest
from application.agents.classic_agent import ClassicAgent
from docsgpt.agents.classic_agent import ClassicAgent
def _agent(**kwargs):
with patch("application.llm.llm_creator.LLMCreator.create_llm"), patch(
"application.llm.handlers.handler_creator.LLMHandlerCreator.create_handler"
with patch("docsgpt.llm.llm_creator.LLMCreator.create_llm"), patch(
"docsgpt.llm.handlers.handler_creator.LLMHandlerCreator.create_handler"
):
return ClassicAgent(
endpoint="stream",
@@ -13,8 +13,8 @@ from unittest.mock import MagicMock, patch
import pytest
from application.agents.classic_agent import ClassicAgent
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.agents.classic_agent import ClassicAgent
from docsgpt.api.answer.services.stream_processor import StreamProcessor
DOCS = [
{"text": "Clause 4: reporting is due within 30 days.", "filename": "aml.pdf"},
@@ -72,8 +72,8 @@ class TestRetrievalReachesTheAgent:
@pytest.mark.unit
class TestDocumentsLandInTheUserTurn:
def _agent(self, **kwargs):
with patch("application.llm.llm_creator.LLMCreator.create_llm"), patch(
"application.llm.handlers.handler_creator.LLMHandlerCreator.create_handler"
with patch("docsgpt.llm.llm_creator.LLMCreator.create_llm"), patch(
"docsgpt.llm.handlers.handler_creator.LLMHandlerCreator.create_handler"
):
return ClassicAgent(
endpoint="stream",
@@ -110,7 +110,7 @@ class TestChunksPrecedence:
"""
def _sp(self, request_chunks=None, source_chunks=None):
from application.storage.db.source_config import RetrievalConfig
from docsgpt.storage.db.source_config import RetrievalConfig
sp = _processor()
sp._agent_data = None
@@ -31,9 +31,9 @@ def _patch_journal_session(conn):
yield conn
with patch(
"application.streaming.message_journal.db_session", _yield
"docsgpt.streaming.message_journal.db_session", _yield
), patch(
"application.streaming.event_replay.db_readonly", _yield
"docsgpt.streaming.event_replay.db_readonly", _yield
):
yield
@@ -78,8 +78,8 @@ class TestSnapshotPlusTailRoundTrip:
snapshot back via the same repo and formats it for the wire — the
exact primitive the async reader replays through.
"""
from application.streaming.event_replay import read_snapshot_lines
from application.streaming.message_journal import record_event
from docsgpt.streaming.event_replay import read_snapshot_lines
from docsgpt.streaming.message_journal import record_event
_, message_id = _seed_message(pg_conn)
@@ -99,8 +99,8 @@ class TestSnapshotPlusTailRoundTrip:
assert terminal is True
def test_snapshot_resumes_past_last_event_id(self, pg_conn):
from application.streaming.event_replay import read_snapshot_lines
from application.streaming.message_journal import record_event
from docsgpt.streaming.event_replay import read_snapshot_lines
from docsgpt.streaming.message_journal import record_event
_, message_id = _seed_message(pg_conn)
@@ -121,7 +121,7 @@ class TestSnapshotPlusTailRoundTrip:
"""The async route's ownership gate runs real SQL against
``conversation_messages`` — the owner passes, everyone else 404s.
"""
from application.api import async_sse
from docsgpt.api import async_sse
user_id, message_id = _seed_message(pg_conn)
@@ -129,7 +129,7 @@ class TestSnapshotPlusTailRoundTrip:
def _yield():
yield pg_conn
with patch("application.api.async_sse.db_readonly", _yield):
with patch("docsgpt.api.async_sse.db_readonly", _yield):
assert async_sse._user_owns_message(message_id, user_id) is True
assert async_sse._user_owns_message(message_id, "different-user") is False
# A well-formed but unknown id is also not owned.
@@ -139,8 +139,8 @@ class TestSnapshotPlusTailRoundTrip:
"""``read_snapshot_lines(..., user_id=)`` re-asserts ownership at the
data layer: the owner gets the journal rows, a non-owner gets none.
"""
from application.streaming.event_replay import read_snapshot_lines
from application.streaming.message_journal import record_event
from docsgpt.streaming.event_replay import read_snapshot_lines
from docsgpt.streaming.message_journal import record_event
user_id, message_id = _seed_message(pg_conn)
@@ -166,7 +166,7 @@ class TestSnapshotPlusTailRoundTrip:
own row; a non-owner reads as missing (terminal), not as the row's
real status.
"""
from application.streaming.event_replay import _check_producer_liveness
from docsgpt.streaming.event_replay import _check_producer_liveness
# Seed a row and flip it to a terminal status the watchdog reports.
user_id, message_id = _seed_message(pg_conn)
@@ -181,7 +181,7 @@ class TestSnapshotPlusTailRoundTrip:
def _yield():
yield pg_conn
with patch("application.streaming.event_replay.db_readonly", _yield):
with patch("docsgpt.streaming.event_replay.db_readonly", _yield):
owner = _check_producer_liveness(message_id, user_id, 90.0)
other = _check_producer_liveness(message_id, "different-user", 90.0)
@@ -16,7 +16,7 @@ from __future__ import annotations
import pytest
import application.api.answer.services.stream_processor as sp_mod
import docsgpt.api.answer.services.stream_processor as sp_mod
StreamProcessor = sp_mod.StreamProcessor
@@ -15,7 +15,7 @@ from unittest.mock import MagicMock, patch
import pytest
from application.storage.db.repositories.conversations import HeartbeatState
from docsgpt.storage.db.repositories.conversations import HeartbeatState
def _reservation(message_id="11111111-1111-1111-1111-111111111111"):
@@ -59,8 +59,8 @@ def _service(state=HeartbeatState.STAMPED):
@pytest.mark.unit
class TestHeartbeatTicker:
def _run(self, flask_app, gen, service=None, interval=0.05):
from application.api.answer.routes import base as base_mod
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes import base as base_mod
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -99,8 +99,8 @@ class TestHeartbeatTicker:
assert service.heartbeat_message_state.call_count == settled
def test_ticker_stops_on_client_abort(self, mock_mongo_db, flask_app):
from application.api.answer.routes import base as base_mod
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes import base as base_mod
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -131,8 +131,8 @@ class TestHeartbeatTicker:
def test_no_ticker_without_a_reserved_row(self, mock_mongo_db, flask_app):
"""Headless/continuation rounds have no row to stamp."""
from application.api.answer.routes import base as base_mod
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes import base as base_mod
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -177,8 +177,8 @@ class TestSupersededStreamCancellation:
"""A deleted row must stop the work, not just quiet the logs."""
def _run_with_missing_row(self, flask_app, gen, interval=0.05):
from application.api.answer.routes import base as base_mod
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes import base as base_mod
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -227,8 +227,8 @@ class TestSupersededStreamCancellation:
def test_live_row_is_never_cancelled(self, mock_mongo_db, flask_app):
"""The common case must be untouched."""
from application.api.answer.routes import base as base_mod
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes import base as base_mod
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
@@ -255,8 +255,8 @@ class TestSupersededStreamCancellation:
def test_db_error_does_not_cancel_the_stream(self, mock_mongo_db, flask_app):
"""A transient blip must never be mistaken for a deleted row."""
from application.api.answer.routes import base as base_mod
from application.api.answer.routes.base import BaseAnswerResource
from docsgpt.api.answer.routes import base as base_mod
from docsgpt.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
File diff suppressed because it is too large. Load diff
@@ -6,8 +6,8 @@ from unittest.mock import MagicMock, patch
import pytest
from application.api.answer.services.stream_processor import StreamProcessor
from application.storage.db.source_config import RetrievalConfig
from docsgpt.api.answer.services.stream_processor import StreamProcessor
from docsgpt.storage.db.source_config import RetrievalConfig
def _processor() -> StreamProcessor:
@@ -164,12 +164,12 @@ class TestNonAgentSourceConfig:
"config": {"retrieval": {"exposure": "agentic_tool", "chunks": 7}}
}
with patch(
"application.api.answer.services.stream_processor.db_readonly"
"docsgpt.api.answer.services.stream_processor.db_readonly"
), patch(
"application.api.answer.services.stream_processor.can_access",
"docsgpt.api.answer.services.stream_processor.can_access",
return_value=True,
), patch(
"application.api.answer.services.stream_processor.SourcesRepository"
"docsgpt.api.answer.services.stream_processor.SourcesRepository"
) as repo:
# Read unscoped after the access check, so a team grantee gets the
# source's real config instead of silently falling back to defaults.