test: migrate audit findings sessions and ORM models to SQLite (#40551)

This commit is contained in:
Asuka Minato 2026-08-31 05:55:38 +00:00 committed by GitHub
parent 9398482b53
commit f8f71fdc68
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 392 additions and 309 deletions

View File

@ -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

View File

@ -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,
):

View File

@ -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

View File

@ -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()