From fa000ae8a739cec6642e2ccd5c1efe848607f6b9 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Sat, 25 Jul 2026 12:17:57 +0900 Subject: [PATCH] test: use sqlite3 session in test_human_input_service (#38697) --- .../services/test_human_input_service.py | 197 ++++++++++-------- 1 file changed, 112 insertions(+), 85 deletions(-) diff --git a/api/tests/unit_tests/services/test_human_input_service.py b/api/tests/unit_tests/services/test_human_input_service.py index d23bdd945c6..55dd86129bb 100644 --- a/api/tests/unit_tests/services/test_human_input_service.py +++ b/api/tests/unit_tests/services/test_human_input_service.py @@ -1,10 +1,13 @@ import dataclasses import logging +from collections.abc import Iterator from datetime import datetime, timedelta from unittest.mock import MagicMock import pytest from pytest_mock import MockerFixture +from sqlalchemy.engine import Engine +from sqlalchemy.orm import Session, sessionmaker import services.human_input_service as human_input_service_module from core.app.app_config.entities import WorkflowUIBasedAppConfig @@ -28,7 +31,7 @@ from graphon.file import File, FileTransferMethod, FileType from graphon.runtime import GraphRuntimeState, VariablePool from libs.datetime_utils import naive_utc_now from models.human_input import RecipientType -from models.model import AppMode +from models.model import App, AppMode from services.human_input_service import ( Form, FormExpiredError, @@ -39,15 +42,27 @@ from services.human_input_service import ( @pytest.fixture -def mock_session_factory(): - session = MagicMock() - session_cm = MagicMock() - session_cm.__enter__.return_value = session - session_cm.__exit__.return_value = None +def sqlite_session_factory(sqlite_engine: Engine) -> Iterator[tuple[sessionmaker[Session], Session]]: + factory = sessionmaker(bind=sqlite_engine, expire_on_commit=False) + with factory() as session: + yield factory, session - factory = MagicMock() - factory.return_value = session_cm - return factory, session + +def _persist_app(sqlite_session: Session, mode: AppMode) -> App: + app = App( + id="app-id", + tenant_id="tenant-id", + name="Test App", + description="", + mode=mode, + workflow_id=None, + enable_site=True, + enable_api=True, + max_active_requests=0, + ) + sqlite_session.add(app) + sqlite_session.commit() + return app @pytest.fixture @@ -82,8 +97,13 @@ def sample_form_record(): ) -def test_enqueue_resume_dispatches_task_for_workflow(mocker: MockerFixture, mock_session_factory): - session_factory, session = mock_session_factory +@pytest.mark.parametrize("sqlite_session", [(App,)], indirect=True) +def test_enqueue_resume_dispatches_task_for_workflow( + mocker: MockerFixture, + sqlite_session_factory, + sqlite_session: Session, +): + session_factory, _ = sqlite_session_factory service = HumanInputService(session_factory) workflow_run = MagicMock() @@ -96,9 +116,7 @@ def test_enqueue_resume_dispatches_task_for_workflow(mocker: MockerFixture, mock return_value=workflow_run_repo, ) - app = MagicMock() - app.mode = "workflow" - session.execute.return_value.scalar_one_or_none.return_value = app + _persist_app(sqlite_session, AppMode.WORKFLOW) resume_task = mocker.patch("services.human_input_service.resume_app_execution") @@ -110,9 +128,9 @@ def test_enqueue_resume_dispatches_task_for_workflow(mocker: MockerFixture, mock def test_ensure_form_active_respects_global_timeout( - monkeypatch, sample_form_record: HumanInputFormRecord, mock_session_factory + monkeypatch, sample_form_record: HumanInputFormRecord, sqlite_session_factory ): - session_factory, _ = mock_session_factory + session_factory, _ = sqlite_session_factory service = HumanInputService(session_factory) expired_record = dataclasses.replace( sample_form_record, @@ -125,8 +143,13 @@ def test_ensure_form_active_respects_global_timeout( service.ensure_form_active(Form(expired_record)) -def test_enqueue_resume_dispatches_task_for_advanced_chat(mocker: MockerFixture, mock_session_factory): - session_factory, session = mock_session_factory +@pytest.mark.parametrize("sqlite_session", [(App,)], indirect=True) +def test_enqueue_resume_dispatches_task_for_advanced_chat( + mocker: MockerFixture, + sqlite_session_factory, + sqlite_session: Session, +): + session_factory, _ = sqlite_session_factory service = HumanInputService(session_factory) workflow_run = MagicMock() @@ -139,9 +162,7 @@ def test_enqueue_resume_dispatches_task_for_advanced_chat(mocker: MockerFixture, return_value=workflow_run_repo, ) - app = MagicMock() - app.mode = "advanced-chat" - session.execute.return_value.scalar_one_or_none.return_value = app + _persist_app(sqlite_session, AppMode.ADVANCED_CHAT) resume_task = mocker.patch("services.human_input_service.resume_app_execution") @@ -152,8 +173,13 @@ def test_enqueue_resume_dispatches_task_for_advanced_chat(mocker: MockerFixture, assert call_kwargs["kwargs"]["payload"]["workflow_run_id"] == "workflow-run-id" -def test_enqueue_resume_skips_unsupported_app_mode(mocker: MockerFixture, mock_session_factory): - session_factory, session = mock_session_factory +@pytest.mark.parametrize("sqlite_session", [(App,)], indirect=True) +def test_enqueue_resume_skips_unsupported_app_mode( + mocker: MockerFixture, + sqlite_session_factory, + sqlite_session: Session, +): + session_factory, _ = sqlite_session_factory service = HumanInputService(session_factory) workflow_run = MagicMock() @@ -166,9 +192,7 @@ def test_enqueue_resume_skips_unsupported_app_mode(mocker: MockerFixture, mock_s return_value=workflow_run_repo, ) - app = MagicMock() - app.mode = "completion" - session.execute.return_value.scalar_one_or_none.return_value = app + _persist_app(sqlite_session, AppMode.COMPLETION) resume_task = mocker.patch("services.human_input_service.resume_app_execution") @@ -178,9 +202,9 @@ def test_enqueue_resume_skips_unsupported_app_mode(mocker: MockerFixture, mock_s def test_get_form_definition_by_token_for_console_uses_repository( - sample_form_record: HumanInputFormRecord, mock_session_factory + sample_form_record: HumanInputFormRecord, sqlite_session_factory ): - session_factory, _ = mock_session_factory + session_factory, _ = sqlite_session_factory repo = MagicMock(spec=HumanInputFormSubmissionRepository) console_record = dataclasses.replace(sample_form_record, recipient_type=RecipientType.CONSOLE) repo.get_by_token.return_value = console_record @@ -221,9 +245,9 @@ def _build_resumption_context_state(*, options: list[str], workflow_run_id: str) def test_resolve_form_inputs_uses_runtime_select_options( - sample_form_record: HumanInputFormRecord, mock_session_factory, mocker: MockerFixture + sample_form_record: HumanInputFormRecord, sqlite_session_factory, mocker: MockerFixture ): - session_factory, _ = mock_session_factory + session_factory, _ = sqlite_session_factory configured_input = SelectInputConfig( output_variable_name="decision", option_source=StringListSource( @@ -260,9 +284,9 @@ def test_resolve_form_inputs_uses_runtime_select_options( def test_submit_form_by_token_calls_repository_and_enqueue( - sample_form_record: HumanInputFormRecord, mock_session_factory, mocker: MockerFixture + sample_form_record: HumanInputFormRecord, sqlite_session_factory, mocker: MockerFixture ): - session_factory, _ = mock_session_factory + session_factory, _ = sqlite_session_factory repo = MagicMock(spec=HumanInputFormSubmissionRepository) repo.get_by_token.return_value = sample_form_record repo.mark_submitted.return_value = sample_form_record @@ -289,11 +313,11 @@ def test_submit_form_by_token_calls_repository_and_enqueue( def test_submit_form_by_token_enqueues_agent_app_resume_for_conversation_form( - sample_form_record, mock_session_factory, mocker: MockerFixture + sample_form_record, sqlite_session_factory, mocker: MockerFixture ): # ENG-635: a conversation-owned (Agent v2 chat) form routes to the chat # resume, not the workflow resume. - session_factory, _ = mock_session_factory + session_factory, _ = sqlite_session_factory repo = MagicMock(spec=HumanInputFormSubmissionRepository) conversation_record = dataclasses.replace( sample_form_record, @@ -319,9 +343,9 @@ def test_submit_form_by_token_enqueues_agent_app_resume_for_conversation_form( def test_submit_form_by_token_skips_enqueue_for_delivery_test( - sample_form_record: HumanInputFormRecord, mock_session_factory, mocker: MockerFixture + sample_form_record: HumanInputFormRecord, sqlite_session_factory, mocker: MockerFixture ): - session_factory, _ = mock_session_factory + session_factory, _ = sqlite_session_factory repo = MagicMock(spec=HumanInputFormSubmissionRepository) test_record = dataclasses.replace( sample_form_record, @@ -344,9 +368,9 @@ def test_submit_form_by_token_skips_enqueue_for_delivery_test( def test_submit_form_by_token_passes_submission_user_id( - sample_form_record: HumanInputFormRecord, mock_session_factory, mocker: MockerFixture + sample_form_record: HumanInputFormRecord, sqlite_session_factory, mocker: MockerFixture ): - session_factory, _ = mock_session_factory + session_factory, _ = sqlite_session_factory repo = MagicMock(spec=HumanInputFormSubmissionRepository) repo.get_by_token.return_value = sample_form_record repo.mark_submitted.return_value = sample_form_record @@ -367,8 +391,8 @@ def test_submit_form_by_token_passes_submission_user_id( enqueue_spy.assert_called_once_with(sample_form_record.workflow_run_id) -def test_submit_form_by_token_invalid_action(sample_form_record: HumanInputFormRecord, mock_session_factory): - session_factory, _ = mock_session_factory +def test_submit_form_by_token_invalid_action(sample_form_record: HumanInputFormRecord, sqlite_session_factory): + session_factory, _ = sqlite_session_factory repo = MagicMock(spec=HumanInputFormSubmissionRepository) repo.get_by_token.return_value = dataclasses.replace(sample_form_record) service = HumanInputService(session_factory, form_repository=repo) @@ -385,8 +409,8 @@ def test_submit_form_by_token_invalid_action(sample_form_record: HumanInputFormR repo.mark_submitted.assert_not_called() -def test_submit_form_by_token_missing_inputs(sample_form_record: HumanInputFormRecord, mock_session_factory): - session_factory, _ = mock_session_factory +def test_submit_form_by_token_missing_inputs(sample_form_record: HumanInputFormRecord, sqlite_session_factory): + session_factory, _ = sqlite_session_factory repo = MagicMock(spec=HumanInputFormSubmissionRepository) definition_with_input = FormDefinition( @@ -412,8 +436,8 @@ def test_submit_form_by_token_missing_inputs(sample_form_record: HumanInputFormR repo.mark_submitted.assert_not_called() -def test_validate_human_input_submission_accepts_select_file_and_file_list(mock_session_factory): - session_factory, _ = mock_session_factory +def test_validate_human_input_submission_accepts_select_file_and_file_list(sqlite_session_factory): + session_factory, _ = sqlite_session_factory service = HumanInputService(session_factory) definition = FormDefinition.model_validate( { @@ -498,12 +522,12 @@ def test_validate_human_input_submission_accepts_select_file_and_file_list(mock_ ) def test_validate_human_input_submission_rejects_invalid_select_and_file_payloads( sample_form_record, - mock_session_factory, + sqlite_session_factory, input_definition, submitted_value, expected_message, ): - session_factory, _ = mock_session_factory + session_factory, _ = sqlite_session_factory repo = MagicMock(spec=HumanInputFormSubmissionRepository) definition = FormDefinition.model_validate( { @@ -549,16 +573,15 @@ def test_form_submitted_error_init(): assert error.code == 412 -def test_human_input_service_init_with_engine(mocker: MockerFixture): - engine = MagicMock(spec=human_input_service_module.Engine) - sessionmaker_mock = mocker.patch("services.human_input_service.sessionmaker") +def test_human_input_service_init_with_engine(sqlite_engine: Engine): + service = HumanInputService(session_factory=sqlite_engine) - HumanInputService(session_factory=engine) - sessionmaker_mock.assert_called_once_with(bind=engine) + assert isinstance(service._session_factory, sessionmaker) + assert service._session_factory.kw["bind"] is sqlite_engine -def test_get_form_by_token_none(mock_session_factory): - session_factory, _ = mock_session_factory +def test_get_form_by_token_none(sqlite_session_factory): + session_factory, _ = sqlite_session_factory repo = MagicMock(spec=HumanInputFormSubmissionRepository) repo.get_by_token.return_value = None @@ -566,8 +589,8 @@ def test_get_form_by_token_none(mock_session_factory): assert service.get_form_by_token("invalid") is None -def test_get_form_definition_by_token_mismatch(sample_form_record: HumanInputFormRecord, mock_session_factory): - session_factory, _ = mock_session_factory +def test_get_form_definition_by_token_mismatch(sample_form_record: HumanInputFormRecord, sqlite_session_factory): + session_factory, _ = sqlite_session_factory repo = MagicMock(spec=HumanInputFormSubmissionRepository) repo.get_by_token.return_value = sample_form_record @@ -576,8 +599,8 @@ def test_get_form_definition_by_token_mismatch(sample_form_record: HumanInputFor assert service.get_form_definition_by_token(RecipientType.CONSOLE, "token") is None -def test_get_form_definition_by_token_success(sample_form_record: HumanInputFormRecord, mock_session_factory): - session_factory, _ = mock_session_factory +def test_get_form_definition_by_token_success(sample_form_record: HumanInputFormRecord, sqlite_session_factory): + session_factory, _ = sqlite_session_factory repo = MagicMock(spec=HumanInputFormSubmissionRepository) repo.get_by_token.return_value = sample_form_record @@ -588,9 +611,9 @@ def test_get_form_definition_by_token_success(sample_form_record: HumanInputForm def test_get_form_definition_by_token_for_console_mismatch( - sample_form_record: HumanInputFormRecord, mock_session_factory + sample_form_record: HumanInputFormRecord, sqlite_session_factory ): - session_factory, _ = mock_session_factory + session_factory, _ = sqlite_session_factory repo = MagicMock(spec=HumanInputFormSubmissionRepository) repo.get_by_token.return_value = sample_form_record # is STANDALONE_WEB_APP @@ -598,8 +621,8 @@ def test_get_form_definition_by_token_for_console_mismatch( assert service.get_form_definition_by_token_for_console("token") is None -def test_submit_form_by_token_delivery_not_enabled(mock_session_factory): - session_factory, _ = mock_session_factory +def test_submit_form_by_token_delivery_not_enabled(sqlite_session_factory): + session_factory, _ = sqlite_session_factory repo = MagicMock(spec=HumanInputFormSubmissionRepository) repo.get_by_token.return_value = None @@ -609,9 +632,9 @@ def test_submit_form_by_token_delivery_not_enabled(mock_session_factory): def test_submit_form_by_token_no_workflow_run_id( - sample_form_record: HumanInputFormRecord, mock_session_factory, mocker: MockerFixture + sample_form_record: HumanInputFormRecord, sqlite_session_factory, mocker: MockerFixture ): - session_factory, _ = mock_session_factory + session_factory, _ = sqlite_session_factory repo = MagicMock(spec=HumanInputFormSubmissionRepository) repo.get_by_token.return_value = sample_form_record @@ -626,8 +649,8 @@ def test_submit_form_by_token_no_workflow_run_id( enqueue_spy.assert_not_called() -def test_ensure_form_active_errors(sample_form_record: HumanInputFormRecord, mock_session_factory): - session_factory, _ = mock_session_factory +def test_ensure_form_active_errors(sample_form_record: HumanInputFormRecord, sqlite_session_factory): + session_factory, _ = sqlite_session_factory service = HumanInputService(session_factory) # Submitted @@ -648,8 +671,8 @@ def test_ensure_form_active_errors(sample_form_record: HumanInputFormRecord, moc service.ensure_form_active(Form(expired_time_record)) -def test_ensure_not_submitted_raises(sample_form_record: HumanInputFormRecord, mock_session_factory): - session_factory, _ = mock_session_factory +def test_ensure_not_submitted_raises(sample_form_record: HumanInputFormRecord, sqlite_session_factory): + session_factory, _ = sqlite_session_factory service = HumanInputService(session_factory) submitted_record = dataclasses.replace(sample_form_record, submitted_at=naive_utc_now()) @@ -657,8 +680,8 @@ def test_ensure_not_submitted_raises(sample_form_record: HumanInputFormRecord, m service._ensure_not_submitted(Form(submitted_record)) -def test_enqueue_resume_workflow_not_found(mocker: MockerFixture, mock_session_factory): - session_factory, _ = mock_session_factory +def test_enqueue_resume_workflow_not_found(mocker: MockerFixture, sqlite_session_factory): + session_factory, _ = sqlite_session_factory service = HumanInputService(session_factory) workflow_run_repo = MagicMock() @@ -673,8 +696,14 @@ def test_enqueue_resume_workflow_not_found(mocker: MockerFixture, mock_session_f assert "WorkflowRun not found" in str(excinfo.value) -def test_enqueue_resume_app_not_found(mocker, mock_session_factory, caplog: pytest.LogCaptureFixture): - session_factory, session = mock_session_factory +@pytest.mark.parametrize("sqlite_session", [(App,)], indirect=True) +def test_enqueue_resume_app_not_found( + mocker, + sqlite_session_factory, + sqlite_session: Session, + caplog: pytest.LogCaptureFixture, +): + session_factory, _ = sqlite_session_factory service = HumanInputService(session_factory) workflow_run = MagicMock() @@ -687,17 +716,15 @@ def test_enqueue_resume_app_not_found(mocker, mock_session_factory, caplog: pyte return_value=workflow_run_repo, ) - session.execute.return_value.scalar_one_or_none.return_value = None - with caplog.at_level(logging.ERROR, logger="services.human_input_service"): service.enqueue_resume("workflow-run-id") assert any(r.levelno >= logging.ERROR for r in caplog.records) def test_is_globally_expired_zero_timeout( - monkeypatch: pytest.MonkeyPatch, sample_form_record: HumanInputFormRecord, mock_session_factory + monkeypatch: pytest.MonkeyPatch, sample_form_record: HumanInputFormRecord, sqlite_session_factory ): - session_factory, _ = mock_session_factory + session_factory, _ = sqlite_session_factory service = HumanInputService(session_factory) monkeypatch.setattr(human_input_service_module.dify_config, "HUMAN_INPUT_GLOBAL_TIMEOUT_SECONDS", 0) @@ -705,9 +732,9 @@ def test_is_globally_expired_zero_timeout( def test_submit_form_by_token_normalizes_select_and_files( - sample_form_record: HumanInputFormRecord, mock_session_factory, mocker: MockerFixture + sample_form_record: HumanInputFormRecord, sqlite_session_factory, mocker: MockerFixture ) -> None: - session_factory, _ = mock_session_factory + session_factory, _ = sqlite_session_factory repo = MagicMock(spec=HumanInputFormSubmissionRepository) definition = FormDefinition( form_content="hello", @@ -788,9 +815,9 @@ def test_submit_form_by_token_normalizes_select_and_files( def test_submit_form_by_token_invalid_select_value( - sample_form_record: HumanInputFormRecord, mock_session_factory + sample_form_record: HumanInputFormRecord, sqlite_session_factory ) -> None: - session_factory, _ = mock_session_factory + session_factory, _ = sqlite_session_factory repo = MagicMock(spec=HumanInputFormSubmissionRepository) definition = FormDefinition( form_content="hello", @@ -817,9 +844,9 @@ def test_submit_form_by_token_invalid_select_value( def test_submit_form_by_token_invalid_file_list_item( - sample_form_record: HumanInputFormRecord, mock_session_factory + sample_form_record: HumanInputFormRecord, sqlite_session_factory ) -> None: - session_factory, _ = mock_session_factory + session_factory, _ = sqlite_session_factory repo = MagicMock(spec=HumanInputFormSubmissionRepository) definition = FormDefinition( form_content="hello", @@ -844,9 +871,9 @@ def test_submit_form_by_token_invalid_file_list_item( def test_submit_form_by_token_rejects_cross_tenant_file( - sample_form_record: HumanInputFormRecord, mock_session_factory, mocker: MockerFixture + sample_form_record: HumanInputFormRecord, sqlite_session_factory, mocker: MockerFixture ) -> None: - session_factory, _ = mock_session_factory + session_factory, _ = sqlite_session_factory repo = MagicMock(spec=HumanInputFormSubmissionRepository) definition = FormDefinition( form_content="hello", @@ -877,9 +904,9 @@ def test_submit_form_by_token_rejects_cross_tenant_file( def test_submit_form_by_token_rejects_cross_tenant_file_list( - sample_form_record: HumanInputFormRecord, mock_session_factory, mocker: MockerFixture + sample_form_record: HumanInputFormRecord, sqlite_session_factory, mocker: MockerFixture ) -> None: - session_factory, _ = mock_session_factory + session_factory, _ = sqlite_session_factory repo = MagicMock(spec=HumanInputFormSubmissionRepository) definition = FormDefinition( form_content="hello",