diff --git a/api/tests/unit_tests/controllers/service_api/app/test_annotation.py b/api/tests/unit_tests/controllers/service_api/app/test_annotation.py index b27c478ba6d..f149f462eba 100644 --- a/api/tests/unit_tests/controllers/service_api/app/test_annotation.py +++ b/api/tests/unit_tests/controllers/service_api/app/test_annotation.py @@ -14,13 +14,13 @@ Note: API endpoint tests for annotation controllers are complex due to: import uuid from inspect import unwrap -from types import SimpleNamespace -from unittest.mock import MagicMock, Mock +from unittest.mock import Mock import pytest from flask import Flask, request from flask_restx.api import HTTPStatus from pydantic import ValidationError +from sqlalchemy.orm import Session from controllers.service_api.app.annotation import ( AnnotationCreatePayload, @@ -32,10 +32,34 @@ from controllers.service_api.app.annotation import ( AnnotationUpdateDeleteApi, ) from extensions.ext_redis import redis_client -from models.model import App +from models.model import App, AppMode, MessageAnnotation from services.annotation_service import AppAnnotationService +def _app(app_id: str = "app", tenant_id: str = "tenant") -> App: + return App( + id=app_id, + tenant_id=tenant_id, + name="Service API app", + description="", + mode=AppMode.CHAT, + enable_site=True, + enable_api=True, + max_active_requests=0, + ) + + +def _annotation() -> MessageAnnotation: + annotation = MessageAnnotation( + app_id="app", + question="q", + content="a", + account_id="account", + ) + annotation.id = "a1" + return annotation + + class TestAnnotationCreatePayload: """Test suite for AnnotationCreatePayload Pydantic model.""" @@ -164,7 +188,7 @@ class TestAnnotationReplyActionApi: monkeypatch.setattr(AppAnnotationService, "enable_app_annotation", enable_mock) api = AnnotationReplyActionApi() handler = unwrap(api.post) - app_model = SimpleNamespace(id="app", tenant_id="tenant") + app_model = _app() with app.test_request_context( "/apps/annotation-reply/enable", method="POST", @@ -181,7 +205,7 @@ class TestAnnotationReplyActionApi: monkeypatch.setattr(AppAnnotationService, "disable_app_annotation", disable_mock) api = AnnotationReplyActionApi() handler = unwrap(api.post) - app_model = SimpleNamespace(id="app", tenant_id="tenant") + app_model = _app() with app.test_request_context( "/apps/annotation-reply/disable", method="POST", @@ -199,7 +223,7 @@ class TestAnnotationReplyActionStatusApi: monkeypatch.setattr(redis_client, "get", lambda *_args, **_kwargs: None) api = AnnotationReplyActionStatusApi() handler = unwrap(api.get) - app_model = SimpleNamespace(id="app") + app_model = _app() with pytest.raises(ValueError): handler(api, app_model=app_model, job_id="j1", action="enable") @@ -213,7 +237,7 @@ class TestAnnotationReplyActionStatusApi: monkeypatch.setattr(redis_client, "get", _get) api = AnnotationReplyActionStatusApi() handler = unwrap(api.get) - app_model = SimpleNamespace(id="app") + app_model = _app() response, status = handler(api, app_model=app_model, job_id="j1", action="enable") assert status == 200 assert response["job_status"] == "error" @@ -221,42 +245,47 @@ class TestAnnotationReplyActionStatusApi: class TestAnnotationListApi: - def test_get_uses_defaults(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: - annotation = SimpleNamespace(id="a1", question="q", content="a", created_at=0) + def test_get_uses_defaults(self, app: Flask, monkeypatch: pytest.MonkeyPatch, sqlite_session: Session) -> None: + annotation = _annotation() get_mock = Mock(return_value=([annotation], 1)) monkeypatch.setattr(AppAnnotationService, "get_annotation_list_by_app_id", get_mock) api = AnnotationListApi() handler = unwrap(api.get) - app_model = SimpleNamespace(id="app") + app_model = _app() with app.test_request_context("/apps/annotations", method="GET"): query = AnnotationListQuery.model_validate(request.args.to_dict(flat=True)) - response = handler(api, query, MagicMock(), app_model=app_model) + response = handler(api, query, sqlite_session, app_model=app_model) assert response["page"] == 1 assert response["limit"] == 20 session = get_mock.call_args.args[-1] - assert isinstance(session, MagicMock) + assert session is sqlite_session assert get_mock.call_args.args == ("app", 1, 20, "", session) - def test_get_accepts_valid_numeric_strings(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: - annotation = SimpleNamespace(id="a1", question="q", content="a", created_at=0) + def test_get_accepts_valid_numeric_strings( + self, app: Flask, monkeypatch: pytest.MonkeyPatch, sqlite_session: Session + ) -> None: + annotation = _annotation() get_mock = Mock(return_value=([annotation], 1)) monkeypatch.setattr(AppAnnotationService, "get_annotation_list_by_app_id", get_mock) api = AnnotationListApi() handler = unwrap(api.get) - app_model = SimpleNamespace(id="app") + app_model = _app() with app.test_request_context("/apps/annotations?page=2&limit=5&keyword=refund", method="GET"): query = AnnotationListQuery.model_validate(request.args.to_dict(flat=True)) - response = handler(api, query, MagicMock(), app_model=app_model) + response = handler(api, query, sqlite_session, app_model=app_model) assert response["total"] == 1 assert response["page"] == 2 assert response["limit"] == 5 session = get_mock.call_args.args[-1] - assert isinstance(session, MagicMock) + assert session is sqlite_session assert get_mock.call_args.args == ("app", 2, 5, "refund", session) @pytest.mark.parametrize("query_string", ["page=abc&limit=5", "page=1&limit=abc", "page=&limit=5", "limit=0"]) def test_get_rejects_invalid_explicit_pagination_value( - self, app: Flask, monkeypatch: pytest.MonkeyPatch, query_string: str + self, + app: Flask, + monkeypatch: pytest.MonkeyPatch, + query_string: str, ) -> None: get_mock = Mock(return_value=([], 0)) monkeypatch.setattr(AppAnnotationService, "get_annotation_list_by_app_id", get_mock) @@ -269,24 +298,24 @@ class TestAnnotationListApi: assert unwrap(api.get) is not api.get get_mock.assert_not_called() - def test_create(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: - annotation = SimpleNamespace(id="a1", question="q", content="a", created_at=0) + def test_create(self, app: Flask, monkeypatch: pytest.MonkeyPatch, sqlite_session: Session) -> None: + annotation = _annotation() monkeypatch.setattr( AppAnnotationService, "insert_app_annotation_directly", lambda *_args, **_kwargs: annotation ) api = AnnotationListApi() handler = unwrap(api.post) - app_model = SimpleNamespace(id="app") + app_model = _app() with app.test_request_context("/apps/annotations", method="POST", json={"question": "q", "answer": "a"}): payload = AnnotationCreatePayload.model_validate(request.get_json() or {}) - response, status = handler(api, payload, MagicMock(), app_model=app_model) + response, status = handler(api, payload, sqlite_session, app_model=app_model) assert status == HTTPStatus.CREATED assert response["question"] == "q" class TestAnnotationUpdateDeleteApi: - def test_update_delete(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: - annotation = SimpleNamespace(id="a1", question="q", content="a", created_at=0) + def test_update_delete(self, app: Flask, monkeypatch: pytest.MonkeyPatch, sqlite_session: Session) -> None: + annotation = _annotation() monkeypatch.setattr( AppAnnotationService, "update_app_annotation_directly", lambda *_args, **_kwargs: annotation ) @@ -295,12 +324,12 @@ class TestAnnotationUpdateDeleteApi: api = AnnotationUpdateDeleteApi() put_handler = unwrap(api.put) delete_handler = unwrap(api.delete) - app_model = SimpleNamespace(id="app", tenant_id="tenant") + app_model = _app() with app.test_request_context("/apps/annotations/1", method="PUT", json={"question": "q", "answer": "a"}): payload = AnnotationCreatePayload.model_validate(request.get_json() or {}) - response = put_handler(api, payload, MagicMock(), app_model=app_model, annotation_id="1") + response = put_handler(api, payload, sqlite_session, app_model=app_model, annotation_id="1") assert response["answer"] == "a" with app.test_request_context("/apps/annotations/1", method="DELETE"): - response, status = delete_handler(api, MagicMock(), app_model=app_model, annotation_id="1") + response, status = delete_handler(api, sqlite_session, app_model=app_model, annotation_id="1") assert status == 204 delete_mock.assert_called_once() diff --git a/api/tests/unit_tests/controllers/service_api/app/test_conversation.py b/api/tests/unit_tests/controllers/service_api/app/test_conversation.py index 17feffd4400..093852d792f 100644 --- a/api/tests/unit_tests/controllers/service_api/app/test_conversation.py +++ b/api/tests/unit_tests/controllers/service_api/app/test_conversation.py @@ -45,7 +45,7 @@ from core.app.entities.app_invoke_entities import InvokeFrom from fields._value_type_serializer import serialize_value_type from graphon.variables import StringSegment from graphon.variables.types import SegmentType -from models.enums import ConversationFromSource +from models.enums import ConversationFromSource, EndUserType from models.model import App, AppMode, Conversation, EndUser from services.conversation_service import ConversationService from services.errors.conversation import ( @@ -55,11 +55,29 @@ from services.errors.conversation import ( ) -def _end_user(user_id: str = "end-user-1") -> EndUser: - end_user = EndUser( +def _app(*, app_id: str = "app-1", mode: AppMode = AppMode.CHAT) -> App: + return App( + id=app_id, + tenant_id="tenant-1", + name="Service API app", + description="", + mode=mode, + enable_site=True, + enable_api=True, + max_active_requests=0, + ) + + +def _end_user(user_id: str = "end-user-1", app_id: str = "app-1") -> EndUser: + return EndUser( id=user_id, + tenant_id="tenant-1", + app_id=app_id, + type=EndUserType.SERVICE_API, + external_user_id="external-user-1", + name="Service API user", + session_id="session-1", ) - return end_user def _conversation( @@ -482,8 +500,8 @@ class TestConversationService: mock_pagination.return_value = mock_result result = ConversationService.pagination_by_last_id( - app_model=App(), - user=EndUser(), + app_model=_app(), + user=_end_user(), last_id=None, limit=20, invoke_from=Mock(), @@ -501,9 +519,7 @@ class TestConversationService: sqlite_session.add(conversation) sqlite_session.commit() - app_model = App( - id="app-1", - ) + app_model = _app() end_user = _end_user() result = ConversationService.rename( @@ -534,8 +550,8 @@ class TestConversationApiController: def test_list_not_chat(self, app: Flask) -> None: api = ConversationApi() handler = unwrap(api.get) - app_model = SimpleNamespace(mode=AppMode.COMPLETION) - end_user = SimpleNamespace() + app_model = _app(mode=AppMode.COMPLETION) + end_user = _end_user() with app.test_request_context("/conversations", method="GET"): with pytest.raises(NotChatAppError): @@ -563,7 +579,7 @@ class TestConversationApiController: api = ConversationApi() handler = unwrap(api.get) - app_model = SimpleNamespace(id="app-1", mode=AppMode.CHAT) + app_model = _app() end_user = _end_user() with app.test_request_context( @@ -583,8 +599,8 @@ class TestConversationDetailApiController: def test_delete_not_chat(self, app: Flask) -> None: api = ConversationDetailApi() handler = unwrap(api.delete) - app_model = SimpleNamespace(mode=AppMode.COMPLETION) - end_user = SimpleNamespace() + app_model = _app(mode=AppMode.COMPLETION) + end_user = _end_user() with app.test_request_context("/conversations/1", method="DELETE"): with pytest.raises(NotChatAppError): @@ -604,8 +620,8 @@ class TestConversationDetailApiController: api = ConversationDetailApi() handler = unwrap(api.delete) - app_model = SimpleNamespace(mode=AppMode.CHAT) - end_user = SimpleNamespace() + app_model = _app() + end_user = _end_user() with app.test_request_context("/conversations/1", method="DELETE"): with pytest.raises(NotFound): @@ -627,8 +643,8 @@ class TestConversationRenameApiController: api = ConversationRenameApi() handler = unwrap(api.post) - app_model = SimpleNamespace(mode=AppMode.CHAT) - end_user = SimpleNamespace() + app_model = _app() + end_user = _end_user() with app.test_request_context( "/conversations/1/name", @@ -650,8 +666,8 @@ class TestConversationVariablesApiController: def test_not_chat(self, app: Flask) -> None: api = ConversationVariablesApi() handler = unwrap(api.get) - app_model = SimpleNamespace(mode=AppMode.COMPLETION) - end_user = SimpleNamespace() + app_model = _app(mode=AppMode.COMPLETION) + end_user = _end_user() with app.test_request_context("/conversations/1/variables", method="GET"): with pytest.raises(NotChatAppError): @@ -672,8 +688,8 @@ class TestConversationVariablesApiController: api = ConversationVariablesApi() handler = unwrap(api.get) - app_model = SimpleNamespace(mode=AppMode.CHAT) - end_user = SimpleNamespace() + app_model = _app() + end_user = _end_user() with app.test_request_context( "/conversations/1/variables?limit=20", @@ -711,8 +727,8 @@ class TestConversationVariablesApiController: api = ConversationVariablesApi() handler = unwrap(api.get) - app_model = SimpleNamespace(mode=AppMode.CHAT) - end_user = SimpleNamespace() + app_model = _app() + end_user = _end_user() with app.test_request_context( "/conversations/1/variables?limit=20", @@ -743,8 +759,8 @@ class TestConversationVariableDetailApiController: api = ConversationVariableDetailApi() handler = unwrap(api.put) - app_model = SimpleNamespace(mode=AppMode.CHAT) - end_user = SimpleNamespace() + app_model = _app() + end_user = _end_user() with app.test_request_context( "/conversations/1/variables/2", @@ -771,8 +787,8 @@ class TestConversationVariableDetailApiController: api = ConversationVariableDetailApi() handler = unwrap(api.put) - app_model = SimpleNamespace(mode=AppMode.CHAT) - end_user = SimpleNamespace() + app_model = _app() + end_user = _end_user() with app.test_request_context( "/conversations/1/variables/2", @@ -807,8 +823,8 @@ class TestConversationVariableDetailApiController: api = ConversationVariableDetailApi() handler = unwrap(api.put) - app_model = SimpleNamespace(mode=AppMode.CHAT) - end_user = SimpleNamespace() + app_model = _app() + end_user = _end_user() with app.test_request_context( "/conversations/1/variables/2", diff --git a/api/tests/unit_tests/controllers/service_api/app/test_hitl_service_api.py b/api/tests/unit_tests/controllers/service_api/app/test_hitl_service_api.py index d0d77557cad..f71c3694859 100644 --- a/api/tests/unit_tests/controllers/service_api/app/test_hitl_service_api.py +++ b/api/tests/unit_tests/controllers/service_api/app/test_hitl_service_api.py @@ -20,6 +20,7 @@ from sqlalchemy.orm import Session, sessionmaker import services.app_generate_service as ags_module from controllers.service_api.app.workflow_events import WorkflowEventsApi from core.app.app_config.entities import AppAdditionalFeatures, WorkflowUIBasedAppConfig +from core.app.apps.advanced_chat.generate_task_pipeline import ConversationSnapshot, MessageSnapshot, WorkflowSnapshot from core.app.apps.common import workflow_response_converter from core.app.apps.common.workflow_response_converter import WorkflowResponseConverter from core.app.entities.app_invoke_entities import AdvancedChatAppGenerateEntity, InvokeFrom, WorkflowAppGenerateEntity @@ -42,10 +43,10 @@ from graphon.enums import WorkflowExecutionStatus, WorkflowNodeExecutionStatus from graphon.runtime import GraphRuntimeState, VariablePool from libs.datetime_utils import to_utc_timestamp from models.account import Account -from models.enums import CreatorUserRole, MessageStatus +from models.enums import CreatorUserRole, EndUserType, MessageStatus from models.human_input import HumanInputForm -from models.model import AppMode -from models.workflow import WorkflowRun +from models.model import App, AppMode, EndUser +from models.workflow import Workflow, WorkflowRun, WorkflowType from repositories.api_workflow_node_execution_repository import WorkflowNodeExecutionSnapshot from repositories.entities.workflow_pause import WorkflowPauseEntity from services.app_generate_service import AppGenerateService @@ -84,6 +85,47 @@ def _mock_repo_for_run(monkeypatch: pytest.MonkeyPatch, workflow_run, sqlite_eng return workflow_events_module +def _app(*, app_id: str = "app-1", tenant_id: str = "tenant-1", mode: AppMode = AppMode.WORKFLOW) -> App: + return App( + id=app_id, + tenant_id=tenant_id, + name="Service API app", + description="", + mode=mode, + enable_site=True, + enable_api=True, + max_active_requests=0, + ) + + +def _end_user(*, user_id: str = "end-user-1", app_id: str = "app-1", tenant_id: str = "tenant-1") -> EndUser: + return EndUser( + id=user_id, + tenant_id=tenant_id, + app_id=app_id, + type=EndUserType.SERVICE_API, + external_user_id="external-user-1", + name="Service API user", + session_id="session-1", + ) + + +def _workflow(*, workflow_id: str = "workflow-id", app_id: str = "app-id", tenant_id: str = "tenant-id") -> Workflow: + return Workflow( + id=workflow_id, + tenant_id=tenant_id, + app_id=app_id, + type=WorkflowType.WORKFLOW, + version="1", + graph=json.dumps({"nodes": [], "edges": []}), + features="{}", + created_by="owner-id", + environment_variables=[], + conversation_variables=[], + rag_pipeline_variables=[], + ) + + def _persist_human_input_form( sqlite_session: Session, *, @@ -288,13 +330,8 @@ class TestHitlServiceApi: monkeypatch: pytest.MonkeyPatch, sqlite_engine: Engine, ) -> None: - workflow_run = SimpleNamespace( - id="run-1", - app_id="app-1", - created_by_role=CreatorUserRole.END_USER, - created_by="end-user-1", - finished_at=None, - ) + workflow_run = _build_workflow_run(WorkflowExecutionStatus.RUNNING) + workflow_run.created_by = "end-user-1" workflow_events_module = _mock_repo_for_run( monkeypatch, workflow_run=workflow_run, @@ -309,8 +346,8 @@ class TestHitlServiceApi: api = WorkflowEventsApi() handler = unwrap(api.get) - app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.WORKFLOW) - end_user = SimpleNamespace(id="end-user-1") + app_model = _app() + end_user = _end_user() with app.test_request_context("/workflow/run-1/events?user=u1&continue_on_pause=true", method="GET"): response = handler(api, app_model=app_model, end_user=end_user, workflow_run_id="run-1") @@ -329,13 +366,8 @@ class TestHitlServiceApi: monkeypatch: pytest.MonkeyPatch, sqlite_engine: Engine, ) -> None: - workflow_run = SimpleNamespace( - id="run-1", - app_id="app-1", - created_by_role=CreatorUserRole.END_USER, - created_by="end-user-1", - finished_at=None, - ) + workflow_run = _build_workflow_run(WorkflowExecutionStatus.RUNNING) + workflow_run.created_by = "end-user-1" workflow_events_module = _mock_repo_for_run( monkeypatch, workflow_run=workflow_run, @@ -351,8 +383,8 @@ class TestHitlServiceApi: api = WorkflowEventsApi() handler = unwrap(api.get) - app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.WORKFLOW) - end_user = SimpleNamespace(id="end-user-1") + app_model = _app() + end_user = _end_user() with app.test_request_context( "/workflow/run-1/events?user=u1&include_state_snapshot=true&continue_on_pause=true", @@ -384,8 +416,7 @@ class TestHitlServiceApi: apply_config_overrides(monkeypatch, DEPLOYMENT_EDITION=DeploymentEdition.COMMUNITY) monkeypatch.setattr(ags_module, "RateLimit", _DummyRateLimit) - workflow = MagicMock() - workflow.created_by = "owner-id" + workflow = _workflow() monkeypatch.setattr(AppGenerateService, "_get_workflow", lambda *args, **kwargs: workflow) sqlite_session_maker = sessionmaker(bind=sqlite_engine, expire_on_commit=False) monkeypatch.setattr(ags_module.session_factory, "get_session_maker", lambda: sqlite_session_maker) @@ -395,16 +426,8 @@ class TestHitlServiceApi: generator_instance.convert_to_event_stream.side_effect = lambda payload: payload monkeypatch.setattr(ags_module, "AdvancedChatAppGenerator", lambda: generator_instance) - app_model = MagicMock() - app_model.mode = AppMode.ADVANCED_CHAT - app_model.id = "app-id" - app_model.tenant_id = "tenant-id" - app_model.max_active_requests = 0 - app_model.is_agent = False - app_model.is_agent_with_session.return_value = False - - user = MagicMock() - user.id = "user-id" + app_model = _app(app_id="app-id", tenant_id="tenant-id", mode=AppMode.ADVANCED_CHAT) + user = _end_user(user_id="user-id", app_id="app-id", tenant_id="tenant-id") with sqlite_session_maker() as session: result = AppGenerateService.generate( @@ -456,7 +479,6 @@ class TestHitlServiceApi: def test_advanced_chat_blocking_pipeline_pause_payload_contract(self) -> None: from core.app.app_config.entities import AppAdditionalFeatures from core.app.apps.advanced_chat.generate_task_pipeline import AdvancedChatAppGenerateTaskPipeline - from models.model import EndUser app_config = WorkflowUIBasedAppConfig( tenant_id="tenant", @@ -481,17 +503,17 @@ class TestHitlServiceApi: ) pipeline = AdvancedChatAppGenerateTaskPipeline( application_generate_entity=application_generate_entity, - workflow=SimpleNamespace(id="workflow-id", tenant_id="tenant", features_dict={}), + workflow=WorkflowSnapshot(id="workflow-id", tenant_id="tenant", features_dict={}), queue_manager=SimpleNamespace(invoke_from=InvokeFrom.WEB_APP, graph_runtime_state=None), - conversation=SimpleNamespace(id="conv-id", mode=AppMode.ADVANCED_CHAT), - message=SimpleNamespace( + conversation=ConversationSnapshot(id="conv-id", mode=AppMode.ADVANCED_CHAT), + message=MessageSnapshot( id="message-id", query="hello", created_at=datetime.utcnow(), status=MessageStatus.NORMAL, answer="", ), - user=EndUser(tenant_id="tenant", type="session", name="tester", session_id="session"), + user=_end_user(user_id="user", app_id="app", tenant_id="tenant"), stream=False, dialogue_count=1, draft_var_saver_factory=lambda **kwargs: None, @@ -574,9 +596,9 @@ class TestHitlServiceApi: ) pipeline = WorkflowAppGenerateTaskPipeline( application_generate_entity=application_generate_entity, - workflow=SimpleNamespace(id="workflow-id", tenant_id="tenant", features_dict={}), + workflow=_workflow(workflow_id="workflow-id", app_id="app", tenant_id="tenant"), queue_manager=SimpleNamespace(invoke_from=InvokeFrom.WEB_APP, graph_runtime_state=None), - user=SimpleNamespace(id="user", session_id="session"), + user=_end_user(user_id="user", app_id="app", tenant_id="tenant"), stream=False, draft_var_saver_factory=lambda **kwargs: None, ) diff --git a/api/tests/unit_tests/controllers/service_api/app/test_human_input_form.py b/api/tests/unit_tests/controllers/service_api/app/test_human_input_form.py index a602a3bdc10..cc6d57255ef 100644 --- a/api/tests/unit_tests/controllers/service_api/app/test_human_input_form.py +++ b/api/tests/unit_tests/controllers/service_api/app/test_human_input_form.py @@ -11,15 +11,53 @@ from unittest.mock import Mock import pytest from flask import Flask, request +from sqlalchemy.engine import Engine from werkzeug.exceptions import NotFound from controllers.common.human_input import HumanInputFormSubmitPayload from controllers.service_api.app.human_input_form import WorkflowHumanInputFormApi +from models.enums import EndUserType from models.human_input import RecipientType +from models.model import App, AppMode, EndUser + + +def _app() -> App: + return App( + id="app-1", + tenant_id="tenant-1", + name="Service API app", + description="", + mode=AppMode.WORKFLOW, + enable_site=True, + enable_api=True, + max_active_requests=0, + ) + + +def _end_user() -> EndUser: + return EndUser( + id="end-user-1", + tenant_id="tenant-1", + app_id="app-1", + type=EndUserType.SERVICE_API, + external_user_id="external-user-1", + name="Service API user", + session_id="session-1", + ) + + +def _configure_service( + monkeypatch: pytest.MonkeyPatch, + service_mock: Mock, + sqlite_engine: Engine, +) -> None: + workflow_module = sys.modules["controllers.service_api.app.human_input_form"] + monkeypatch.setattr(workflow_module, "HumanInputService", lambda _engine: service_mock) + monkeypatch.setattr(workflow_module, "db", SimpleNamespace(engine=sqlite_engine)) class TestWorkflowHumanInputFormApi: - def test_get_success(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: + def test_get_success(self, app: Flask, monkeypatch: pytest.MonkeyPatch, sqlite_engine: Engine) -> None: definition = SimpleNamespace( model_dump=lambda **_kwargs: { "rendered_content": "Rendered form content", @@ -40,13 +78,11 @@ class TestWorkflowHumanInputFormApi: service_mock.resolve_form_inputs.return_value = [ SimpleNamespace(model_dump=lambda **_kwargs: {"output_variable_name": "name"}) ] - workflow_module = sys.modules["controllers.service_api.app.human_input_form"] - monkeypatch.setattr(workflow_module, "HumanInputService", lambda _engine: service_mock) - monkeypatch.setattr(workflow_module, "db", SimpleNamespace(engine=object())) + _configure_service(monkeypatch, service_mock, sqlite_engine) api = WorkflowHumanInputFormApi() handler = unwrap(api.get) - app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1") + app_model = _app() with app.test_request_context("/form/human_input/token-1", method="GET"): response = handler(api, app_model=app_model, form_token="token-1") @@ -63,7 +99,9 @@ class TestWorkflowHumanInputFormApi: service_mock.resolve_form_inputs.assert_called_once_with(form) service_mock.ensure_form_active.assert_called_once_with(form) - def test_get_resolves_runtime_select_values(self, app, monkeypatch: pytest.MonkeyPatch) -> None: + def test_get_resolves_runtime_select_values( + self, app: Flask, monkeypatch: pytest.MonkeyPatch, sqlite_engine: Engine + ) -> None: definition = SimpleNamespace( model_dump=lambda **_kwargs: { "rendered_content": "Rendered form content", @@ -93,13 +131,11 @@ class TestWorkflowHumanInputFormApi: service_mock = Mock() service_mock.get_form_by_token.return_value = form service_mock.resolve_form_inputs.return_value = [resolved_input] - workflow_module = sys.modules["controllers.service_api.app.human_input_form"] - monkeypatch.setattr(workflow_module, "HumanInputService", lambda _engine: service_mock) - monkeypatch.setattr(workflow_module, "db", SimpleNamespace(engine=object())) + _configure_service(monkeypatch, service_mock, sqlite_engine) api = WorkflowHumanInputFormApi() handler = unwrap(api.get) - app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1") + app_model = _app() with app.test_request_context("/form/human_input/token-1", method="GET"): response = handler(api, app_model=app_model, form_token="token-1") @@ -108,7 +144,7 @@ class TestWorkflowHumanInputFormApi: assert payload["inputs"][0]["option_source"]["value"] == ["approve", "reject"] service_mock.resolve_form_inputs.assert_called_once_with(form) - def test_get_form_not_in_app(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: + def test_get_form_not_in_app(self, app: Flask, monkeypatch: pytest.MonkeyPatch, sqlite_engine: Engine) -> None: form = SimpleNamespace( app_id="another-app", tenant_id="tenant-1", @@ -116,13 +152,11 @@ class TestWorkflowHumanInputFormApi: ) service_mock = Mock() service_mock.get_form_by_token.return_value = form - workflow_module = sys.modules["controllers.service_api.app.human_input_form"] - monkeypatch.setattr(workflow_module, "HumanInputService", lambda _engine: service_mock) - monkeypatch.setattr(workflow_module, "db", SimpleNamespace(engine=object())) + _configure_service(monkeypatch, service_mock, sqlite_engine) api = WorkflowHumanInputFormApi() handler = unwrap(api.get) - app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1") + app_model = _app() with app.test_request_context("/form/human_input/token-1", method="GET"): with pytest.raises(NotFound): @@ -138,7 +172,11 @@ class TestWorkflowHumanInputFormApi: ], ) def test_get_rejects_non_service_api_recipient_types( - self, app: Flask, monkeypatch: pytest.MonkeyPatch, recipient_type: RecipientType + self, + app: Flask, + monkeypatch: pytest.MonkeyPatch, + sqlite_engine: Engine, + recipient_type: RecipientType, ) -> None: form = SimpleNamespace( app_id="app-1", @@ -148,13 +186,11 @@ class TestWorkflowHumanInputFormApi: ) service_mock = Mock() service_mock.get_form_by_token.return_value = form - workflow_module = sys.modules["controllers.service_api.app.human_input_form"] - monkeypatch.setattr(workflow_module, "HumanInputService", lambda _engine: service_mock) - monkeypatch.setattr(workflow_module, "db", SimpleNamespace(engine=object())) + _configure_service(monkeypatch, service_mock, sqlite_engine) api = WorkflowHumanInputFormApi() handler = unwrap(api.get) - app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1") + app_model = _app() with app.test_request_context("/form/human_input/token-1", method="GET"): with pytest.raises(NotFound): @@ -162,7 +198,7 @@ class TestWorkflowHumanInputFormApi: service_mock.ensure_form_active.assert_not_called() - def test_post_success(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: + def test_post_success(self, app: Flask, monkeypatch: pytest.MonkeyPatch, sqlite_engine: Engine) -> None: form = SimpleNamespace( app_id="app-1", tenant_id="tenant-1", @@ -170,14 +206,12 @@ class TestWorkflowHumanInputFormApi: ) service_mock = Mock() service_mock.get_form_by_token.return_value = form - workflow_module = sys.modules["controllers.service_api.app.human_input_form"] - monkeypatch.setattr(workflow_module, "HumanInputService", lambda _engine: service_mock) - monkeypatch.setattr(workflow_module, "db", SimpleNamespace(engine=object())) + _configure_service(monkeypatch, service_mock, sqlite_engine) api = WorkflowHumanInputFormApi() handler = unwrap(api.post) - app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1") - end_user = SimpleNamespace(id="end-user-1") + app_model = _app() + end_user = _end_user() with app.test_request_context( "/form/human_input/token-1", @@ -197,7 +231,9 @@ class TestWorkflowHumanInputFormApi: submission_end_user_id="end-user-1", ) - def test_post_accepts_select_file_and_file_list_inputs(self, app, monkeypatch: pytest.MonkeyPatch) -> None: + def test_post_accepts_select_file_and_file_list_inputs( + self, app: Flask, monkeypatch: pytest.MonkeyPatch, sqlite_engine: Engine + ) -> None: form = SimpleNamespace( app_id="app-1", tenant_id="tenant-1", @@ -205,14 +241,12 @@ class TestWorkflowHumanInputFormApi: ) service_mock = Mock() service_mock.get_form_by_token.return_value = form - workflow_module = sys.modules["controllers.service_api.app.human_input_form"] - monkeypatch.setattr(workflow_module, "HumanInputService", lambda _engine: service_mock) - monkeypatch.setattr(workflow_module, "db", SimpleNamespace(engine=object())) + _configure_service(monkeypatch, service_mock, sqlite_engine) api = WorkflowHumanInputFormApi() handler = unwrap(api.post) - app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1") - end_user = SimpleNamespace(id="end-user-1") + app_model = _app() + end_user = _end_user() inputs = { "decision": "approve", "attachment": { @@ -273,7 +307,11 @@ class TestWorkflowHumanInputFormApi: ], ) def test_post_rejects_non_service_api_recipient_types( - self, app: Flask, monkeypatch: pytest.MonkeyPatch, recipient_type: RecipientType + self, + app: Flask, + monkeypatch: pytest.MonkeyPatch, + sqlite_engine: Engine, + recipient_type: RecipientType, ) -> None: form = SimpleNamespace( app_id="app-1", @@ -282,14 +320,12 @@ class TestWorkflowHumanInputFormApi: ) service_mock = Mock() service_mock.get_form_by_token.return_value = form - workflow_module = sys.modules["controllers.service_api.app.human_input_form"] - monkeypatch.setattr(workflow_module, "HumanInputService", lambda _engine: service_mock) - monkeypatch.setattr(workflow_module, "db", SimpleNamespace(engine=object())) + _configure_service(monkeypatch, service_mock, sqlite_engine) api = WorkflowHumanInputFormApi() handler = unwrap(api.post) - app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1") - end_user = SimpleNamespace(id="end-user-1") + app_model = _app() + end_user = _end_user() with app.test_request_context( "/form/human_input/token-1", diff --git a/api/tests/unit_tests/controllers/service_api/app/test_message.py b/api/tests/unit_tests/controllers/service_api/app/test_message.py index 95c6a49b186..6d4770b30d4 100644 --- a/api/tests/unit_tests/controllers/service_api/app/test_message.py +++ b/api/tests/unit_tests/controllers/service_api/app/test_message.py @@ -17,7 +17,6 @@ Focus on: import uuid from collections.abc import Iterator from inspect import unwrap -from types import SimpleNamespace from unittest.mock import Mock, patch import pytest @@ -36,7 +35,7 @@ from controllers.service_api.app.message import ( MessageListQuery, MessageSuggestedApi, ) -from models.enums import FeedbackRating +from models.enums import EndUserType, FeedbackRating from models.model import App, AppMode, EndUser from services.errors.conversation import ConversationNotExistsError from services.errors.message import ( @@ -47,6 +46,31 @@ from services.errors.message import ( from services.message_service import MessageService +def _app(*, mode: AppMode = AppMode.CHAT) -> App: + return App( + id="app-1", + tenant_id="tenant-1", + name="Service API app", + description="", + mode=mode, + enable_site=True, + enable_api=True, + max_active_requests=0, + ) + + +def _end_user() -> EndUser: + return EndUser( + id="end-user-1", + tenant_id="tenant-1", + app_id="app-1", + type=EndUserType.SERVICE_API, + external_user_id="external-user-1", + name="Service API user", + session_id="session-1", + ) + + @pytest.fixture def orm_session(sqlite_engine: Engine) -> Iterator[Session]: """Provide a real caller-owned session for MessageService interface tests.""" @@ -273,8 +297,8 @@ class TestMessageService: mock_pagination.return_value = mock_result result = MessageService.pagination_by_first_id( - app_model=App(), - user=EndUser(), + app_model=_app(), + user=_end_user(), conversation_id=str(uuid.uuid4()), first_id=None, limit=20, @@ -294,8 +318,8 @@ class TestMessageService: with pytest.raises(services.errors.conversation.ConversationNotExistsError): MessageService.pagination_by_first_id( - app_model=App(), - user=EndUser(), + app_model=_app(), + user=_end_user(), conversation_id="invalid_id", first_id=None, limit=20, @@ -309,8 +333,8 @@ class TestMessageService: with pytest.raises(FirstMessageNotExistsError): MessageService.pagination_by_first_id( - app_model=App(), - user=EndUser(), + app_model=_app(), + user=_end_user(), conversation_id=str(uuid.uuid4()), first_id="invalid_first_id", limit=20, @@ -323,9 +347,9 @@ class TestMessageService: mock_create_feedback.return_value = None MessageService.create_feedback( - app_model=App(), + app_model=_app(), message_id=str(uuid.uuid4()), - user=EndUser(), + user=_end_user(), rating=FeedbackRating.LIKE, content="Great response!", session=orm_session, @@ -340,9 +364,9 @@ class TestMessageService: with pytest.raises(MessageNotExistsError): MessageService.create_feedback( - app_model=App(), + app_model=_app(), message_id="invalid_message_id", - user=EndUser(), + user=_end_user(), rating=FeedbackRating.LIKE, content=None, session=orm_session, @@ -357,7 +381,7 @@ class TestMessageService: ] mock_get_feedbacks.return_value = mock_feedbacks - result = MessageService.get_all_messages_feedbacks(app_model=App(), page=1, limit=20, session=orm_session) + result = MessageService.get_all_messages_feedbacks(app_model=_app(), page=1, limit=20, session=orm_session) assert len(result) == 2 assert result[0]["rating"] == "like" @@ -369,8 +393,8 @@ class TestMessageService: mock_get_questions.return_value = mock_questions result = MessageService.get_suggested_questions_after_answer( - app_model=App(), - user=EndUser(), + app_model=_app(), + user=_end_user(), message_id=str(uuid.uuid4()), invoke_from=Mock(), session=orm_session, @@ -386,8 +410,8 @@ class TestMessageService: with pytest.raises(SuggestedQuestionsAfterAnswerDisabledError): MessageService.get_suggested_questions_after_answer( - app_model=App(), - user=EndUser(), + app_model=_app(), + user=_end_user(), message_id=str(uuid.uuid4()), invoke_from=Mock(), session=orm_session, @@ -400,8 +424,8 @@ class TestMessageService: with pytest.raises(MessageNotExistsError): MessageService.get_suggested_questions_after_answer( - app_model=App(), - user=EndUser(), + app_model=_app(), + user=_end_user(), message_id="invalid_message_id", invoke_from=Mock(), session=orm_session, @@ -412,8 +436,8 @@ class TestMessageListApi: def test_not_chat_app(self, app: Flask) -> None: api = MessageListApi() handler = unwrap(api.get) - app_model = SimpleNamespace(mode=AppMode.COMPLETION.value) - end_user = SimpleNamespace() + app_model = _app(mode=AppMode.COMPLETION) + end_user = _end_user() # @model_validate parses ahead of the app-mode guard, so the id has to be well-formed to # reach the branch this test is about. @@ -435,8 +459,8 @@ class TestMessageListApi: api = MessageListApi() handler = unwrap(api.get) - app_model = SimpleNamespace(mode=AppMode.CHAT.value) - end_user = SimpleNamespace() + app_model = _app() + end_user = _end_user() with app.test_request_context( "/messages?conversation_id=00000000-0000-0000-0000-000000000001", @@ -459,8 +483,8 @@ class TestMessageListApi: api = MessageListApi() handler = unwrap(api.get) - app_model = SimpleNamespace(mode=AppMode.CHAT.value) - end_user = SimpleNamespace() + app_model = _app() + end_user = _end_user() with app.test_request_context( "/messages?conversation_id=00000000-0000-0000-0000-000000000001&first_id=00000000-0000-0000-0000-000000000002", @@ -485,8 +509,8 @@ class TestMessageFeedbackApi: api = MessageFeedbackApi() handler = unwrap(api.post) - app_model = SimpleNamespace() - end_user = SimpleNamespace() + app_model = _app() + end_user = _end_user() with app.test_request_context( "/messages/m1/feedbacks", @@ -517,7 +541,7 @@ class TestAppGetFeedbacksApi: api = AppGetFeedbacksApi() handler = unwrap(api.get) - app_model = SimpleNamespace() + app_model = _app() with app.test_request_context("/app/feedbacks?page=1&limit=20", method="GET"): response = handler( @@ -531,8 +555,8 @@ class TestMessageSuggestedApi: def test_not_chat(self, app: Flask) -> None: api = MessageSuggestedApi() handler = unwrap(api.get) - app_model = SimpleNamespace(mode=AppMode.COMPLETION.value) - end_user = SimpleNamespace() + app_model = _app(mode=AppMode.COMPLETION) + end_user = _end_user() with app.test_request_context("/messages/m1/suggested", method="GET"): with pytest.raises(NotChatAppError): @@ -547,8 +571,8 @@ class TestMessageSuggestedApi: api = MessageSuggestedApi() handler = unwrap(api.get) - app_model = SimpleNamespace(mode=AppMode.CHAT.value) - end_user = SimpleNamespace() + app_model = _app() + end_user = _end_user() with app.test_request_context("/messages/m1/suggested", method="GET"): with pytest.raises(NotFound): @@ -563,8 +587,8 @@ class TestMessageSuggestedApi: api = MessageSuggestedApi() handler = unwrap(api.get) - app_model = SimpleNamespace(mode=AppMode.CHAT.value) - end_user = SimpleNamespace() + app_model = _app() + end_user = _end_user() with app.test_request_context("/messages/m1/suggested", method="GET"): with pytest.raises(BadRequest): @@ -579,8 +603,8 @@ class TestMessageSuggestedApi: api = MessageSuggestedApi() handler = unwrap(api.get) - app_model = SimpleNamespace(mode=AppMode.CHAT.value) - end_user = SimpleNamespace() + app_model = _app() + end_user = _end_user() with app.test_request_context("/messages/m1/suggested", method="GET"): with pytest.raises(InternalServerError): @@ -595,8 +619,8 @@ class TestMessageSuggestedApi: api = MessageSuggestedApi() handler = unwrap(api.get) - app_model = SimpleNamespace(mode=AppMode.CHAT.value) - end_user = SimpleNamespace() + app_model = _app() + end_user = _end_user() with app.test_request_context("/messages/m1/suggested", method="GET"): response = handler(api, app_model=app_model, end_user=end_user, message_id="m1") diff --git a/api/tests/unit_tests/controllers/service_api/app/test_workflow_events.py b/api/tests/unit_tests/controllers/service_api/app/test_workflow_events.py index 0e4a9db257c..5419a34cb8b 100644 --- a/api/tests/unit_tests/controllers/service_api/app/test_workflow_events.py +++ b/api/tests/unit_tests/controllers/service_api/app/test_workflow_events.py @@ -11,15 +11,66 @@ from unittest.mock import Mock import pytest from flask import Flask +from sqlalchemy.engine import Engine from werkzeug.exceptions import NotFound from controllers.service_api.app.error import NotWorkflowAppError from controllers.service_api.app.workflow_events import WorkflowEventsApi -from models.enums import CreatorUserRole -from models.model import AppMode +from graphon.enums import WorkflowExecutionStatus +from models.enums import CreatorUserRole, EndUserType, WorkflowRunTriggeredFrom +from models.model import App, AppMode, EndUser +from models.workflow import WorkflowRun, WorkflowType -def _mock_repo_for_run(monkeypatch: pytest.MonkeyPatch, workflow_run): +def _app(*, mode: AppMode = AppMode.WORKFLOW) -> App: + return App( + id="app-1", + tenant_id="tenant-1", + name="Service API app", + description="", + mode=mode, + enable_site=True, + enable_api=True, + max_active_requests=0, + ) + + +def _end_user() -> EndUser: + return EndUser( + id="end-user-1", + tenant_id="tenant-1", + app_id="app-1", + type=EndUserType.SERVICE_API, + external_user_id="external-user-1", + name="Service API user", + session_id="session-1", + ) + + +def _workflow_run(*, created_by_role: CreatorUserRole = CreatorUserRole.END_USER) -> WorkflowRun: + return WorkflowRun( + id="run-1", + tenant_id="tenant-1", + app_id="app-1", + workflow_id="workflow-1", + type=WorkflowType.WORKFLOW, + triggered_from=WorkflowRunTriggeredFrom.APP_RUN, + version="1", + graph=None, + inputs="{}", + status=WorkflowExecutionStatus.RUNNING, + outputs="{}", + error=None, + elapsed_time=0, + total_tokens=0, + total_steps=0, + created_by_role=created_by_role, + created_by="end-user-1" if created_by_role == CreatorUserRole.END_USER else "account-1", + created_at=datetime(2026, 1, 1, tzinfo=UTC), + ) + + +def _mock_repo_for_run(monkeypatch: pytest.MonkeyPatch, workflow_run, sqlite_engine: Engine): workflow_events_module = sys.modules["controllers.service_api.app.workflow_events"] repo = SimpleNamespace(get_workflow_run_by_id_and_tenant_id=lambda **_kwargs: workflow_run) monkeypatch.setattr( @@ -27,7 +78,7 @@ def _mock_repo_for_run(monkeypatch: pytest.MonkeyPatch, workflow_run): "create_api_workflow_run_repository", lambda *_args, **_kwargs: repo, ) - monkeypatch.setattr(workflow_events_module, "db", SimpleNamespace(engine=object())) + monkeypatch.setattr(workflow_events_module, "db", SimpleNamespace(engine=sqlite_engine)) return workflow_events_module @@ -35,51 +86,42 @@ class TestWorkflowEventsApi: def test_wrong_app_mode(self, app: Flask) -> None: api = WorkflowEventsApi() handler = unwrap(api.get) - app_model = SimpleNamespace(mode=AppMode.CHAT.value) - end_user = SimpleNamespace(id="end-user-1") + app_model = _app(mode=AppMode.CHAT) + end_user = _end_user() with app.test_request_context("/workflow/run-1/events?user=u1", method="GET"): with pytest.raises(NotWorkflowAppError): handler(api, app_model=app_model, end_user=end_user, workflow_run_id="run-1") - def test_workflow_run_not_found(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: - _mock_repo_for_run(monkeypatch, workflow_run=None) + def test_workflow_run_not_found(self, app: Flask, monkeypatch: pytest.MonkeyPatch, sqlite_engine: Engine) -> None: + _mock_repo_for_run(monkeypatch, workflow_run=None, sqlite_engine=sqlite_engine) api = WorkflowEventsApi() handler = unwrap(api.get) - app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.WORKFLOW) - end_user = SimpleNamespace(id="end-user-1") + app_model = _app() + end_user = _end_user() with app.test_request_context("/workflow/run-1/events?user=u1", method="GET"): with pytest.raises(NotFound): handler(api, app_model=app_model, end_user=end_user, workflow_run_id="run-1") - def test_workflow_run_permission_denied(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: - workflow_run = SimpleNamespace( - id="run-1", - app_id="app-1", - created_by_role=CreatorUserRole.ACCOUNT, - created_by="another-user", - finished_at=None, - ) - _mock_repo_for_run(monkeypatch, workflow_run=workflow_run) + def test_workflow_run_permission_denied( + self, app: Flask, monkeypatch: pytest.MonkeyPatch, sqlite_engine: Engine + ) -> None: + workflow_run = _workflow_run(created_by_role=CreatorUserRole.ACCOUNT) + _mock_repo_for_run(monkeypatch, workflow_run=workflow_run, sqlite_engine=sqlite_engine) api = WorkflowEventsApi() handler = unwrap(api.get) - app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.WORKFLOW) - end_user = SimpleNamespace(id="end-user-1") + app_model = _app() + end_user = _end_user() with app.test_request_context("/workflow/run-1/events?user=u1", method="GET"): with pytest.raises(NotFound): handler(api, app_model=app_model, end_user=end_user, workflow_run_id="run-1") - def test_finished_run_returns_sse(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: - workflow_run = SimpleNamespace( - id="run-1", - app_id="app-1", - created_by_role=CreatorUserRole.END_USER, - created_by="end-user-1", - finished_at=datetime(2099, 1, 1, tzinfo=UTC), - ) - workflow_events_module = _mock_repo_for_run(monkeypatch, workflow_run=workflow_run) + def test_finished_run_returns_sse(self, app: Flask, monkeypatch: pytest.MonkeyPatch, sqlite_engine: Engine) -> None: + workflow_run = _workflow_run() + workflow_run.finished_at = datetime(2099, 1, 1, tzinfo=UTC) + workflow_events_module = _mock_repo_for_run(monkeypatch, workflow_run=workflow_run, sqlite_engine=sqlite_engine) monkeypatch.setattr( workflow_events_module.WorkflowResponseConverter, "workflow_run_result_to_finish_response", @@ -91,8 +133,8 @@ class TestWorkflowEventsApi: api = WorkflowEventsApi() handler = unwrap(api.get) - app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.WORKFLOW) - end_user = SimpleNamespace(id="end-user-1") + app_model = _app() + end_user = _end_user() with app.test_request_context("/workflow/run-1/events?user=u1", method="GET"): response = handler(api, app_model=app_model, end_user=end_user, workflow_run_id="run-1") @@ -104,15 +146,11 @@ class TestWorkflowEventsApi: assert payload["task_id"] == "run-1" assert payload["event"] == "workflow_finished" - def test_running_run_streams_events(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: - workflow_run = SimpleNamespace( - id="run-1", - app_id="app-1", - created_by_role=CreatorUserRole.END_USER, - created_by="end-user-1", - finished_at=None, - ) - workflow_events_module = _mock_repo_for_run(monkeypatch, workflow_run=workflow_run) + def test_running_run_streams_events( + self, app: Flask, monkeypatch: pytest.MonkeyPatch, sqlite_engine: Engine + ) -> None: + workflow_run = _workflow_run() + workflow_events_module = _mock_repo_for_run(monkeypatch, workflow_run=workflow_run, sqlite_engine=sqlite_engine) msg_generator = Mock() msg_generator.retrieve_events.return_value = ["raw-event"] workflow_generator = Mock() @@ -122,8 +160,8 @@ class TestWorkflowEventsApi: api = WorkflowEventsApi() handler = unwrap(api.get) - app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.WORKFLOW) - end_user = SimpleNamespace(id="end-user-1") + app_model = _app() + end_user = _end_user() with app.test_request_context("/workflow/run-1/events?user=u1", method="GET"): response = handler(api, app_model=app_model, end_user=end_user, workflow_run_id="run-1") @@ -136,15 +174,11 @@ class TestWorkflowEventsApi: ) workflow_generator.convert_to_event_stream.assert_called_once_with(["raw-event"]) - def test_running_run_with_snapshot(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: - workflow_run = SimpleNamespace( - id="run-1", - app_id="app-1", - created_by_role=CreatorUserRole.END_USER, - created_by="end-user-1", - finished_at=None, - ) - workflow_events_module = _mock_repo_for_run(monkeypatch, workflow_run=workflow_run) + def test_running_run_with_snapshot( + self, app: Flask, monkeypatch: pytest.MonkeyPatch, sqlite_engine: Engine + ) -> None: + workflow_run = _workflow_run() + workflow_events_module = _mock_repo_for_run(monkeypatch, workflow_run=workflow_run, sqlite_engine=sqlite_engine) msg_generator = Mock() workflow_generator = Mock() workflow_generator.convert_to_event_stream.return_value = iter(["data: snapshot\n\n"]) @@ -155,8 +189,8 @@ class TestWorkflowEventsApi: api = WorkflowEventsApi() handler = unwrap(api.get) - app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.WORKFLOW) - end_user = SimpleNamespace(id="end-user-1") + app_model = _app() + end_user = _end_user() with app.test_request_context("/workflow/run-1/events?user=u1&include_state_snapshot=true", method="GET"): response = handler(api, app_model=app_model, end_user=end_user, workflow_run_id="run-1")