mirror of
https://github.com/langgenius/dify.git
synced 2026-09-03 15:27:49 +08:00
test: migrate audit findings sessions and ORM models to SQLite (#40551)
This commit is contained in:
parent
9398482b53
commit
f8f71fdc68
@ -4,12 +4,16 @@ from unittest.mock import MagicMock, patch
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.app.apps.base_app_runner import AppRunner
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from graphon.file import FileTransferMethod, FileType
|
||||
from graphon.model_runtime.entities.message_entities import ImagePromptMessageContent
|
||||
from models.enums import CreatorUserRole
|
||||
from models.model import MessageFile
|
||||
from models.tools import ToolFile
|
||||
|
||||
|
||||
class TestBaseAppRunnerMultimodal:
|
||||
@ -38,18 +42,18 @@ class TestBaseAppRunnerMultimodal:
|
||||
return manager
|
||||
|
||||
@pytest.fixture
|
||||
def mock_tool_file(self):
|
||||
"""Create a mock tool file."""
|
||||
tool_file = MagicMock()
|
||||
tool_file.id = str(uuid4())
|
||||
return tool_file
|
||||
|
||||
@pytest.fixture
|
||||
def mock_message_file(self):
|
||||
"""Create a mock message file."""
|
||||
message_file = MagicMock()
|
||||
message_file.id = str(uuid4())
|
||||
return message_file
|
||||
def tool_file(self, mock_user_id: str, mock_tenant_id: str) -> ToolFile:
|
||||
"""Create a real transient tool-file model returned by the external file manager."""
|
||||
return ToolFile(
|
||||
user_id=mock_user_id,
|
||||
tenant_id=mock_tenant_id,
|
||||
conversation_id=None,
|
||||
file_key="generated/image.png",
|
||||
mimetype="image/png",
|
||||
original_url="http://example.com/image.png",
|
||||
name="image.png",
|
||||
size=68,
|
||||
)
|
||||
|
||||
def test_handle_multimodal_image_content_with_url(
|
||||
self,
|
||||
@ -57,8 +61,8 @@ class TestBaseAppRunnerMultimodal:
|
||||
mock_tenant_id,
|
||||
mock_message_id,
|
||||
mock_queue_manager,
|
||||
mock_tool_file,
|
||||
mock_message_file,
|
||||
tool_file,
|
||||
sqlite_session: Session,
|
||||
):
|
||||
"""Test handling image from URL."""
|
||||
# Arrange
|
||||
@ -72,48 +76,33 @@ class TestBaseAppRunnerMultimodal:
|
||||
with patch("core.app.apps.base_app_runner.ToolFileManager", autospec=True) as mock_mgr_class:
|
||||
# Setup mock tool file manager
|
||||
mock_mgr = MagicMock()
|
||||
mock_mgr.create_file_by_url.return_value = mock_tool_file
|
||||
mock_mgr.create_file_by_url.return_value = tool_file
|
||||
mock_mgr_class.return_value = mock_mgr
|
||||
|
||||
with patch("core.app.apps.base_app_runner.MessageFile", autospec=True) as mock_msg_file_class:
|
||||
# Setup mock message file
|
||||
mock_msg_file_class.return_value = mock_message_file
|
||||
message_file_id = AppRunner()._handle_multimodal_image_content(
|
||||
session=sqlite_session,
|
||||
content=content,
|
||||
message_id=mock_message_id,
|
||||
user_id=mock_user_id,
|
||||
tenant_id=mock_tenant_id,
|
||||
queue_manager=mock_queue_manager,
|
||||
)
|
||||
|
||||
file_session = MagicMock()
|
||||
# Act
|
||||
runner = MagicMock()
|
||||
method = AppRunner._handle_multimodal_image_content
|
||||
runner._handle_multimodal_image_content = lambda *args, **kwargs: method(runner, *args, **kwargs)
|
||||
|
||||
message_file_id = runner._handle_multimodal_image_content(
|
||||
session=file_session,
|
||||
content=content,
|
||||
message_id=mock_message_id,
|
||||
user_id=mock_user_id,
|
||||
tenant_id=mock_tenant_id,
|
||||
queue_manager=mock_queue_manager,
|
||||
)
|
||||
|
||||
# Assert
|
||||
mock_mgr.create_file_by_url.assert_called_once_with(
|
||||
user_id=mock_user_id,
|
||||
tenant_id=mock_tenant_id,
|
||||
file_url=image_url,
|
||||
conversation_id=None,
|
||||
)
|
||||
|
||||
mock_msg_file_class.assert_called_once()
|
||||
call_kwargs = mock_msg_file_class.call_args[1]
|
||||
assert call_kwargs["message_id"] == mock_message_id
|
||||
assert call_kwargs["type"] == FileType.IMAGE
|
||||
assert call_kwargs["transfer_method"] == FileTransferMethod.TOOL_FILE
|
||||
assert call_kwargs["belongs_to"] == "assistant"
|
||||
assert call_kwargs["created_by"] == mock_user_id
|
||||
|
||||
file_session.add.assert_called_once_with(mock_message_file)
|
||||
file_session.flush.assert_called_once()
|
||||
assert message_file_id == mock_message_file.id
|
||||
mock_queue_manager.publish.assert_not_called()
|
||||
mock_mgr.create_file_by_url.assert_called_once_with(
|
||||
user_id=mock_user_id,
|
||||
tenant_id=mock_tenant_id,
|
||||
file_url=image_url,
|
||||
conversation_id=None,
|
||||
)
|
||||
message_file = sqlite_session.get(MessageFile, message_file_id)
|
||||
assert message_file is not None
|
||||
assert message_file.message_id == mock_message_id
|
||||
assert message_file.type == FileType.IMAGE
|
||||
assert message_file.transfer_method == FileTransferMethod.TOOL_FILE
|
||||
assert message_file.belongs_to == "assistant"
|
||||
assert message_file.created_by == mock_user_id
|
||||
assert message_file.upload_file_id == tool_file.id
|
||||
mock_queue_manager.publish.assert_not_called()
|
||||
|
||||
def test_handle_multimodal_image_content_with_base64(
|
||||
self,
|
||||
@ -121,8 +110,8 @@ class TestBaseAppRunnerMultimodal:
|
||||
mock_tenant_id,
|
||||
mock_message_id,
|
||||
mock_queue_manager,
|
||||
mock_tool_file,
|
||||
mock_message_file,
|
||||
tool_file,
|
||||
sqlite_session: Session,
|
||||
):
|
||||
"""Test handling image from base64 data."""
|
||||
# Arrange
|
||||
@ -141,41 +130,29 @@ class TestBaseAppRunnerMultimodal:
|
||||
with patch("core.app.apps.base_app_runner.ToolFileManager", autospec=True) as mock_mgr_class:
|
||||
# Setup mock tool file manager
|
||||
mock_mgr = MagicMock()
|
||||
mock_mgr.create_file_by_raw.return_value = mock_tool_file
|
||||
mock_mgr.create_file_by_raw.return_value = tool_file
|
||||
mock_mgr_class.return_value = mock_mgr
|
||||
|
||||
with patch("core.app.apps.base_app_runner.MessageFile", autospec=True) as mock_msg_file_class:
|
||||
mock_msg_file_class.return_value = mock_message_file
|
||||
message_file_id = AppRunner()._handle_multimodal_image_content(
|
||||
session=sqlite_session,
|
||||
content=content,
|
||||
message_id=mock_message_id,
|
||||
user_id=mock_user_id,
|
||||
tenant_id=mock_tenant_id,
|
||||
queue_manager=mock_queue_manager,
|
||||
)
|
||||
|
||||
file_session = MagicMock()
|
||||
runner = MagicMock()
|
||||
method = AppRunner._handle_multimodal_image_content
|
||||
runner._handle_multimodal_image_content = lambda *args, **kwargs: method(runner, *args, **kwargs)
|
||||
|
||||
message_file_id = runner._handle_multimodal_image_content(
|
||||
session=file_session,
|
||||
content=content,
|
||||
message_id=mock_message_id,
|
||||
user_id=mock_user_id,
|
||||
tenant_id=mock_tenant_id,
|
||||
queue_manager=mock_queue_manager,
|
||||
)
|
||||
|
||||
mock_mgr.create_file_by_raw.assert_called_once()
|
||||
call_kwargs = mock_mgr.create_file_by_raw.call_args[1]
|
||||
assert call_kwargs["user_id"] == mock_user_id
|
||||
assert call_kwargs["tenant_id"] == mock_tenant_id
|
||||
assert call_kwargs["conversation_id"] is None
|
||||
assert "file_binary" in call_kwargs
|
||||
assert call_kwargs["mimetype"] == "image/png"
|
||||
assert call_kwargs["filename"].startswith("generated_image")
|
||||
assert call_kwargs["filename"].endswith(".png")
|
||||
|
||||
mock_msg_file_class.assert_called_once()
|
||||
file_session.add.assert_called_once()
|
||||
file_session.flush.assert_called_once()
|
||||
assert message_file_id == mock_message_file.id
|
||||
mock_queue_manager.publish.assert_not_called()
|
||||
mock_mgr.create_file_by_raw.assert_called_once()
|
||||
call_kwargs = mock_mgr.create_file_by_raw.call_args[1]
|
||||
assert call_kwargs["user_id"] == mock_user_id
|
||||
assert call_kwargs["tenant_id"] == mock_tenant_id
|
||||
assert call_kwargs["conversation_id"] is None
|
||||
assert "file_binary" in call_kwargs
|
||||
assert call_kwargs["mimetype"] == "image/png"
|
||||
assert call_kwargs["filename"].startswith("generated_image")
|
||||
assert call_kwargs["filename"].endswith(".png")
|
||||
assert sqlite_session.get(MessageFile, message_file_id) is not None
|
||||
mock_queue_manager.publish.assert_not_called()
|
||||
|
||||
def test_handle_multimodal_image_content_with_base64_data_uri(
|
||||
self,
|
||||
@ -183,8 +160,8 @@ class TestBaseAppRunnerMultimodal:
|
||||
mock_tenant_id,
|
||||
mock_message_id,
|
||||
mock_queue_manager,
|
||||
mock_tool_file,
|
||||
mock_message_file,
|
||||
tool_file,
|
||||
sqlite_session: Session,
|
||||
):
|
||||
"""Test handling image from base64 data with URI prefix."""
|
||||
# Arrange
|
||||
@ -201,29 +178,22 @@ class TestBaseAppRunnerMultimodal:
|
||||
with patch("core.app.apps.base_app_runner.ToolFileManager", autospec=True) as mock_mgr_class:
|
||||
# Setup mock tool file manager
|
||||
mock_mgr = MagicMock()
|
||||
mock_mgr.create_file_by_raw.return_value = mock_tool_file
|
||||
mock_mgr.create_file_by_raw.return_value = tool_file
|
||||
mock_mgr_class.return_value = mock_mgr
|
||||
|
||||
with patch("core.app.apps.base_app_runner.MessageFile", autospec=True) as mock_msg_file_class:
|
||||
mock_msg_file_class.return_value = mock_message_file
|
||||
message_file_id = AppRunner()._handle_multimodal_image_content(
|
||||
session=sqlite_session,
|
||||
content=content,
|
||||
message_id=mock_message_id,
|
||||
user_id=mock_user_id,
|
||||
tenant_id=mock_tenant_id,
|
||||
queue_manager=mock_queue_manager,
|
||||
)
|
||||
|
||||
file_session = MagicMock()
|
||||
runner = MagicMock()
|
||||
method = AppRunner._handle_multimodal_image_content
|
||||
runner._handle_multimodal_image_content = lambda *args, **kwargs: method(runner, *args, **kwargs)
|
||||
|
||||
runner._handle_multimodal_image_content(
|
||||
session=file_session,
|
||||
content=content,
|
||||
message_id=mock_message_id,
|
||||
user_id=mock_user_id,
|
||||
tenant_id=mock_tenant_id,
|
||||
queue_manager=mock_queue_manager,
|
||||
)
|
||||
|
||||
mock_mgr.create_file_by_raw.assert_called_once()
|
||||
call_kwargs = mock_mgr.create_file_by_raw.call_args[1]
|
||||
assert "file_binary" in call_kwargs
|
||||
mock_mgr.create_file_by_raw.assert_called_once()
|
||||
call_kwargs = mock_mgr.create_file_by_raw.call_args[1]
|
||||
assert "file_binary" in call_kwargs
|
||||
assert sqlite_session.get(MessageFile, message_file_id) is not None
|
||||
|
||||
def test_handle_multimodal_image_content_without_url_or_base64(
|
||||
self,
|
||||
@ -231,6 +201,7 @@ class TestBaseAppRunnerMultimodal:
|
||||
mock_tenant_id,
|
||||
mock_message_id,
|
||||
mock_queue_manager,
|
||||
sqlite_session: Session,
|
||||
):
|
||||
"""Test handling image content without URL or base64 data."""
|
||||
# Arrange
|
||||
@ -242,24 +213,19 @@ class TestBaseAppRunnerMultimodal:
|
||||
)
|
||||
|
||||
with patch("core.app.apps.base_app_runner.ToolFileManager", autospec=True) as mock_mgr_class:
|
||||
with patch("core.app.apps.base_app_runner.MessageFile", autospec=True) as mock_msg_file_class:
|
||||
file_session = MagicMock()
|
||||
runner = MagicMock()
|
||||
method = AppRunner._handle_multimodal_image_content
|
||||
runner._handle_multimodal_image_content = lambda *args, **kwargs: method(runner, *args, **kwargs)
|
||||
result = AppRunner()._handle_multimodal_image_content(
|
||||
session=sqlite_session,
|
||||
content=content,
|
||||
message_id=mock_message_id,
|
||||
user_id=mock_user_id,
|
||||
tenant_id=mock_tenant_id,
|
||||
queue_manager=mock_queue_manager,
|
||||
)
|
||||
|
||||
runner._handle_multimodal_image_content(
|
||||
session=file_session,
|
||||
content=content,
|
||||
message_id=mock_message_id,
|
||||
user_id=mock_user_id,
|
||||
tenant_id=mock_tenant_id,
|
||||
queue_manager=mock_queue_manager,
|
||||
)
|
||||
|
||||
mock_mgr_class.assert_not_called()
|
||||
mock_msg_file_class.assert_not_called()
|
||||
mock_queue_manager.publish.assert_not_called()
|
||||
assert result is None
|
||||
assert sqlite_session.scalar(select(func.count()).select_from(MessageFile)) == 0
|
||||
mock_mgr_class.assert_not_called()
|
||||
mock_queue_manager.publish.assert_not_called()
|
||||
|
||||
def test_handle_multimodal_image_content_with_error(
|
||||
self,
|
||||
@ -267,6 +233,7 @@ class TestBaseAppRunnerMultimodal:
|
||||
mock_tenant_id,
|
||||
mock_message_id,
|
||||
mock_queue_manager,
|
||||
sqlite_session: Session,
|
||||
):
|
||||
"""Test handling image content when an error occurs."""
|
||||
# Arrange
|
||||
@ -282,23 +249,18 @@ class TestBaseAppRunnerMultimodal:
|
||||
mock_mgr.create_file_by_url.side_effect = Exception("Network error")
|
||||
mock_mgr_class.return_value = mock_mgr
|
||||
|
||||
with patch("core.app.apps.base_app_runner.MessageFile", autospec=True) as mock_msg_file_class:
|
||||
file_session = MagicMock()
|
||||
runner = MagicMock()
|
||||
method = AppRunner._handle_multimodal_image_content
|
||||
runner._handle_multimodal_image_content = lambda *args, **kwargs: method(runner, *args, **kwargs)
|
||||
result = AppRunner()._handle_multimodal_image_content(
|
||||
session=sqlite_session,
|
||||
content=content,
|
||||
message_id=mock_message_id,
|
||||
user_id=mock_user_id,
|
||||
tenant_id=mock_tenant_id,
|
||||
queue_manager=mock_queue_manager,
|
||||
)
|
||||
|
||||
runner._handle_multimodal_image_content(
|
||||
session=file_session,
|
||||
content=content,
|
||||
message_id=mock_message_id,
|
||||
user_id=mock_user_id,
|
||||
tenant_id=mock_tenant_id,
|
||||
queue_manager=mock_queue_manager,
|
||||
)
|
||||
|
||||
mock_msg_file_class.assert_not_called()
|
||||
mock_queue_manager.publish.assert_not_called()
|
||||
assert result is None
|
||||
assert sqlite_session.scalar(select(func.count()).select_from(MessageFile)) == 0
|
||||
mock_queue_manager.publish.assert_not_called()
|
||||
|
||||
def test_handle_multimodal_image_content_debugger_mode(
|
||||
self,
|
||||
@ -306,8 +268,8 @@ class TestBaseAppRunnerMultimodal:
|
||||
mock_tenant_id,
|
||||
mock_message_id,
|
||||
mock_queue_manager,
|
||||
mock_tool_file,
|
||||
mock_message_file,
|
||||
tool_file,
|
||||
sqlite_session: Session,
|
||||
):
|
||||
"""Test that debugger mode sets correct created_by_role."""
|
||||
# Arrange
|
||||
@ -321,28 +283,21 @@ class TestBaseAppRunnerMultimodal:
|
||||
|
||||
with patch("core.app.apps.base_app_runner.ToolFileManager", autospec=True) as mock_mgr_class:
|
||||
mock_mgr = MagicMock()
|
||||
mock_mgr.create_file_by_url.return_value = mock_tool_file
|
||||
mock_mgr.create_file_by_url.return_value = tool_file
|
||||
mock_mgr_class.return_value = mock_mgr
|
||||
|
||||
with patch("core.app.apps.base_app_runner.MessageFile", autospec=True) as mock_msg_file_class:
|
||||
mock_msg_file_class.return_value = mock_message_file
|
||||
message_file_id = AppRunner()._handle_multimodal_image_content(
|
||||
session=sqlite_session,
|
||||
content=content,
|
||||
message_id=mock_message_id,
|
||||
user_id=mock_user_id,
|
||||
tenant_id=mock_tenant_id,
|
||||
queue_manager=mock_queue_manager,
|
||||
)
|
||||
|
||||
file_session = MagicMock()
|
||||
runner = MagicMock()
|
||||
method = AppRunner._handle_multimodal_image_content
|
||||
runner._handle_multimodal_image_content = lambda *args, **kwargs: method(runner, *args, **kwargs)
|
||||
|
||||
runner._handle_multimodal_image_content(
|
||||
session=file_session,
|
||||
content=content,
|
||||
message_id=mock_message_id,
|
||||
user_id=mock_user_id,
|
||||
tenant_id=mock_tenant_id,
|
||||
queue_manager=mock_queue_manager,
|
||||
)
|
||||
|
||||
call_kwargs = mock_msg_file_class.call_args[1]
|
||||
assert call_kwargs["created_by_role"] == CreatorUserRole.ACCOUNT
|
||||
message_file = sqlite_session.get(MessageFile, message_file_id)
|
||||
assert message_file is not None
|
||||
assert message_file.created_by_role == CreatorUserRole.ACCOUNT
|
||||
|
||||
def test_handle_multimodal_image_content_service_api_mode(
|
||||
self,
|
||||
@ -350,8 +305,8 @@ class TestBaseAppRunnerMultimodal:
|
||||
mock_tenant_id,
|
||||
mock_message_id,
|
||||
mock_queue_manager,
|
||||
mock_tool_file,
|
||||
mock_message_file,
|
||||
tool_file,
|
||||
sqlite_session: Session,
|
||||
):
|
||||
"""Test that service API mode sets correct created_by_role."""
|
||||
# Arrange
|
||||
@ -365,25 +320,18 @@ class TestBaseAppRunnerMultimodal:
|
||||
|
||||
with patch("core.app.apps.base_app_runner.ToolFileManager", autospec=True) as mock_mgr_class:
|
||||
mock_mgr = MagicMock()
|
||||
mock_mgr.create_file_by_url.return_value = mock_tool_file
|
||||
mock_mgr.create_file_by_url.return_value = tool_file
|
||||
mock_mgr_class.return_value = mock_mgr
|
||||
|
||||
with patch("core.app.apps.base_app_runner.MessageFile", autospec=True) as mock_msg_file_class:
|
||||
mock_msg_file_class.return_value = mock_message_file
|
||||
message_file_id = AppRunner()._handle_multimodal_image_content(
|
||||
session=sqlite_session,
|
||||
content=content,
|
||||
message_id=mock_message_id,
|
||||
user_id=mock_user_id,
|
||||
tenant_id=mock_tenant_id,
|
||||
queue_manager=mock_queue_manager,
|
||||
)
|
||||
|
||||
file_session = MagicMock()
|
||||
runner = MagicMock()
|
||||
method = AppRunner._handle_multimodal_image_content
|
||||
runner._handle_multimodal_image_content = lambda *args, **kwargs: method(runner, *args, **kwargs)
|
||||
|
||||
runner._handle_multimodal_image_content(
|
||||
session=file_session,
|
||||
content=content,
|
||||
message_id=mock_message_id,
|
||||
user_id=mock_user_id,
|
||||
tenant_id=mock_tenant_id,
|
||||
queue_manager=mock_queue_manager,
|
||||
)
|
||||
|
||||
call_kwargs = mock_msg_file_class.call_args[1]
|
||||
assert call_kwargs["created_by_role"] == CreatorUserRole.END_USER
|
||||
message_file = sqlite_session.get(MessageFile, message_file_id)
|
||||
assert message_file is not None
|
||||
assert message_file.created_by_role == CreatorUserRole.END_USER
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import threading
|
||||
from collections.abc import Callable
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import patch
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from opentelemetry.trace import StatusCode, get_current_span, get_tracer
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.rag.rerank.rerank_type import RerankMode
|
||||
from core.rag.retrieval.dataset_retrieval import DatasetRetrieval
|
||||
@ -20,6 +21,7 @@ def _otel_enabled(config_overrides: Callable[..., None]) -> None:
|
||||
def test_knowledge_retrieval_creates_a_child_otel_span(
|
||||
memory_span_exporter,
|
||||
tracer_provider_with_memory_exporter,
|
||||
sqlite_session: Session,
|
||||
) -> None:
|
||||
"""The retrieval entry point must be visible beneath its workflow node span."""
|
||||
request = KnowledgeRetrievalRequest(
|
||||
@ -38,7 +40,7 @@ def test_knowledge_retrieval_creates_a_child_otel_span(
|
||||
patch.object(retrieval, "_get_available_datasets", return_value=[]),
|
||||
get_tracer(__name__).start_as_current_span("knowledge-retrieval-node") as node_span,
|
||||
):
|
||||
assert retrieval.knowledge_retrieval(MagicMock(), request) == []
|
||||
assert retrieval.knowledge_retrieval(sqlite_session, request) == []
|
||||
|
||||
retrieval_span = next(
|
||||
span
|
||||
@ -101,7 +103,6 @@ def test_retriever_thread_exception_sets_error_span_and_is_collected(
|
||||
expected_error = RuntimeError("retrieval failed")
|
||||
|
||||
with (
|
||||
patch("core.rag.retrieval.dataset_retrieval.session_factory.create_session"),
|
||||
patch.object(retrieval, "_retriever", side_effect=expected_error),
|
||||
):
|
||||
retrieval._run_retriever_thread_safely(
|
||||
@ -139,7 +140,6 @@ def test_retriever_thread_exception_emits_skip_event_when_requested(
|
||||
dataset_id = str(uuid4())
|
||||
|
||||
with (
|
||||
patch("core.rag.retrieval.dataset_retrieval.session_factory.create_session"),
|
||||
patch.object(retrieval, "_retriever", side_effect=expected_error),
|
||||
get_tracer(__name__).start_as_current_span("dataset-retrieval-parent") as parent_span,
|
||||
):
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import types
|
||||
from unittest.mock import Mock, create_autospec
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from redis.exceptions import LockNotOwnedError
|
||||
@ -203,19 +203,48 @@ def test_add_segment_ignores_lock_not_owned(
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sqlite_session", [(Account, Tenant, Dataset, Document, DocumentSegment)], indirect=True)
|
||||
def test_multi_create_segment_ignores_lock_not_owned(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
fake_current_user,
|
||||
fake_lock,
|
||||
sqlite_session: Session,
|
||||
):
|
||||
# Arrange
|
||||
dataset = create_autospec(Dataset, instance=True)
|
||||
dataset.id = "ds-1"
|
||||
dataset.tenant_id = fake_current_user.current_tenant_id
|
||||
dataset.indexing_technique = IndexTechniqueType.ECONOMY # again, skip high_quality path
|
||||
dataset = Dataset(
|
||||
id=DATASET_ID,
|
||||
tenant_id=TENANT_ID,
|
||||
name="Test Dataset",
|
||||
description="",
|
||||
created_by=USER_ID,
|
||||
indexing_technique=IndexTechniqueType.ECONOMY,
|
||||
)
|
||||
document = Document(
|
||||
id=DOCUMENT_ID,
|
||||
tenant_id=TENANT_ID,
|
||||
dataset_id=DATASET_ID,
|
||||
position=1,
|
||||
data_source_type="upload_file",
|
||||
data_source_info="{}",
|
||||
batch="batch-1",
|
||||
name="Test Document",
|
||||
created_from="web",
|
||||
created_by=USER_ID,
|
||||
word_count=0,
|
||||
doc_form=IndexStructureType.QA_INDEX,
|
||||
)
|
||||
sqlite_session.add_all([fake_current_user._current_tenant, fake_current_user, dataset, document])
|
||||
sqlite_session.commit()
|
||||
|
||||
document = create_autospec(Document, instance=True)
|
||||
document.id = "doc-1"
|
||||
document.dataset_id = dataset.id
|
||||
document.word_count = 0
|
||||
document.doc_form = IndexStructureType.QA_INDEX
|
||||
result = SegmentService.multi_create_segment(
|
||||
segments=[{"content": "question", "answer": "answer", "keywords": ["key"]}],
|
||||
document=document,
|
||||
dataset=dataset,
|
||||
session=sqlite_session,
|
||||
)
|
||||
|
||||
assert result is None
|
||||
assert not sqlite_session.in_transaction()
|
||||
assert sqlite_session.scalar(select(func.count(DocumentSegment.id))) == 0
|
||||
sqlite_session.refresh(document)
|
||||
assert document.word_count == 0
|
||||
|
||||
@ -1,154 +1,260 @@
|
||||
"""Unit tests for the ``resume_agent_app_execution`` celery task (ENG-635).
|
||||
|
||||
Every DB access (``db.session.get``) and the generator are patched at the module
|
||||
level, so the task's branch logic is exercised without a database or live stack.
|
||||
"""
|
||||
"""Unit tests for the ``resume_agent_app_execution`` Celery task (ENG-635)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
from collections.abc import Iterator
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
from sqlalchemy.orm import Session, scoped_session, sessionmaker
|
||||
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from models.account import Account
|
||||
from core.workflow.nodes.human_input.enums import HumanInputFormKind, HumanInputFormStatus
|
||||
from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole
|
||||
from models.enums import ConversationFromSource, EndUserType
|
||||
from models.enums import InvokeFrom as StoredInvokeFrom
|
||||
from models.human_input import HumanInputForm
|
||||
from models.model import App, Conversation, EndUser
|
||||
from models.model import App, AppMode, Conversation, EndUser
|
||||
from tasks.app_generate import resume_agent_app_task as mod
|
||||
|
||||
MODULE = "tasks.app_generate.resume_agent_app_task"
|
||||
|
||||
|
||||
def _form(conversation_id: str = "conv-1", app_id: str = "app-1") -> MagicMock:
|
||||
return MagicMock(conversation_id=conversation_id, app_id=app_id)
|
||||
@pytest.fixture
|
||||
def task_session(mocker: MockerFixture, sqlite_session_factory: sessionmaker[Session]) -> Iterator[Session]:
|
||||
"""Bind the task's Flask-SQLAlchemy session proxy to the shared SQLite database."""
|
||||
registry = scoped_session(sqlite_session_factory)
|
||||
mocker.patch.object(mod.db, "session", registry)
|
||||
session = registry()
|
||||
yield session
|
||||
registry.remove()
|
||||
|
||||
|
||||
def _wire_db(
|
||||
mocker: MockerFixture,
|
||||
def _app(*, app_id: str, tenant_id: str) -> App:
|
||||
return App(
|
||||
id=app_id,
|
||||
tenant_id=tenant_id,
|
||||
name="Agent app",
|
||||
description="",
|
||||
mode=AppMode.AGENT_CHAT,
|
||||
icon_type=None,
|
||||
icon=None,
|
||||
icon_background=None,
|
||||
enable_site=False,
|
||||
enable_api=False,
|
||||
)
|
||||
|
||||
|
||||
def _conversation(
|
||||
*,
|
||||
form=None,
|
||||
app=None,
|
||||
conversation=None,
|
||||
account=None,
|
||||
end_user=None,
|
||||
) -> MagicMock:
|
||||
"""Patch the module ``db`` so ``db.session.get(Model, id)`` dispatches by model."""
|
||||
table = {
|
||||
HumanInputForm: form,
|
||||
App: app,
|
||||
Conversation: conversation,
|
||||
Account: account,
|
||||
EndUser: end_user,
|
||||
}
|
||||
db = mocker.patch(f"{MODULE}.db")
|
||||
db.session.get.side_effect = lambda model, _id: table.get(model)
|
||||
return db
|
||||
conversation_id: str,
|
||||
app_id: str,
|
||||
account_id: str | None = None,
|
||||
end_user_id: str | None = None,
|
||||
invoke_from: StoredInvokeFrom = StoredInvokeFrom.WEB_APP,
|
||||
) -> Conversation:
|
||||
return Conversation(
|
||||
id=conversation_id,
|
||||
app_id=app_id,
|
||||
mode=AppMode.AGENT_CHAT,
|
||||
name="Agent conversation",
|
||||
inputs={},
|
||||
invoke_from=invoke_from,
|
||||
from_source=ConversationFromSource.API,
|
||||
from_account_id=account_id,
|
||||
from_end_user_id=end_user_id,
|
||||
)
|
||||
|
||||
|
||||
def test_resume_happy_path_account_user_sets_tenant_and_runs(mocker: MockerFixture):
|
||||
conversation = MagicMock(from_account_id="acct-1", from_end_user_id=None, invoke_from=InvokeFrom.WEB_APP)
|
||||
account = MagicMock()
|
||||
app = MagicMock(tenant_id="tenant-1")
|
||||
db = _wire_db(mocker, form=_form(), app=app, conversation=conversation, account=account)
|
||||
gen = mocker.patch(f"{MODULE}.AgentAppGenerator")
|
||||
|
||||
mod.resume_agent_app_execution(conversation_id="conv-1", form_id="form-1")
|
||||
|
||||
account.set_tenant_id_with_session.assert_called_once_with("tenant-1", session=db.session.return_value)
|
||||
gen.return_value.resume_after_form_submission.assert_called_once()
|
||||
kwargs = gen.return_value.resume_after_form_submission.call_args.kwargs
|
||||
assert kwargs["conversation_id"] == "conv-1"
|
||||
assert kwargs["form_id"] == "form-1"
|
||||
assert kwargs["user"] is account
|
||||
assert kwargs["app_model"] is app
|
||||
assert kwargs["invoke_from"] == InvokeFrom.WEB_APP
|
||||
assert kwargs["session"] is db.session.return_value
|
||||
def _form(*, form_id: str, conversation_id: str, app_id: str) -> HumanInputForm:
|
||||
return HumanInputForm(
|
||||
id=form_id,
|
||||
tenant_id=str(uuid4()),
|
||||
app_id=app_id,
|
||||
workflow_run_id=None,
|
||||
conversation_id=conversation_id,
|
||||
form_kind=HumanInputFormKind.RUNTIME,
|
||||
node_id="ask-human",
|
||||
form_definition="{}",
|
||||
rendered_content="Question",
|
||||
status=HumanInputFormStatus.WAITING,
|
||||
expiration_time=datetime.now(UTC).replace(tzinfo=None) + timedelta(hours=1),
|
||||
)
|
||||
|
||||
|
||||
def test_resume_end_user_path(mocker: MockerFixture):
|
||||
conversation = MagicMock(from_account_id=None, from_end_user_id="eu-1", invoke_from=InvokeFrom.WEB_APP)
|
||||
end_user = MagicMock()
|
||||
_wire_db(mocker, form=_form(), app=MagicMock(tenant_id="t"), conversation=conversation, end_user=end_user)
|
||||
gen = mocker.patch(f"{MODULE}.AgentAppGenerator")
|
||||
|
||||
mod.resume_agent_app_execution(conversation_id="conv-1", form_id="form-1")
|
||||
|
||||
assert gen.return_value.resume_after_form_submission.call_args.kwargs["user"] is end_user
|
||||
def _seed_account(session: Session, *, tenant_id: str, account_id: str) -> Account:
|
||||
tenant = Tenant(name="Tenant")
|
||||
tenant.id = tenant_id
|
||||
account = Account(name="Account", email="account@example.com")
|
||||
account.id = account_id
|
||||
join = TenantAccountJoin(
|
||||
tenant_id=tenant_id,
|
||||
account_id=account_id,
|
||||
current=True,
|
||||
role=TenantAccountRole.NORMAL,
|
||||
)
|
||||
session.add_all([tenant, account, join])
|
||||
return account
|
||||
|
||||
|
||||
def test_resume_preserves_debugger_invoke_from(mocker: MockerFixture):
|
||||
conversation = MagicMock(from_account_id="acct-1", from_end_user_id=None, invoke_from=InvokeFrom.DEBUGGER)
|
||||
account = MagicMock()
|
||||
app = MagicMock(tenant_id="tenant-1")
|
||||
_wire_db(mocker, form=_form(), app=app, conversation=conversation, account=account)
|
||||
gen = mocker.patch(f"{MODULE}.AgentAppGenerator")
|
||||
def test_resume_happy_path_account_user_sets_tenant_and_runs(mocker: MockerFixture, task_session: Session) -> None:
|
||||
tenant_id, app_id, conversation_id, form_id, account_id = (str(uuid4()) for _ in range(5))
|
||||
app = _app(app_id=app_id, tenant_id=tenant_id)
|
||||
account = _seed_account(task_session, tenant_id=tenant_id, account_id=account_id)
|
||||
conversation = _conversation(conversation_id=conversation_id, app_id=app_id, account_id=account_id)
|
||||
task_session.add_all([app, conversation, _form(form_id=form_id, conversation_id=conversation_id, app_id=app_id)])
|
||||
task_session.commit()
|
||||
generator = mocker.patch(f"{MODULE}.AgentAppGenerator")
|
||||
|
||||
mod.resume_agent_app_execution(conversation_id="conv-1", form_id="form-1")
|
||||
mod.resume_agent_app_execution(conversation_id=conversation_id, form_id=form_id)
|
||||
|
||||
assert gen.return_value.resume_after_form_submission.call_args.kwargs["invoke_from"] == InvokeFrom.DEBUGGER
|
||||
call = generator.return_value.resume_after_form_submission.call_args
|
||||
assert call is not None
|
||||
assert call.kwargs["conversation_id"] == conversation_id
|
||||
assert call.kwargs["form_id"] == form_id
|
||||
assert call.kwargs["user"] is account
|
||||
assert call.kwargs["app_model"] is app
|
||||
assert call.kwargs["invoke_from"] == InvokeFrom.WEB_APP
|
||||
assert isinstance(call.kwargs["session"], Session)
|
||||
assert account.current_tenant_id == tenant_id
|
||||
|
||||
|
||||
def test_resume_returns_when_form_missing(mocker: MockerFixture):
|
||||
_wire_db(mocker, form=None)
|
||||
gen = mocker.patch(f"{MODULE}.AgentAppGenerator")
|
||||
def test_resume_end_user_path(mocker: MockerFixture, task_session: Session) -> None:
|
||||
tenant_id, app_id, conversation_id, form_id, end_user_id = (str(uuid4()) for _ in range(5))
|
||||
app = _app(app_id=app_id, tenant_id=tenant_id)
|
||||
end_user = EndUser(
|
||||
id=end_user_id,
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
type=EndUserType.BROWSER,
|
||||
name="End user",
|
||||
session_id="browser-session",
|
||||
)
|
||||
task_session.add_all(
|
||||
[
|
||||
app,
|
||||
end_user,
|
||||
_conversation(conversation_id=conversation_id, app_id=app_id, end_user_id=end_user_id),
|
||||
_form(form_id=form_id, conversation_id=conversation_id, app_id=app_id),
|
||||
]
|
||||
)
|
||||
task_session.commit()
|
||||
generator = mocker.patch(f"{MODULE}.AgentAppGenerator")
|
||||
|
||||
mod.resume_agent_app_execution(conversation_id="conv-1", form_id="form-1")
|
||||
mod.resume_agent_app_execution(conversation_id=conversation_id, form_id=form_id)
|
||||
|
||||
gen.assert_not_called()
|
||||
assert generator.return_value.resume_after_form_submission.call_args.kwargs["user"] is end_user
|
||||
|
||||
|
||||
def test_resume_returns_on_conversation_mismatch(mocker: MockerFixture):
|
||||
_wire_db(mocker, form=_form(conversation_id="other-conv"))
|
||||
gen = mocker.patch(f"{MODULE}.AgentAppGenerator")
|
||||
def test_resume_preserves_debugger_invoke_from(mocker: MockerFixture, task_session: Session) -> None:
|
||||
tenant_id, app_id, conversation_id, form_id, account_id = (str(uuid4()) for _ in range(5))
|
||||
app = _app(app_id=app_id, tenant_id=tenant_id)
|
||||
_seed_account(task_session, tenant_id=tenant_id, account_id=account_id)
|
||||
task_session.add_all(
|
||||
[
|
||||
app,
|
||||
_conversation(
|
||||
conversation_id=conversation_id,
|
||||
app_id=app_id,
|
||||
account_id=account_id,
|
||||
invoke_from=StoredInvokeFrom.DEBUGGER,
|
||||
),
|
||||
_form(form_id=form_id, conversation_id=conversation_id, app_id=app_id),
|
||||
]
|
||||
)
|
||||
task_session.commit()
|
||||
generator = mocker.patch(f"{MODULE}.AgentAppGenerator")
|
||||
|
||||
mod.resume_agent_app_execution(conversation_id="conv-1", form_id="form-1")
|
||||
mod.resume_agent_app_execution(conversation_id=conversation_id, form_id=form_id)
|
||||
|
||||
gen.assert_not_called()
|
||||
assert generator.return_value.resume_after_form_submission.call_args.kwargs["invoke_from"] == InvokeFrom.DEBUGGER
|
||||
|
||||
|
||||
def test_resume_returns_when_app_missing(mocker: MockerFixture):
|
||||
_wire_db(mocker, form=_form(), app=None)
|
||||
gen = mocker.patch(f"{MODULE}.AgentAppGenerator")
|
||||
|
||||
mod.resume_agent_app_execution(conversation_id="conv-1", form_id="form-1")
|
||||
|
||||
gen.assert_not_called()
|
||||
@pytest.mark.usefixtures("task_session")
|
||||
def test_resume_returns_when_form_missing(mocker: MockerFixture) -> None:
|
||||
generator = mocker.patch(f"{MODULE}.AgentAppGenerator")
|
||||
mod.resume_agent_app_execution(conversation_id=str(uuid4()), form_id=str(uuid4()))
|
||||
generator.assert_not_called()
|
||||
|
||||
|
||||
def test_resume_returns_when_conversation_missing(mocker: MockerFixture):
|
||||
_wire_db(mocker, form=_form(), app=MagicMock(), conversation=None)
|
||||
gen = mocker.patch(f"{MODULE}.AgentAppGenerator")
|
||||
|
||||
mod.resume_agent_app_execution(conversation_id="conv-1", form_id="form-1")
|
||||
|
||||
gen.assert_not_called()
|
||||
def test_resume_returns_on_conversation_mismatch(mocker: MockerFixture, task_session: Session) -> None:
|
||||
app_id, form_id = str(uuid4()), str(uuid4())
|
||||
task_session.add(_form(form_id=form_id, conversation_id=str(uuid4()), app_id=app_id))
|
||||
task_session.commit()
|
||||
generator = mocker.patch(f"{MODULE}.AgentAppGenerator")
|
||||
mod.resume_agent_app_execution(conversation_id=str(uuid4()), form_id=form_id)
|
||||
generator.assert_not_called()
|
||||
|
||||
|
||||
def test_resume_returns_when_no_user_resolvable(mocker: MockerFixture):
|
||||
conversation = MagicMock(from_account_id=None, from_end_user_id=None, invoke_from=InvokeFrom.WEB_APP)
|
||||
_wire_db(mocker, form=_form(), app=MagicMock(), conversation=conversation)
|
||||
gen = mocker.patch(f"{MODULE}.AgentAppGenerator")
|
||||
|
||||
mod.resume_agent_app_execution(conversation_id="conv-1", form_id="form-1")
|
||||
|
||||
gen.assert_not_called()
|
||||
def test_resume_returns_when_app_missing(mocker: MockerFixture, task_session: Session) -> None:
|
||||
conversation_id, form_id = str(uuid4()), str(uuid4())
|
||||
task_session.add(_form(form_id=form_id, conversation_id=conversation_id, app_id=str(uuid4())))
|
||||
task_session.commit()
|
||||
generator = mocker.patch(f"{MODULE}.AgentAppGenerator")
|
||||
mod.resume_agent_app_execution(conversation_id=conversation_id, form_id=form_id)
|
||||
generator.assert_not_called()
|
||||
|
||||
|
||||
def test_resume_returns_when_account_id_set_but_account_gone(mocker: MockerFixture):
|
||||
conversation = MagicMock(from_account_id="acct-x", from_end_user_id=None, invoke_from=InvokeFrom.WEB_APP)
|
||||
_wire_db(mocker, form=_form(), app=MagicMock(), conversation=conversation, account=None)
|
||||
gen = mocker.patch(f"{MODULE}.AgentAppGenerator")
|
||||
|
||||
mod.resume_agent_app_execution(conversation_id="conv-1", form_id="form-1")
|
||||
|
||||
gen.assert_not_called()
|
||||
def test_resume_returns_when_conversation_missing(mocker: MockerFixture, task_session: Session) -> None:
|
||||
tenant_id, app_id, conversation_id, form_id = (str(uuid4()) for _ in range(4))
|
||||
task_session.add_all(
|
||||
[
|
||||
_app(app_id=app_id, tenant_id=tenant_id),
|
||||
_form(form_id=form_id, conversation_id=conversation_id, app_id=app_id),
|
||||
]
|
||||
)
|
||||
task_session.commit()
|
||||
generator = mocker.patch(f"{MODULE}.AgentAppGenerator")
|
||||
mod.resume_agent_app_execution(conversation_id=conversation_id, form_id=form_id)
|
||||
generator.assert_not_called()
|
||||
|
||||
|
||||
def test_resume_swallows_generator_exception(mocker: MockerFixture):
|
||||
conversation = MagicMock(from_account_id="acct-1", from_end_user_id=None, invoke_from=InvokeFrom.WEB_APP)
|
||||
_wire_db(mocker, form=_form(), app=MagicMock(tenant_id="t"), conversation=conversation, account=MagicMock())
|
||||
gen = mocker.patch(f"{MODULE}.AgentAppGenerator")
|
||||
gen.return_value.resume_after_form_submission.side_effect = RuntimeError("boom")
|
||||
def test_resume_returns_when_no_user_resolvable(mocker: MockerFixture, task_session: Session) -> None:
|
||||
tenant_id, app_id, conversation_id, form_id = (str(uuid4()) for _ in range(4))
|
||||
task_session.add_all(
|
||||
[
|
||||
_app(app_id=app_id, tenant_id=tenant_id),
|
||||
_conversation(conversation_id=conversation_id, app_id=app_id),
|
||||
_form(form_id=form_id, conversation_id=conversation_id, app_id=app_id),
|
||||
]
|
||||
)
|
||||
task_session.commit()
|
||||
generator = mocker.patch(f"{MODULE}.AgentAppGenerator")
|
||||
mod.resume_agent_app_execution(conversation_id=conversation_id, form_id=form_id)
|
||||
generator.assert_not_called()
|
||||
|
||||
# The task must not propagate the failure (it is logged and the session closed).
|
||||
mod.resume_agent_app_execution(conversation_id="conv-1", form_id="form-1")
|
||||
|
||||
def test_resume_returns_when_account_id_set_but_account_gone(mocker: MockerFixture, task_session: Session) -> None:
|
||||
tenant_id, app_id, conversation_id, form_id = (str(uuid4()) for _ in range(4))
|
||||
task_session.add_all(
|
||||
[
|
||||
_app(app_id=app_id, tenant_id=tenant_id),
|
||||
_conversation(conversation_id=conversation_id, app_id=app_id, account_id=str(uuid4())),
|
||||
_form(form_id=form_id, conversation_id=conversation_id, app_id=app_id),
|
||||
]
|
||||
)
|
||||
task_session.commit()
|
||||
generator = mocker.patch(f"{MODULE}.AgentAppGenerator")
|
||||
mod.resume_agent_app_execution(conversation_id=conversation_id, form_id=form_id)
|
||||
generator.assert_not_called()
|
||||
|
||||
|
||||
def test_resume_swallows_generator_exception(mocker: MockerFixture, task_session: Session) -> None:
|
||||
tenant_id, app_id, conversation_id, form_id, account_id = (str(uuid4()) for _ in range(5))
|
||||
_seed_account(task_session, tenant_id=tenant_id, account_id=account_id)
|
||||
task_session.add_all(
|
||||
[
|
||||
_app(app_id=app_id, tenant_id=tenant_id),
|
||||
_conversation(conversation_id=conversation_id, app_id=app_id, account_id=account_id),
|
||||
_form(form_id=form_id, conversation_id=conversation_id, app_id=app_id),
|
||||
]
|
||||
)
|
||||
task_session.commit()
|
||||
generator = mocker.patch(f"{MODULE}.AgentAppGenerator")
|
||||
generator.return_value.resume_after_form_submission.side_effect = RuntimeError("boom")
|
||||
|
||||
mod.resume_agent_app_execution(conversation_id=conversation_id, form_id=form_id)
|
||||
|
||||
generator.return_value.resume_after_form_submission.assert_called_once()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user