test: use real repositories for human input node fixtures (#43206)

This commit is contained in:
Asuka Minato authored and GitHub committed 2026-10-02 12:21:05 +00:00
1 parent b4ae43986c
commit 6cca398f1c
2 files changed
+86 -78

No files matched your search

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