mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
test: migrate Service API app sessions and ORM models to SQLite (#40558)
This commit is contained in:
1 parent
e7cfe142bc
commit
c529c3b2f1
6 files changed
+388
-227
No files matched your search
@@ -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()
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in new issue
Block a user