From 6cca398f1c62c6248cfe4790c1aac67cd57a72d7 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Fri, 2 Oct 2026 12:21:05 +0000 Subject: [PATCH] test: use real repositories for human input node fixtures (#43206) --- .../nodes/agent_v2/test_ask_human_hitl.py | 60 +++++----- .../nodes/human_input/test_entities.py | 104 +++++++++--------- 2 files changed, 86 insertions(+), 78 deletions(-) diff --git a/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_ask_human_hitl.py b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_ask_human_hitl.py index 3caa37cb3d5..52ca2d5c0ec 100644 --- a/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_ask_human_hitl.py +++ b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_ask_human_hitl.py @@ -3,13 +3,14 @@ from __future__ import annotations from typing import Any -from unittest.mock import MagicMock import pytest from dify_agent.layers.ask_human import AskHumanToolArgs from dify_agent.protocol import DeferredToolCallPayload +from pytest_mock import MockerFixture +from sqlalchemy.orm import Session, sessionmaker -from core.repositories.human_input_repository import FormCreateParams, HumanInputFormRepository +from core.repositories.human_input_repository import FormCreateParams, HumanInputFormRepositoryImpl from core.workflow.human_input_adapter import ( EmailDeliveryMethod, ExternalRecipient, @@ -43,10 +44,12 @@ def _deferred_call(args: dict[str, Any], *, tool_name: str = "ask_human") -> Def return DeferredToolCallPayload(tool_call_id="call-1", tool_name=tool_name, args=args) -def _fake_repository(form_id: str = "form-123") -> MagicMock: - repo = MagicMock(spec=HumanInputFormRepository) - repo.create_form.return_value = MagicMock(id=form_id) - return repo +@pytest.fixture +def repo(sqlite_session_factory: sessionmaker[Session], mocker: MockerFixture) -> HumanInputFormRepositoryImpl: + mocker.patch( + "core.repositories.human_input_repository.session_factory.create_session", side_effect=sqlite_session_factory + ) + return HumanInputFormRepositoryImpl(tenant_id="tenant-1", app_id="app-1", workflow_execution_id="wf-1") # ─────────────────────────── parse_ask_human_args ─────────────────────────── @@ -206,19 +209,19 @@ def test_delivery_high_urgency_prefixes_subject() -> None: # ─────────────────────────── build_ask_human_pause_reason ─────────────────── -def test_pause_reason_none_for_non_ask_human_tool() -> None: +def test_pause_reason_none_for_non_ask_human_tool(repo: HumanInputFormRepositoryImpl) -> None: result = build_ask_human_pause_reason( deferred_tool_call=_deferred_call({"question": "q"}, tool_name="final_output"), node_id="node-1", default_node_title="Agent", workflow_run_id="wf-1", contacts=[], - repository=_fake_repository(), + repository=repo, ) assert result is None -def test_pause_reason_requires_workflow_run_id() -> None: +def test_pause_reason_requires_workflow_run_id(repo: HumanInputFormRepositoryImpl) -> None: with pytest.raises(AskHumanFormBuildError): build_ask_human_pause_reason( deferred_tool_call=_deferred_call({"question": "q"}), @@ -226,12 +229,14 @@ def test_pause_reason_requires_workflow_run_id() -> None: default_node_title="Agent", workflow_run_id="", contacts=[], - repository=_fake_repository(), + repository=repo, ) -def test_pause_reason_builds_form_and_returns_dify_pause_reason() -> None: - repo = _fake_repository(form_id="form-xyz") +def test_pause_reason_builds_form_and_returns_dify_pause_reason( + repo: HumanInputFormRepositoryImpl, mocker: MockerFixture +) -> None: + create = mocker.spy(repo, "create_form") contacts = [AgentHumanContactConfig(email="a@x.com")] result = build_ask_human_pause_reason( @@ -251,14 +256,16 @@ def test_pause_reason_builds_form_and_returns_dify_pause_reason() -> None: ) assert result is not None - assert result.form_id == "form-xyz" + form = repo.get_form("node-1") + assert form is not None + assert result.form_id == form.id assert isinstance(result, HumanInputRequired) assert result.node_id == "node-1" assert result.node_title == "Approve?" # args.title wins over default assert [i.output_variable_name for i in result.inputs] == ["note"] assert [a.id for a in result.actions] == ["ok"] - params: FormCreateParams = repo.create_form.call_args.args[0] + params: FormCreateParams = create.call_args.args[0] assert params.workflow_execution_id == "wf-1" assert params.node_id == "node-1" # No conversation_id passed -> pure workflow run owns the form by workflow_run_id only. @@ -266,10 +273,12 @@ def test_pause_reason_builds_form_and_returns_dify_pause_reason() -> None: assert any(isinstance(m, EmailDeliveryMethod) for m in params.delivery_methods) -def test_pause_reason_forwards_conversation_id_for_chatflow() -> None: +def test_pause_reason_forwards_conversation_id_for_chatflow( + repo: HumanInputFormRepositoryImpl, mocker: MockerFixture +) -> None: # ENG-635 (review): an agent node running in a chatflow tags its ask_human form # with the conversation in addition to the workflow run. - repo = _fake_repository(form_id="form-xyz") + create = mocker.spy(repo, "create_form") build_ask_human_pause_reason( deferred_tool_call=_deferred_call({"question": "Please approve"}), @@ -281,26 +290,28 @@ def test_pause_reason_forwards_conversation_id_for_chatflow() -> None: repository=repo, ) - params: FormCreateParams = repo.create_form.call_args.args[0] + params: FormCreateParams = create.call_args.args[0] assert params.workflow_execution_id == "wf-1" assert params.conversation_id == "conv-1" -def test_pause_reason_falls_back_to_default_node_title() -> None: +def test_pause_reason_falls_back_to_default_node_title(repo: HumanInputFormRepositoryImpl) -> None: result = build_ask_human_pause_reason( deferred_tool_call=_deferred_call({"question": "q with no title"}), node_id="node-1", default_node_title="Agent fallback", workflow_run_id="wf-1", contacts=[], - repository=_fake_repository(), + repository=repo, ) assert result is not None assert result.node_title == "Agent fallback" -def test_pause_reason_select_default_flows_into_resolved_defaults() -> None: - repo = _fake_repository() +def test_pause_reason_select_default_flows_into_resolved_defaults( + repo: HumanInputFormRepositoryImpl, mocker: MockerFixture +) -> None: + create = mocker.spy(repo, "create_form") result = build_ask_human_pause_reason( deferred_tool_call=_deferred_call( { @@ -323,13 +334,12 @@ def test_pause_reason_select_default_flows_into_resolved_defaults() -> None: repository=repo, ) assert result is not None - params: FormCreateParams = repo.create_form.call_args.args[0] + params: FormCreateParams = create.call_args.args[0] assert result.resolved_default_values == {"tier": "t1"} -def test_pause_reason_wraps_repository_value_error() -> None: - repo = MagicMock(spec=HumanInputFormRepository) - repo.create_form.side_effect = ValueError("db boom") +def test_pause_reason_wraps_repository_value_error(repo: HumanInputFormRepositoryImpl, mocker: MockerFixture) -> None: + mocker.patch.object(repo, "create_form", side_effect=ValueError("db boom")) with pytest.raises(AskHumanFormBuildError): build_ask_human_pause_reason( deferred_tool_call=_deferred_call({"question": "q"}), diff --git a/api/tests/unit_tests/core/workflow/nodes/human_input/test_entities.py b/api/tests/unit_tests/core/workflow/nodes/human_input/test_entities.py index 4f70787052d..feb04a4a38a 100644 --- a/api/tests/unit_tests/core/workflow/nodes/human_input/test_entities.py +++ b/api/tests/unit_tests/core/workflow/nodes/human_input/test_entities.py @@ -5,12 +5,13 @@ Unit tests for human input node entities. from collections.abc import Mapping from dataclasses import dataclass, field from datetime import datetime, timedelta -from types import SimpleNamespace from typing import Any from unittest.mock import MagicMock import pytest from pydantic import ValidationError +from pytest_mock import MockerFixture +from sqlalchemy.orm import Session, sessionmaker from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY from core.repositories.human_input_repository import ( @@ -61,6 +62,8 @@ from graphon.nodes.protocols import FileReferenceFactoryProtocol from graphon.runtime import GraphRuntimeState, VariablePool from graphon.variables.segments import ArrayFileSegment, FileSegment, StringSegment from libs.datetime_utils import naive_utc_now +from models.account import TenantAccountJoin, TenantAccountRole +from tests.unit_tests.model_factories import make_account, make_tenant @dataclass @@ -467,7 +470,22 @@ class TestRecipients: class TestHumanInputNodeVariableResolution: """Tests for resolving variable-based defaults in HumanInputNode.""" - def test_resolves_variable_defaults(self): + @pytest.fixture(autouse=True) + def _bind_repository(self, sqlite_session_factory: sessionmaker[Session], mocker: MockerFixture) -> None: + mocker.patch( + "core.repositories.human_input_repository.session_factory.create_session", + side_effect=sqlite_session_factory, + ) + with sqlite_session_factory.begin() as session: + session.add_all( + [ + make_account(account_id="user-123"), + make_tenant(tenant_id="tenant"), + TenantAccountJoin(tenant_id="tenant", account_id="user-123", role=TenantAccountRole.NORMAL), + ] + ) + + def test_resolves_variable_defaults(self, mocker: MockerFixture): variable_pool = VariablePool.from_bootstrap( system_variables=build_system_variables( user_id="user", @@ -512,18 +530,13 @@ class TestHumanInputNodeVariableResolution: ) config = {"id": "human", "data": node_data.model_dump()} - mock_repo = MagicMock(spec=HumanInputFormRepository) - mock_repo.get_form.return_value = None - mock_repo.create_form.return_value = SimpleNamespace( - id="form-1", - rendered_content="Provide your name", - submission_token="token", - recipients=[], - submitted=False, + runtime = DifyHumanInputNodeRuntime( + graph_init_params.run_context, + workflow_execution_id_getter=lambda: "exec-1", ) - - runtime = DifyHumanInputNodeRuntime(graph_init_params.run_context) - runtime._build_form_repository = MagicMock(return_value=mock_repo) # type: ignore[attr-defined] + repository = runtime.build_form_repository() + create = mocker.spy(repository, "create_form") + runtime = runtime.with_form_repository(repository) node = _build_human_input_node( node_id=config["id"], node_data=config["data"], @@ -537,13 +550,13 @@ class TestHumanInputNodeVariableResolution: assert isinstance(pause_event, PauseRequestedEvent) expected_values = {"user_name": "Jane Doe"} - create_params = mock_repo.create_form.call_args.args[0] + create_params = create.call_args.args[0] assert create_params.resolved_default_values == expected_values - params = mock_repo.create_form.call_args.args[0] + params = create.call_args.args[0] assert params.resolved_default_values == expected_values - def test_debugger_falls_back_to_recipient_token_when_webapp_disabled(self): + def test_debugger_falls_back_to_recipient_token_when_webapp_disabled(self, mocker: MockerFixture): variable_pool = VariablePool.from_bootstrap( system_variables=build_system_variables( user_id="user", @@ -578,18 +591,13 @@ class TestHumanInputNodeVariableResolution: ) config = {"id": "human", "data": node_data.model_dump()} - mock_repo = MagicMock(spec=HumanInputFormRepository) - mock_repo.get_form.return_value = None - mock_repo.create_form.return_value = SimpleNamespace( - id="form-2", - rendered_content="Provide your name", - submission_token="console-token", - recipients=[SimpleNamespace(token="recipient-token")], - submitted=False, + runtime = DifyHumanInputNodeRuntime( + graph_init_params.run_context, + workflow_execution_id_getter=lambda: "exec-2", ) - - runtime = DifyHumanInputNodeRuntime(graph_init_params.run_context) - runtime._build_form_repository = MagicMock(return_value=mock_repo) # type: ignore[attr-defined] + repository = runtime.build_form_repository() + create = mocker.spy(repository, "create_form") + runtime = runtime.with_form_repository(repository) node = _build_human_input_node( node_id=config["id"], node_data=config["data"], @@ -604,7 +612,7 @@ class TestHumanInputNodeVariableResolution: assert isinstance(pause_event, PauseRequestedEvent) assert not hasattr(pause_event.reason, "form_token") - def test_webapp_runtime_keeps_form_visible_in_ui_when_webapp_delivery_is_enabled(self): + def test_webapp_runtime_keeps_form_visible_in_ui_when_webapp_delivery_is_enabled(self, mocker: MockerFixture): variable_pool = VariablePool.from_bootstrap( system_variables=build_system_variables( user_id="user", @@ -643,18 +651,13 @@ class TestHumanInputNodeVariableResolution: }, } - mock_repo = MagicMock(spec=HumanInputFormRepository) - mock_repo.get_form.return_value = None - mock_repo.create_form.return_value = SimpleNamespace( - id="form-4", - rendered_content="Provide your name", - submission_token="token", - recipients=[], - submitted=False, + runtime = DifyHumanInputNodeRuntime( + graph_init_params.run_context, + workflow_execution_id_getter=lambda: "exec-4", ) - - runtime = DifyHumanInputNodeRuntime(graph_init_params.run_context) - runtime._build_form_repository = MagicMock(return_value=mock_repo) # type: ignore[attr-defined] + repository = runtime.build_form_repository() + create = mocker.spy(repository, "create_form") + runtime = runtime.with_form_repository(repository) node = _build_human_input_node( node_id=config["id"], node_data=config["data"], @@ -667,10 +670,10 @@ class TestHumanInputNodeVariableResolution: pause_event = next(run_result) assert isinstance(pause_event, PauseRequestedEvent) - params = mock_repo.create_form.call_args.args[0] + params = create.call_args.args[0] assert params.display_in_ui is True - def test_debugger_debug_mode_overrides_email_recipients(self): + def test_debugger_debug_mode_overrides_email_recipients(self, mocker: MockerFixture): variable_pool = VariablePool.from_bootstrap( system_variables=build_system_variables( user_id="user-123", @@ -719,18 +722,13 @@ class TestHumanInputNodeVariableResolution: ) config = {"id": "human", "data": node_data.model_dump()} - mock_repo = MagicMock(spec=HumanInputFormRepository) - mock_repo.get_form.return_value = None - mock_repo.create_form.return_value = SimpleNamespace( - id="form-3", - rendered_content="Provide your name", - submission_token="token", - recipients=[], - submitted=False, + runtime = DifyHumanInputNodeRuntime( + graph_init_params.run_context, + workflow_execution_id_getter=lambda: "exec-3", ) - - runtime = DifyHumanInputNodeRuntime(graph_init_params.run_context) - runtime._build_form_repository = MagicMock(return_value=mock_repo) # type: ignore[attr-defined] + repository = runtime.build_form_repository() + create = mocker.spy(repository, "create_form") + runtime = runtime.with_form_repository(repository) node = _build_human_input_node( node_id=config["id"], node_data=config["data"], @@ -743,7 +741,7 @@ class TestHumanInputNodeVariableResolution: pause_event = next(run_result) assert isinstance(pause_event, PauseRequestedEvent) - params = mock_repo.create_form.call_args.args[0] + params = create.call_args.args[0] assert len(params.delivery_methods) == 1 method = params.delivery_methods[0] assert isinstance(method, EmailDeliveryMethod)