mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
test: use real repositories for human input node fixtures (#43206)
This commit is contained in:
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)
|
||||
|
||||
Reference in new issue
Block a user