test: use sqlite3 session in test_human_input_service (#38697)

This commit is contained in:
Asuka Minato 2026-07-25 12:17:57 +09:00 committed by GitHub
parent 8906a49e56
commit fa000ae8a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

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