mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-14 18:18:08 +00:00
chore: more tests
This commit is contained in:
1 parent
dc6db847ca
commit
d5c0322e2a
64 files changed
+24436
-140
No files matched your search
@@ -0,0 +1,46 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestCompressionThresholdChecker:
|
||||
|
||||
def _make_checker(self, pct=0.7):
|
||||
from application.api.answer.services.compression.threshold_checker import (
|
||||
CompressionThresholdChecker,
|
||||
)
|
||||
|
||||
return CompressionThresholdChecker(threshold_percentage=pct)
|
||||
|
||||
@patch(
|
||||
"application.api.answer.services.compression.threshold_checker.get_token_limit",
|
||||
return_value=8000,
|
||||
)
|
||||
@patch(
|
||||
"application.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):
|
||||
checker = self._make_checker(0.7)
|
||||
assert checker.check_message_tokens([{"role": "user"}], "gpt-4") is True
|
||||
|
||||
@patch(
|
||||
"application.api.answer.services.compression.threshold_checker.get_token_limit",
|
||||
return_value=8000,
|
||||
)
|
||||
@patch(
|
||||
"application.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):
|
||||
checker = self._make_checker(0.7)
|
||||
assert checker.check_message_tokens([{"role": "user"}], "gpt-4") is False
|
||||
|
||||
@patch(
|
||||
"application.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):
|
||||
checker = self._make_checker(0.7)
|
||||
assert checker.check_message_tokens([], "gpt-4") is False
|
||||
@@ -0,0 +1,363 @@
|
||||
"""Unit tests for application/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
|
||||
- complete_stream: tool_calls, thoughts, structured output, metadata,
|
||||
isNoneDoc, GeneratorExit handling, compression metadata
|
||||
- process_response_stream: structured answer, incomplete stream
|
||||
- error_stream_generate: format
|
||||
- check_usage: string boolean parsing ("True" strings)
|
||||
"""
|
||||
|
||||
import json
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from bson import ObjectId
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestPrepareToolCallsForLogging:
|
||||
|
||||
def test_empty_list(self, mock_mongo_db, flask_app):
|
||||
from application.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
|
||||
|
||||
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
|
||||
|
||||
with flask_app.app_context():
|
||||
resource = BaseAnswerResource()
|
||||
tool_calls = [{"result": "x" * 20000}]
|
||||
prepared = resource._prepare_tool_calls_for_logging(tool_calls, max_chars=100)
|
||||
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
|
||||
|
||||
with flask_app.app_context():
|
||||
resource = BaseAnswerResource()
|
||||
tool_calls = [{"result_full": "y" * 20000}]
|
||||
prepared = resource._prepare_tool_calls_for_logging(tool_calls, max_chars=50)
|
||||
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
|
||||
|
||||
with flask_app.app_context():
|
||||
resource = BaseAnswerResource()
|
||||
tool_calls = ["string_item", 42]
|
||||
prepared = resource._prepare_tool_calls_for_logging(tool_calls)
|
||||
assert prepared[0] == {"result": "string_item"}
|
||||
assert prepared[1] == {"result": "42"}
|
||||
|
||||
def test_preserves_short_results(self, mock_mongo_db, flask_app):
|
||||
from application.api.answer.routes.base import BaseAnswerResource
|
||||
|
||||
with flask_app.app_context():
|
||||
resource = BaseAnswerResource()
|
||||
tool_calls = [{"tool_name": "search", "result": "short text"}]
|
||||
prepared = resource._prepare_tool_calls_for_logging(tool_calls)
|
||||
assert prepared[0]["result"] == "short text"
|
||||
assert prepared[0]["tool_name"] == "search"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestCompleteStreamToolCalls:
|
||||
|
||||
def test_streams_tool_calls(self, mock_mongo_db, flask_app):
|
||||
from application.api.answer.routes.base import BaseAnswerResource
|
||||
|
||||
with flask_app.app_context():
|
||||
resource = BaseAnswerResource()
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.gen.return_value = iter(
|
||||
[
|
||||
{"answer": "Using tool..."},
|
||||
{"tool_calls": [{"name": "search", "result": "found"}]},
|
||||
]
|
||||
)
|
||||
|
||||
stream = list(
|
||||
resource.complete_stream(
|
||||
question="Search for X",
|
||||
agent=mock_agent,
|
||||
conversation_id=None,
|
||||
user_api_key=None,
|
||||
decoded_token={"sub": "u"},
|
||||
should_save_conversation=False,
|
||||
)
|
||||
)
|
||||
tool_chunks = [s for s in stream if '"type": "tool_calls"' in s]
|
||||
assert len(tool_chunks) == 1
|
||||
|
||||
def test_streams_thought_events(self, mock_mongo_db, flask_app):
|
||||
from application.api.answer.routes.base import BaseAnswerResource
|
||||
|
||||
with flask_app.app_context():
|
||||
resource = BaseAnswerResource()
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.gen.return_value = iter(
|
||||
[
|
||||
{"thought": "Let me think..."},
|
||||
{"answer": "Here is the answer"},
|
||||
]
|
||||
)
|
||||
|
||||
stream = list(
|
||||
resource.complete_stream(
|
||||
question="Q",
|
||||
agent=mock_agent,
|
||||
conversation_id=None,
|
||||
user_api_key=None,
|
||||
decoded_token={"sub": "u"},
|
||||
should_save_conversation=False,
|
||||
)
|
||||
)
|
||||
thought_chunks = [s for s in stream if '"type": "thought"' in s]
|
||||
assert len(thought_chunks) == 1
|
||||
assert "Let me think" in thought_chunks[0]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestCompleteStreamStructuredOutput:
|
||||
|
||||
def test_streams_structured_answer(self, mock_mongo_db, flask_app):
|
||||
from application.api.answer.routes.base import BaseAnswerResource
|
||||
|
||||
with flask_app.app_context():
|
||||
resource = BaseAnswerResource()
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.gen.return_value = iter(
|
||||
[
|
||||
{
|
||||
"answer": '{"key": "value"}',
|
||||
"structured": True,
|
||||
"schema": {"type": "object"},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
stream = list(
|
||||
resource.complete_stream(
|
||||
question="Q",
|
||||
agent=mock_agent,
|
||||
conversation_id=None,
|
||||
user_api_key=None,
|
||||
decoded_token={"sub": "u"},
|
||||
should_save_conversation=False,
|
||||
)
|
||||
)
|
||||
structured_chunks = [
|
||||
s for s in stream if '"type": "structured_answer"' in s
|
||||
]
|
||||
assert len(structured_chunks) == 1
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestCompleteStreamMetadata:
|
||||
|
||||
def test_metadata_collected(self, mock_mongo_db, flask_app):
|
||||
from application.api.answer.routes.base import BaseAnswerResource
|
||||
|
||||
with flask_app.app_context():
|
||||
resource = BaseAnswerResource()
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.gen.return_value = iter(
|
||||
[
|
||||
{"metadata": {"search_query": "test"}},
|
||||
{"answer": "result"},
|
||||
]
|
||||
)
|
||||
|
||||
stream = list(
|
||||
resource.complete_stream(
|
||||
question="Q",
|
||||
agent=mock_agent,
|
||||
conversation_id=None,
|
||||
user_api_key=None,
|
||||
decoded_token={"sub": "u"},
|
||||
should_save_conversation=False,
|
||||
)
|
||||
)
|
||||
# Should not crash, metadata handled silently
|
||||
answer_chunks = [s for s in stream if '"type": "answer"' in s]
|
||||
assert len(answer_chunks) == 1
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestCompleteStreamIsNoneDoc:
|
||||
|
||||
def test_isNoneDoc_sets_source_to_none(self, mock_mongo_db, flask_app):
|
||||
from application.api.answer.routes.base import BaseAnswerResource
|
||||
|
||||
with flask_app.app_context():
|
||||
resource = BaseAnswerResource()
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.gen.return_value = iter(
|
||||
[
|
||||
{"answer": "answer"},
|
||||
{"sources": [{"text": "doc", "source": "real_source"}]},
|
||||
]
|
||||
)
|
||||
|
||||
stream = list(
|
||||
resource.complete_stream(
|
||||
question="Q",
|
||||
agent=mock_agent,
|
||||
conversation_id=None,
|
||||
user_api_key=None,
|
||||
decoded_token={"sub": "u"},
|
||||
isNoneDoc=True,
|
||||
should_save_conversation=False,
|
||||
)
|
||||
)
|
||||
# Verify stream completes without error
|
||||
end_chunks = [s for s in stream if '"type": "end"' in s]
|
||||
assert len(end_chunks) == 1
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestCompleteStreamErrorType:
|
||||
|
||||
def test_error_type_event_sanitized(self, mock_mongo_db, flask_app):
|
||||
from application.api.answer.routes.base import BaseAnswerResource
|
||||
|
||||
with flask_app.app_context():
|
||||
resource = BaseAnswerResource()
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.gen.return_value = iter(
|
||||
[
|
||||
{"type": "error", "error": "API key invalid: sk-xxx"},
|
||||
]
|
||||
)
|
||||
|
||||
stream = list(
|
||||
resource.complete_stream(
|
||||
question="Q",
|
||||
agent=mock_agent,
|
||||
conversation_id=None,
|
||||
user_api_key=None,
|
||||
decoded_token={"sub": "u"},
|
||||
should_save_conversation=False,
|
||||
)
|
||||
)
|
||||
error_chunks = [s for s in stream if '"type": "error"' in s]
|
||||
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
|
||||
|
||||
with flask_app.app_context():
|
||||
resource = BaseAnswerResource()
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.gen.return_value = iter(
|
||||
[
|
||||
{"type": "custom_event", "data": "value"},
|
||||
]
|
||||
)
|
||||
|
||||
stream = list(
|
||||
resource.complete_stream(
|
||||
question="Q",
|
||||
agent=mock_agent,
|
||||
conversation_id=None,
|
||||
user_api_key=None,
|
||||
decoded_token={"sub": "u"},
|
||||
should_save_conversation=False,
|
||||
)
|
||||
)
|
||||
custom_chunks = [s for s in stream if '"type": "custom_event"' in s]
|
||||
assert len(custom_chunks) == 1
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestProcessResponseStreamExtended:
|
||||
|
||||
def test_handles_structured_answer(self, mock_mongo_db, flask_app):
|
||||
from application.api.answer.routes.base import BaseAnswerResource
|
||||
|
||||
with flask_app.app_context():
|
||||
resource = BaseAnswerResource()
|
||||
stream = [
|
||||
f'data: {json.dumps({"type": "structured_answer", "answer": "{}", "structured": True, "schema": None})}\n\n',
|
||||
f'data: {json.dumps({"type": "id", "id": str(ObjectId())})}\n\n',
|
||||
f'data: {json.dumps({"type": "end"})}\n\n',
|
||||
]
|
||||
result = resource.process_response_stream(iter(stream))
|
||||
assert result[1] == "{}"
|
||||
# Structured output adds extra tuple element
|
||||
assert len(result) == 7
|
||||
assert result[6]["structured"] is True
|
||||
|
||||
def test_handles_tool_calls_event(self, mock_mongo_db, flask_app):
|
||||
from application.api.answer.routes.base import BaseAnswerResource
|
||||
|
||||
with flask_app.app_context():
|
||||
resource = BaseAnswerResource()
|
||||
stream = [
|
||||
f'data: {json.dumps({"type": "answer", "answer": "result"})}\n\n',
|
||||
f'data: {json.dumps({"type": "tool_calls", "tool_calls": [{"name": "t1"}]})}\n\n',
|
||||
f'data: {json.dumps({"type": "id", "id": "conv1"})}\n\n',
|
||||
f'data: {json.dumps({"type": "end"})}\n\n',
|
||||
]
|
||||
result = resource.process_response_stream(iter(stream))
|
||||
assert result[3] == [{"name": "t1"}]
|
||||
|
||||
def test_incomplete_stream(self, mock_mongo_db, flask_app):
|
||||
from application.api.answer.routes.base import BaseAnswerResource
|
||||
|
||||
with flask_app.app_context():
|
||||
resource = BaseAnswerResource()
|
||||
stream = [
|
||||
f'data: {json.dumps({"type": "answer", "answer": "partial"})}\n\n',
|
||||
]
|
||||
result = resource.process_response_stream(iter(stream))
|
||||
assert result[4] == "Stream ended unexpectedly"
|
||||
|
||||
def test_handles_thought_event(self, mock_mongo_db, flask_app):
|
||||
from application.api.answer.routes.base import BaseAnswerResource
|
||||
|
||||
with flask_app.app_context():
|
||||
resource = BaseAnswerResource()
|
||||
stream = [
|
||||
f'data: {json.dumps({"type": "thought", "thought": "thinking..."})}\n\n',
|
||||
f'data: {json.dumps({"type": "end"})}\n\n',
|
||||
]
|
||||
result = resource.process_response_stream(iter(stream))
|
||||
assert result[4] == "thinking..."
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestCheckUsageStringBooleans:
|
||||
|
||||
def test_string_true_parsed_correctly(self, mock_mongo_db, flask_app):
|
||||
from application.api.answer.routes.base import BaseAnswerResource
|
||||
from application.core.settings import settings
|
||||
|
||||
with flask_app.app_context():
|
||||
agents_collection = mock_mongo_db[settings.MONGO_DB_NAME]["agents"]
|
||||
agents_collection.insert_one(
|
||||
{
|
||||
"_id": ObjectId(),
|
||||
"key": "str_bool_key",
|
||||
"limited_token_mode": "True",
|
||||
"token_limit": 1000000,
|
||||
"limited_request_mode": "True",
|
||||
"request_limit": 1000000,
|
||||
}
|
||||
)
|
||||
resource = BaseAnswerResource()
|
||||
result = resource.check_usage({"user_api_key": "str_bool_key"})
|
||||
# Should not exceed limits, so returns None
|
||||
assert result is None
|
||||
@@ -0,0 +1,418 @@
|
||||
"""Unit tests for application/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
|
||||
- update_compression_metadata
|
||||
- append_compression_message
|
||||
- get_compression_metadata
|
||||
- Edge cases: None token, empty summary, shared_with access
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from bson import ObjectId
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestConversationServiceGetExtended:
|
||||
|
||||
def test_returns_conversation_for_shared_user(self, mock_mongo_db):
|
||||
from application.api.answer.services.conversation_service import (
|
||||
ConversationService,
|
||||
)
|
||||
from application.core.settings import settings
|
||||
|
||||
service = ConversationService()
|
||||
collection = mock_mongo_db[settings.MONGO_DB_NAME]["conversations"]
|
||||
|
||||
conv_id = ObjectId()
|
||||
collection.insert_one(
|
||||
{
|
||||
"_id": conv_id,
|
||||
"user": "owner_123",
|
||||
"shared_with": ["shared_user"],
|
||||
"name": "Shared Conv",
|
||||
"queries": [],
|
||||
}
|
||||
)
|
||||
|
||||
result = service.get_conversation(str(conv_id), "shared_user")
|
||||
assert result is not None
|
||||
assert result["name"] == "Shared Conv"
|
||||
|
||||
def test_handles_exception_gracefully(self, mock_mongo_db):
|
||||
from application.api.answer.services.conversation_service import (
|
||||
ConversationService,
|
||||
)
|
||||
|
||||
service = ConversationService()
|
||||
# Pass an invalid ObjectId
|
||||
result = service.get_conversation("not-an-objectid", "user_123")
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestSaveConversationExtended:
|
||||
|
||||
def test_raises_for_none_token(self, mock_mongo_db):
|
||||
from application.api.answer.services.conversation_service import (
|
||||
ConversationService,
|
||||
)
|
||||
|
||||
service = ConversationService()
|
||||
with pytest.raises(ValueError, match="Invalid or missing authentication"):
|
||||
service.save_conversation(
|
||||
conversation_id=None,
|
||||
question="Q",
|
||||
response="A",
|
||||
thought="",
|
||||
sources=[],
|
||||
tool_calls=[],
|
||||
llm=Mock(),
|
||||
model_id="m",
|
||||
decoded_token=None,
|
||||
)
|
||||
|
||||
def test_update_existing_at_index(self, mock_mongo_db):
|
||||
from application.api.answer.services.conversation_service import (
|
||||
ConversationService,
|
||||
)
|
||||
from application.core.settings import settings
|
||||
|
||||
service = ConversationService()
|
||||
collection = mock_mongo_db[settings.MONGO_DB_NAME]["conversations"]
|
||||
|
||||
conv_id = ObjectId()
|
||||
collection.insert_one(
|
||||
{
|
||||
"_id": conv_id,
|
||||
"user": "user_123",
|
||||
"name": "Conv",
|
||||
"queries": [
|
||||
{
|
||||
"prompt": "Q1",
|
||||
"response": "A1",
|
||||
"thought": "",
|
||||
"sources": [],
|
||||
"tool_calls": [],
|
||||
},
|
||||
{
|
||||
"prompt": "Q2",
|
||||
"response": "A2",
|
||||
"thought": "",
|
||||
"sources": [],
|
||||
"tool_calls": [],
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
result = service.save_conversation(
|
||||
conversation_id=str(conv_id),
|
||||
question="Q1_updated",
|
||||
response="A1_updated",
|
||||
thought="thinking",
|
||||
sources=[],
|
||||
tool_calls=[],
|
||||
llm=Mock(),
|
||||
model_id="gpt-4",
|
||||
decoded_token={"sub": "user_123"},
|
||||
index=0,
|
||||
)
|
||||
assert result == str(conv_id)
|
||||
|
||||
saved = collection.find_one({"_id": conv_id})
|
||||
assert saved["queries"][0]["prompt"] == "Q1_updated"
|
||||
assert saved["queries"][0]["response"] == "A1_updated"
|
||||
|
||||
def test_update_at_index_unauthorized(self, mock_mongo_db):
|
||||
from application.api.answer.services.conversation_service import (
|
||||
ConversationService,
|
||||
)
|
||||
from application.core.settings import settings
|
||||
|
||||
service = ConversationService()
|
||||
collection = mock_mongo_db[settings.MONGO_DB_NAME]["conversations"]
|
||||
|
||||
conv_id = ObjectId()
|
||||
collection.insert_one(
|
||||
{
|
||||
"_id": conv_id,
|
||||
"user": "owner",
|
||||
"queries": [{"prompt": "Q", "response": "A"}],
|
||||
}
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="not found or unauthorized"):
|
||||
service.save_conversation(
|
||||
conversation_id=str(conv_id),
|
||||
question="Hack",
|
||||
response="Attempt",
|
||||
thought="",
|
||||
sources=[],
|
||||
tool_calls=[],
|
||||
llm=Mock(),
|
||||
model_id="m",
|
||||
decoded_token={"sub": "hacker"},
|
||||
index=0,
|
||||
)
|
||||
|
||||
def test_saves_metadata(self, mock_mongo_db):
|
||||
from application.api.answer.services.conversation_service import (
|
||||
ConversationService,
|
||||
)
|
||||
from application.core.settings import settings
|
||||
|
||||
service = ConversationService()
|
||||
collection = mock_mongo_db[settings.MONGO_DB_NAME]["conversations"]
|
||||
|
||||
mock_llm = Mock()
|
||||
mock_llm.gen.return_value = "Title"
|
||||
|
||||
conv_id = service.save_conversation(
|
||||
conversation_id=None,
|
||||
question="Q",
|
||||
response="A",
|
||||
thought="",
|
||||
sources=[],
|
||||
tool_calls=[],
|
||||
llm=mock_llm,
|
||||
model_id="m",
|
||||
decoded_token={"sub": "user_123"},
|
||||
metadata={"search_query": "rewritten query"},
|
||||
)
|
||||
|
||||
saved = collection.find_one({"_id": ObjectId(conv_id)})
|
||||
assert saved["queries"][0]["metadata"] == {"search_query": "rewritten query"}
|
||||
|
||||
def test_no_metadata_when_none(self, mock_mongo_db):
|
||||
from application.api.answer.services.conversation_service import (
|
||||
ConversationService,
|
||||
)
|
||||
from application.core.settings import settings
|
||||
|
||||
service = ConversationService()
|
||||
collection = mock_mongo_db[settings.MONGO_DB_NAME]["conversations"]
|
||||
|
||||
mock_llm = Mock()
|
||||
mock_llm.gen.return_value = "Title"
|
||||
|
||||
conv_id = service.save_conversation(
|
||||
conversation_id=None,
|
||||
question="Q",
|
||||
response="A",
|
||||
thought="",
|
||||
sources=[],
|
||||
tool_calls=[],
|
||||
llm=mock_llm,
|
||||
model_id="m",
|
||||
decoded_token={"sub": "user_123"},
|
||||
metadata=None,
|
||||
)
|
||||
|
||||
saved = collection.find_one({"_id": ObjectId(conv_id)})
|
||||
assert "metadata" not in saved["queries"][0]
|
||||
|
||||
def test_saves_with_api_key_and_agent(self, mock_mongo_db):
|
||||
from application.api.answer.services.conversation_service import (
|
||||
ConversationService,
|
||||
)
|
||||
from application.core.settings import settings
|
||||
|
||||
service = ConversationService()
|
||||
collection = mock_mongo_db[settings.MONGO_DB_NAME]["conversations"]
|
||||
agents_collection = mock_mongo_db[settings.MONGO_DB_NAME]["agents"]
|
||||
|
||||
agent_id = ObjectId()
|
||||
agents_collection.insert_one(
|
||||
{"_id": agent_id, "key": "agent_key_123", "user": "user_123"}
|
||||
)
|
||||
|
||||
mock_llm = Mock()
|
||||
mock_llm.gen.return_value = "Title"
|
||||
|
||||
conv_id = service.save_conversation(
|
||||
conversation_id=None,
|
||||
question="Q",
|
||||
response="A",
|
||||
thought="",
|
||||
sources=[],
|
||||
tool_calls=[],
|
||||
llm=mock_llm,
|
||||
model_id="m",
|
||||
decoded_token={"sub": "user_123"},
|
||||
api_key="agent_key_123",
|
||||
agent_id=str(agent_id),
|
||||
)
|
||||
|
||||
saved = collection.find_one({"_id": ObjectId(conv_id)})
|
||||
assert saved["api_key"] == "agent_key_123"
|
||||
assert saved["agent_id"] == str(agent_id)
|
||||
|
||||
def test_empty_completion_uses_question_prefix(self, mock_mongo_db):
|
||||
from application.api.answer.services.conversation_service import (
|
||||
ConversationService,
|
||||
)
|
||||
from application.core.settings import settings
|
||||
|
||||
service = ConversationService()
|
||||
collection = mock_mongo_db[settings.MONGO_DB_NAME]["conversations"]
|
||||
|
||||
mock_llm = Mock()
|
||||
mock_llm.gen.return_value = " " # whitespace only
|
||||
|
||||
conv_id = service.save_conversation(
|
||||
conversation_id=None,
|
||||
question="What is the meaning of life in programming?",
|
||||
response="42",
|
||||
thought="",
|
||||
sources=[],
|
||||
tool_calls=[],
|
||||
llm=mock_llm,
|
||||
model_id="m",
|
||||
decoded_token={"sub": "user_123"},
|
||||
)
|
||||
|
||||
saved = collection.find_one({"_id": ObjectId(conv_id)})
|
||||
assert saved["name"] == "What is the meaning of life in programming?"[:50]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestUpdateCompressionMetadata:
|
||||
|
||||
def test_updates_compression_fields(self, mock_mongo_db):
|
||||
from application.api.answer.services.conversation_service import (
|
||||
ConversationService,
|
||||
)
|
||||
from application.core.settings import settings
|
||||
|
||||
service = ConversationService()
|
||||
collection = mock_mongo_db[settings.MONGO_DB_NAME]["conversations"]
|
||||
|
||||
conv_id = ObjectId()
|
||||
collection.insert_one(
|
||||
{"_id": conv_id, "user": "u", "queries": []}
|
||||
)
|
||||
|
||||
meta = {
|
||||
"timestamp": datetime.now(timezone.utc),
|
||||
"compressed_summary": "Summary of conversation",
|
||||
"model_used": "gpt-4",
|
||||
}
|
||||
|
||||
service.update_compression_metadata(str(conv_id), meta)
|
||||
|
||||
saved = collection.find_one({"_id": conv_id})
|
||||
assert saved["compression_metadata"]["is_compressed"] is True
|
||||
assert len(saved["compression_metadata"]["compression_points"]) == 1
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestAppendCompressionMessage:
|
||||
|
||||
def test_appends_summary_query(self, mock_mongo_db):
|
||||
from application.api.answer.services.conversation_service import (
|
||||
ConversationService,
|
||||
)
|
||||
from application.core.settings import settings
|
||||
|
||||
service = ConversationService()
|
||||
collection = mock_mongo_db[settings.MONGO_DB_NAME]["conversations"]
|
||||
|
||||
conv_id = ObjectId()
|
||||
collection.insert_one(
|
||||
{"_id": conv_id, "user": "u", "queries": []}
|
||||
)
|
||||
|
||||
meta = {
|
||||
"compressed_summary": "This is the summary",
|
||||
"timestamp": datetime.now(timezone.utc),
|
||||
"model_used": "gpt-4",
|
||||
}
|
||||
|
||||
service.append_compression_message(str(conv_id), meta)
|
||||
|
||||
saved = collection.find_one({"_id": conv_id})
|
||||
assert len(saved["queries"]) == 1
|
||||
assert saved["queries"][0]["prompt"] == "[Context Compression Summary]"
|
||||
assert saved["queries"][0]["response"] == "This is the summary"
|
||||
|
||||
def test_empty_summary_does_nothing(self, mock_mongo_db):
|
||||
from application.api.answer.services.conversation_service import (
|
||||
ConversationService,
|
||||
)
|
||||
from application.core.settings import settings
|
||||
|
||||
service = ConversationService()
|
||||
collection = mock_mongo_db[settings.MONGO_DB_NAME]["conversations"]
|
||||
|
||||
conv_id = ObjectId()
|
||||
collection.insert_one(
|
||||
{"_id": conv_id, "user": "u", "queries": []}
|
||||
)
|
||||
|
||||
service.append_compression_message(str(conv_id), {"compressed_summary": ""})
|
||||
|
||||
saved = collection.find_one({"_id": conv_id})
|
||||
assert len(saved["queries"]) == 0
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestGetCompressionMetadata:
|
||||
|
||||
def test_returns_metadata(self, mock_mongo_db):
|
||||
from application.api.answer.services.conversation_service import (
|
||||
ConversationService,
|
||||
)
|
||||
from application.core.settings import settings
|
||||
|
||||
service = ConversationService()
|
||||
collection = mock_mongo_db[settings.MONGO_DB_NAME]["conversations"]
|
||||
|
||||
conv_id = ObjectId()
|
||||
collection.insert_one(
|
||||
{
|
||||
"_id": conv_id,
|
||||
"user": "u",
|
||||
"compression_metadata": {"is_compressed": True},
|
||||
}
|
||||
)
|
||||
|
||||
result = service.get_compression_metadata(str(conv_id))
|
||||
assert result is not None
|
||||
assert result["is_compressed"] is True
|
||||
|
||||
def test_returns_none_for_no_metadata(self, mock_mongo_db):
|
||||
from application.api.answer.services.conversation_service import (
|
||||
ConversationService,
|
||||
)
|
||||
from application.core.settings import settings
|
||||
|
||||
service = ConversationService()
|
||||
collection = mock_mongo_db[settings.MONGO_DB_NAME]["conversations"]
|
||||
|
||||
conv_id = ObjectId()
|
||||
collection.insert_one({"_id": conv_id, "user": "u"})
|
||||
|
||||
result = service.get_compression_metadata(str(conv_id))
|
||||
assert result is None
|
||||
|
||||
def test_returns_none_for_missing_conversation(self, mock_mongo_db):
|
||||
from application.api.answer.services.conversation_service import (
|
||||
ConversationService,
|
||||
)
|
||||
|
||||
service = ConversationService()
|
||||
result = service.get_compression_metadata(str(ObjectId()))
|
||||
assert result is None
|
||||
|
||||
def test_handles_invalid_id(self, mock_mongo_db):
|
||||
from application.api.answer.services.conversation_service import (
|
||||
ConversationService,
|
||||
)
|
||||
|
||||
service = ConversationService()
|
||||
result = service.get_compression_metadata("invalid-id")
|
||||
assert result is None
|
||||
@@ -1,4 +1,17 @@
|
||||
"""Tests for application/api/answer/services/stream_processor.py — get_prompt and helpers."""
|
||||
"""Tests for application/api/answer/services/stream_processor.py — get_prompt and helpers.
|
||||
|
||||
Extended coverage for StreamProcessor including:
|
||||
- get_prompt: all presets and DB fallback
|
||||
- StreamProcessor init, _resolve_agent_id, _get_prompt_content
|
||||
- _get_required_tool_actions
|
||||
- _get_attachments_content: valid, invalid, empty
|
||||
- _configure_retriever
|
||||
- _validate_and_set_model
|
||||
- _get_agent_key
|
||||
- _get_data_from_api_key
|
||||
- _configure_source
|
||||
- pre_fetch_docs
|
||||
"""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
@@ -119,6 +132,22 @@ class TestStreamProcessorInit:
|
||||
sp = StreamProcessor(request_data={}, decoded_token=None)
|
||||
assert sp.initial_user_id is None
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_init_default_model_and_config(self):
|
||||
mock_db = MagicMock()
|
||||
with patch("application.api.answer.services.stream_processor.MongoDB") as MockMongo, \
|
||||
patch("application.api.answer.services.stream_processor.settings") as mock_settings:
|
||||
mock_settings.MONGO_DB_NAME = "docsgpt"
|
||||
MockMongo.get_client.return_value = {"docsgpt": mock_db}
|
||||
|
||||
from application.api.answer.services.stream_processor import StreamProcessor
|
||||
sp = StreamProcessor(request_data={}, decoded_token={"sub": "u"})
|
||||
assert sp.model_id is None
|
||||
assert sp.is_shared_usage is False
|
||||
assert sp.shared_token is None
|
||||
assert sp.compressed_summary is None
|
||||
assert sp.compressed_summary_tokens == 0
|
||||
|
||||
|
||||
class TestGetAttachmentsContent:
|
||||
|
||||
@@ -169,6 +198,19 @@ class TestGetAttachmentsContent:
|
||||
result = sp._get_attachments_content(["bad"], "u")
|
||||
assert result == []
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_none_ids_returns_empty(self):
|
||||
mock_db = MagicMock()
|
||||
with patch("application.api.answer.services.stream_processor.MongoDB") as MockMongo, \
|
||||
patch("application.api.answer.services.stream_processor.settings") as mock_settings:
|
||||
mock_settings.MONGO_DB_NAME = "docsgpt"
|
||||
MockMongo.get_client.return_value = {"docsgpt": mock_db}
|
||||
|
||||
from application.api.answer.services.stream_processor import StreamProcessor
|
||||
sp = StreamProcessor(request_data={}, decoded_token={"sub": "u"})
|
||||
result = sp._get_attachments_content(None, "u")
|
||||
assert result == []
|
||||
|
||||
|
||||
class TestResolveAgentId:
|
||||
|
||||
@@ -250,6 +292,23 @@ class TestResolveAgentId:
|
||||
sp.conversation_service.get_conversation.side_effect = Exception("db error")
|
||||
assert sp._resolve_agent_id() is None
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_conversation_without_agent_id(self):
|
||||
mock_db = MagicMock()
|
||||
with patch("application.api.answer.services.stream_processor.MongoDB") as MockMongo, \
|
||||
patch("application.api.answer.services.stream_processor.settings") as mock_settings:
|
||||
mock_settings.MONGO_DB_NAME = "docsgpt"
|
||||
MockMongo.get_client.return_value = {"docsgpt": mock_db}
|
||||
|
||||
from application.api.answer.services.stream_processor import StreamProcessor
|
||||
sp = StreamProcessor(
|
||||
request_data={"conversation_id": "conv1"},
|
||||
decoded_token={"sub": "u"},
|
||||
)
|
||||
sp.conversation_service = MagicMock()
|
||||
sp.conversation_service.get_conversation.return_value = {"name": "test conv"}
|
||||
assert sp._resolve_agent_id() is None
|
||||
|
||||
|
||||
class TestGetPromptContent:
|
||||
|
||||
@@ -299,6 +358,19 @@ class TestGetPromptContent:
|
||||
sp.agent_config = {"prompt_id": "bad_id"}
|
||||
assert sp._get_prompt_content() is None
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_agent_config_not_dict(self):
|
||||
mock_db = MagicMock()
|
||||
with patch("application.api.answer.services.stream_processor.MongoDB") as MockMongo, \
|
||||
patch("application.api.answer.services.stream_processor.settings") as mock_settings:
|
||||
mock_settings.MONGO_DB_NAME = "docsgpt"
|
||||
MockMongo.get_client.return_value = {"docsgpt": mock_db}
|
||||
|
||||
from application.api.answer.services.stream_processor import StreamProcessor
|
||||
sp = StreamProcessor(request_data={}, decoded_token={"sub": "u"})
|
||||
sp.agent_config = "not_a_dict"
|
||||
assert sp._get_prompt_content() is None
|
||||
|
||||
|
||||
class TestGetRequiredToolActions:
|
||||
|
||||
@@ -329,3 +401,114 @@ class TestGetRequiredToolActions:
|
||||
sp._prompt_content = "No template syntax here"
|
||||
result = sp._get_required_tool_actions()
|
||||
assert result == {}
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_caches_result(self):
|
||||
mock_db = MagicMock()
|
||||
with patch("application.api.answer.services.stream_processor.MongoDB") as MockMongo, \
|
||||
patch("application.api.answer.services.stream_processor.settings") as mock_settings:
|
||||
mock_settings.MONGO_DB_NAME = "docsgpt"
|
||||
MockMongo.get_client.return_value = {"docsgpt": mock_db}
|
||||
|
||||
from application.api.answer.services.stream_processor import StreamProcessor
|
||||
sp = StreamProcessor(request_data={}, decoded_token={"sub": "u"})
|
||||
sp._required_tool_actions = {"tool1": {"action1"}}
|
||||
result = sp._get_required_tool_actions()
|
||||
assert result == {"tool1": {"action1"}}
|
||||
|
||||
|
||||
class TestConfigureRetriever:
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_default_values(self):
|
||||
mock_db = MagicMock()
|
||||
with patch("application.api.answer.services.stream_processor.MongoDB") as MockMongo, \
|
||||
patch("application.api.answer.services.stream_processor.settings") as mock_settings:
|
||||
mock_settings.MONGO_DB_NAME = "docsgpt"
|
||||
MockMongo.get_client.return_value = {"docsgpt": mock_db}
|
||||
|
||||
from application.api.answer.services.stream_processor import StreamProcessor
|
||||
sp = StreamProcessor(
|
||||
request_data={"question": "Q"},
|
||||
decoded_token={"sub": "u"},
|
||||
)
|
||||
sp.model_id = "test-model"
|
||||
sp.agent_key = None
|
||||
sp._configure_retriever()
|
||||
assert sp.retriever_config["retriever_name"] == "classic"
|
||||
assert sp.retriever_config["chunks"] == 2
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_isNoneDoc_sets_zero_chunks(self):
|
||||
mock_db = MagicMock()
|
||||
with patch("application.api.answer.services.stream_processor.MongoDB") as MockMongo, \
|
||||
patch("application.api.answer.services.stream_processor.settings") as mock_settings:
|
||||
mock_settings.MONGO_DB_NAME = "docsgpt"
|
||||
MockMongo.get_client.return_value = {"docsgpt": mock_db}
|
||||
|
||||
from application.api.answer.services.stream_processor import StreamProcessor
|
||||
sp = StreamProcessor(
|
||||
request_data={"question": "Q", "isNoneDoc": True},
|
||||
decoded_token={"sub": "u"},
|
||||
)
|
||||
sp.model_id = "test-model"
|
||||
sp.agent_key = None
|
||||
sp._configure_retriever()
|
||||
assert sp.retriever_config["chunks"] == 0
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_custom_retriever_and_chunks(self):
|
||||
mock_db = MagicMock()
|
||||
with patch("application.api.answer.services.stream_processor.MongoDB") as MockMongo, \
|
||||
patch("application.api.answer.services.stream_processor.settings") as mock_settings:
|
||||
mock_settings.MONGO_DB_NAME = "docsgpt"
|
||||
MockMongo.get_client.return_value = {"docsgpt": mock_db}
|
||||
|
||||
from application.api.answer.services.stream_processor import StreamProcessor
|
||||
sp = StreamProcessor(
|
||||
request_data={"question": "Q", "retriever": "hybrid", "chunks": "5"},
|
||||
decoded_token={"sub": "u"},
|
||||
)
|
||||
sp.model_id = "test-model"
|
||||
sp.agent_key = None
|
||||
sp._configure_retriever()
|
||||
assert sp.retriever_config["retriever_name"] == "hybrid"
|
||||
assert sp.retriever_config["chunks"] == 5
|
||||
|
||||
|
||||
class TestConfigureSource:
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_active_docs_from_request(self):
|
||||
mock_db = MagicMock()
|
||||
with patch("application.api.answer.services.stream_processor.MongoDB") as MockMongo, \
|
||||
patch("application.api.answer.services.stream_processor.settings") as mock_settings:
|
||||
mock_settings.MONGO_DB_NAME = "docsgpt"
|
||||
MockMongo.get_client.return_value = {"docsgpt": mock_db}
|
||||
|
||||
from application.api.answer.services.stream_processor import StreamProcessor
|
||||
sp = StreamProcessor(
|
||||
request_data={"question": "Q", "active_docs": "source_123"},
|
||||
decoded_token={"sub": "u"},
|
||||
)
|
||||
sp.agent_key = None
|
||||
sp._configure_source()
|
||||
assert sp.source == {"active_docs": "source_123"}
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_no_source_config(self):
|
||||
mock_db = MagicMock()
|
||||
with patch("application.api.answer.services.stream_processor.MongoDB") as MockMongo, \
|
||||
patch("application.api.answer.services.stream_processor.settings") as mock_settings:
|
||||
mock_settings.MONGO_DB_NAME = "docsgpt"
|
||||
MockMongo.get_client.return_value = {"docsgpt": mock_db}
|
||||
|
||||
from application.api.answer.services.stream_processor import StreamProcessor
|
||||
sp = StreamProcessor(
|
||||
request_data={"question": "Q"},
|
||||
decoded_token={"sub": "u"},
|
||||
)
|
||||
sp.agent_key = None
|
||||
sp._configure_source()
|
||||
assert sp.source == {}
|
||||
assert sp.all_sources == []
|
||||
Reference in new issue
Block a user