mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-15 06:15:57 +00:00
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:
1 parent
3e7f1f91b4
commit
574f96341e
984 files changed
+9847
-9682
No files matched your search
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user