test: persist real human input forms in callback tests (#43204)

This commit is contained in:
Asuka Minato authored and GitHub committed 2026-10-02 11:59:33 +00:00
1 parent 84cc8ef77a
commit c05a1c4e9a
1 file changed
+97 -36
@@ -1,13 +1,14 @@
from __future__ import annotations
import json
from dataclasses import dataclass, field
from datetime import timedelta
from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
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.nodes.human_input.callback import DifyHITLCallback
from core.workflow.nodes.human_input.entities import HumanInputNodeData, ParagraphInputConfig, UserActionConfig
from core.workflow.nodes.human_input.enums import HumanInputFormStatus
@@ -15,6 +16,7 @@ from core.workflow.nodes.human_input.session_binding import SessionBinding
from graphon.runtime import VariablePool
from graphon.variables.factory import build_segment
from libs.datetime_utils import naive_utc_now
from models.human_input import HumanInputForm
@dataclass(frozen=True, slots=True)
@@ -29,6 +31,17 @@ def _ctx(workflow_execution_id: str, node_id: str, node_title: str = "Human Inpu
return _Context(workflow_execution_id=workflow_execution_id, node_id=node_id, node_title=node_title)
@pytest.fixture
def repository(
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="run-1")
def test_session_binding_identity_mapping() -> None:
binding = SessionBinding()
@@ -36,10 +49,10 @@ def test_session_binding_identity_mapping() -> None:
assert binding.resolve_form_id_from_session_id(session_id="form-1") == "form-1"
def test_dify_hitl_callback_creates_pause_requested_for_new_form() -> None:
repository = MagicMock(spec=HumanInputFormRepository)
repository.get_form.return_value = None
repository.create_form.return_value = SimpleNamespace(id="form-1")
def test_dify_hitl_callback_creates_pause_requested_for_new_form(
repository: HumanInputFormRepositoryImpl, mocker: MockerFixture
) -> None:
create = mocker.spy(repository, "create_form")
callback = DifyHITLCallback(
form_repository=repository,
node_data=HumanInputNodeData(
@@ -53,16 +66,20 @@ def test_dify_hitl_callback_creates_pause_requested_for_new_form() -> None:
decision = callback(_ctx("run-1", "node-1"))
assert decision == callback.pause_requested_type(session_id="form-1")
params: FormCreateParams = repository.create_form.call_args.args[0]
assert decision == callback.pause_requested_type(session_id=create.spy_return.id)
form = repository.get_form("node-1")
assert form is not None
assert form.id == decision.session_id
params: FormCreateParams = create.call_args.args[0]
assert params.workflow_execution_id == "run-1"
assert params.node_id == "node-1"
def test_dify_hitl_callback_scopes_form_to_node_execution() -> None:
repository = MagicMock(spec=HumanInputFormRepository)
repository.get_form.return_value = None
repository.create_form.return_value = SimpleNamespace(id="execution-1")
def test_dify_hitl_callback_scopes_form_to_node_execution(
repository: HumanInputFormRepositoryImpl, mocker: MockerFixture
) -> None:
create = mocker.spy(repository, "create_form")
lookup = mocker.spy(repository, "get_form")
callback = DifyHITLCallback(
form_repository=repository,
node_data=HumanInputNodeData(
@@ -75,22 +92,34 @@ def test_dify_hitl_callback_scopes_form_to_node_execution() -> None:
callback(_ctx("run-1", "node-1"))
repository.get_form.assert_called_once_with("node-1", form_id="execution-1")
params: FormCreateParams = repository.create_form.call_args.args[0]
lookup.assert_called_once_with("node-1", form_id="execution-1")
params: FormCreateParams = create.call_args.args[0]
assert params.form_id == "execution-1"
form = repository.get_form("node-1", form_id="execution-1")
assert form is not None
assert form.id == "execution-1"
def test_dify_hitl_callback_returns_completed_for_submitted_form() -> None:
repository = MagicMock(spec=HumanInputFormRepository)
repository.get_form.return_value = SimpleNamespace(
def test_dify_hitl_callback_returns_completed_for_submitted_form(
repository: HumanInputFormRepositoryImpl, sqlite_session: Session
) -> None:
form = HumanInputForm(
id="form-1",
rendered_content="<p>Please approve</p>",
selected_action_id="approve",
submitted_data={"answer": "yes"},
submitted=True,
submitted_data=json.dumps({"answer": "yes"}),
status=HumanInputFormStatus.SUBMITTED,
expiration_time=naive_utc_now() + timedelta(hours=1),
submitted_at=naive_utc_now(),
created_at=naive_utc_now(),
tenant_id="tenant-1",
app_id="app-1",
workflow_run_id="run-1",
node_id="node-1",
form_definition="{}",
)
sqlite_session.add(form)
sqlite_session.commit()
callback = DifyHITLCallback(
form_repository=repository,
node_data=HumanInputNodeData(
@@ -113,18 +142,26 @@ def test_dify_hitl_callback_returns_completed_for_submitted_form() -> None:
}
def test_dify_hitl_callback_returns_timeout_for_explicit_timeout_form() -> None:
repository = MagicMock(spec=HumanInputFormRepository)
repository.get_form.return_value = SimpleNamespace(
def test_dify_hitl_callback_returns_timeout_for_explicit_timeout_form(
repository: HumanInputFormRepositoryImpl, sqlite_session: Session
) -> None:
form = HumanInputForm(
id="form-1",
rendered_content="<p>Please approve</p>",
selected_action_id=None,
submitted_data=None,
submitted=False,
status=HumanInputFormStatus.TIMEOUT,
created_at=naive_utc_now(),
expiration_time=naive_utc_now() + timedelta(hours=1),
submitted_at=None,
tenant_id="tenant-1",
app_id="app-1",
workflow_run_id="run-1",
node_id="node-1",
form_definition="{}",
)
sqlite_session.add(form)
sqlite_session.commit()
callback = DifyHITLCallback(
form_repository=repository,
node_data=HumanInputNodeData(title="Approval", form_content="Please approve"),
@@ -140,18 +177,26 @@ def test_dify_hitl_callback_returns_timeout_for_explicit_timeout_form() -> None:
}
def test_dify_hitl_callback_returns_timeout_for_waiting_form_past_node_deadline() -> None:
repository = MagicMock(spec=HumanInputFormRepository)
repository.get_form.return_value = SimpleNamespace(
def test_dify_hitl_callback_returns_timeout_for_waiting_form_past_node_deadline(
repository: HumanInputFormRepositoryImpl, sqlite_session: Session
) -> None:
form = HumanInputForm(
id="form-1",
rendered_content="<p>Please approve</p>",
selected_action_id=None,
submitted_data=None,
submitted=False,
status=HumanInputFormStatus.WAITING,
created_at=naive_utc_now(),
expiration_time=naive_utc_now() - timedelta(minutes=1),
submitted_at=None,
tenant_id="tenant-1",
app_id="app-1",
workflow_run_id="run-1",
node_id="node-1",
form_definition="{}",
)
sqlite_session.add(form)
sqlite_session.commit()
callback = DifyHITLCallback(
form_repository=repository,
node_data=HumanInputNodeData(title="Approval", form_content="Please approve"),
@@ -167,18 +212,26 @@ def test_dify_hitl_callback_returns_timeout_for_waiting_form_past_node_deadline(
}
def test_dify_hitl_callback_rejects_expired_form_as_invalid_resume_state() -> None:
repository = MagicMock(spec=HumanInputFormRepository)
repository.get_form.return_value = SimpleNamespace(
def test_dify_hitl_callback_rejects_expired_form_as_invalid_resume_state(
repository: HumanInputFormRepositoryImpl, sqlite_session: Session
) -> None:
form = HumanInputForm(
id="form-1",
rendered_content="<p>Please approve</p>",
selected_action_id=None,
submitted_data=None,
submitted=False,
status=HumanInputFormStatus.EXPIRED,
created_at=naive_utc_now() - timedelta(days=8),
expiration_time=naive_utc_now() + timedelta(hours=1),
submitted_at=None,
tenant_id="tenant-1",
app_id="app-1",
workflow_run_id="run-1",
node_id="node-1",
form_definition="{}",
)
sqlite_session.add(form)
sqlite_session.commit()
callback = DifyHITLCallback(
form_repository=repository,
node_data=HumanInputNodeData(title="Approval", form_content="Please approve"),
@@ -188,18 +241,26 @@ def test_dify_hitl_callback_rejects_expired_form_as_invalid_resume_state() -> No
callback(_ctx("run-1", "node-1"))
def test_dify_hitl_callback_rejects_waiting_form_past_global_deadline_as_invalid_resume_state() -> None:
repository = MagicMock(spec=HumanInputFormRepository)
repository.get_form.return_value = SimpleNamespace(
def test_dify_hitl_callback_rejects_waiting_form_past_global_deadline_as_invalid_resume_state(
repository: HumanInputFormRepositoryImpl, sqlite_session: Session
) -> None:
form = HumanInputForm(
id="form-1",
rendered_content="<p>Please approve</p>",
selected_action_id=None,
submitted_data=None,
submitted=False,
status=HumanInputFormStatus.WAITING,
created_at=naive_utc_now() - timedelta(days=8),
expiration_time=naive_utc_now() + timedelta(hours=1),
submitted_at=None,
tenant_id="tenant-1",
app_id="app-1",
workflow_run_id="run-1",
node_id="node-1",
form_definition="{}",
)
sqlite_session.add(form)
sqlite_session.commit()
callback = DifyHITLCallback(
form_repository=repository,
node_data=HumanInputNodeData(title="Approval", form_content="Please approve"),