revert: revert human input relevant code (#31766)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
QuantumGhost
2026-01-30 19:18:49 +08:00
committed by GitHub
parent ba568a634d
commit 90fe9abab7
470 changed files with 2082 additions and 32508 deletions

View File

@ -16,9 +16,11 @@ if not hasattr(builtins, "MethodView"):
builtins.MethodView = MethodView # type: ignore[attr-defined]
@pytest.fixture(scope="module")
def app_module():
def _load_app_module():
module_name = "controllers.console.app.app"
if module_name in sys.modules:
return sys.modules[module_name]
root = Path(__file__).resolve().parents[5]
module_path = root / "controllers" / "console" / "app" / "app.py"
@ -57,12 +59,8 @@ def app_module():
stub_namespace = _StubNamespace()
original_modules: dict[str, ModuleType | None] = {
"controllers.console": sys.modules.get("controllers.console"),
"controllers.console.app": sys.modules.get("controllers.console.app"),
"controllers.common.schema": sys.modules.get("controllers.common.schema"),
module_name: sys.modules.get(module_name),
}
original_console = sys.modules.get("controllers.console")
original_app_pkg = sys.modules.get("controllers.console.app")
stubbed_modules: list[tuple[str, ModuleType | None]] = []
console_module = ModuleType("controllers.console")
@ -107,35 +105,35 @@ def app_module():
module = util.module_from_spec(spec)
sys.modules[module_name] = module
assert spec.loader is not None
spec.loader.exec_module(module)
try:
yield module
assert spec.loader is not None
spec.loader.exec_module(module)
finally:
for name, original in reversed(stubbed_modules):
if original is not None:
sys.modules[name] = original
else:
sys.modules.pop(name, None)
for name, original in original_modules.items():
if original is not None:
sys.modules[name] = original
else:
sys.modules.pop(name, None)
if original_console is not None:
sys.modules["controllers.console"] = original_console
else:
sys.modules.pop("controllers.console", None)
if original_app_pkg is not None:
sys.modules["controllers.console.app"] = original_app_pkg
else:
sys.modules.pop("controllers.console.app", None)
return module
@pytest.fixture(scope="module")
def app_models(app_module):
return SimpleNamespace(
AppDetailWithSite=app_module.AppDetailWithSite,
AppPagination=app_module.AppPagination,
AppPartial=app_module.AppPartial,
)
_app_module = _load_app_module()
AppDetailWithSite = _app_module.AppDetailWithSite
AppPagination = _app_module.AppPagination
AppPartial = _app_module.AppPartial
@pytest.fixture(autouse=True)
def patch_signed_url(monkeypatch, app_module):
def patch_signed_url(monkeypatch):
"""Ensure icon URL generation uses a deterministic helper for tests."""
def _fake_signed_url(key: str | None) -> str | None:
@ -143,7 +141,7 @@ def patch_signed_url(monkeypatch, app_module):
return None
return f"signed:{key}"
monkeypatch.setattr(app_module.file_helpers, "get_signed_file_url", _fake_signed_url)
monkeypatch.setattr(_app_module.file_helpers, "get_signed_file_url", _fake_signed_url)
def _ts(hour: int = 12) -> datetime:
@ -171,8 +169,7 @@ def _dummy_workflow():
)
def test_app_partial_serialization_uses_aliases(app_models):
AppPartial = app_models.AppPartial
def test_app_partial_serialization_uses_aliases():
created_at = _ts()
app_obj = SimpleNamespace(
id="app-1",
@ -207,8 +204,7 @@ def test_app_partial_serialization_uses_aliases(app_models):
assert serialized["tags"][0]["name"] == "Utilities"
def test_app_detail_with_site_includes_nested_serialization(app_models):
AppDetailWithSite = app_models.AppDetailWithSite
def test_app_detail_with_site_includes_nested_serialization():
timestamp = _ts(14)
site = SimpleNamespace(
code="site-code",
@ -257,8 +253,7 @@ def test_app_detail_with_site_includes_nested_serialization(app_models):
assert serialized["site"]["created_at"] == int(timestamp.timestamp())
def test_app_pagination_aliases_per_page_and_has_next(app_models):
AppPagination = app_models.AppPagination
def test_app_pagination_aliases_per_page_and_has_next():
item_one = SimpleNamespace(
id="app-10",
name="Paginated One",

View File

@ -1,229 +0,0 @@
from __future__ import annotations
from dataclasses import dataclass
from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
from flask import Flask
from pydantic import ValidationError
from controllers.console import wraps as console_wraps
from controllers.console.app import workflow as workflow_module
from controllers.console.app import wraps as app_wraps
from libs import login as login_lib
from models.account import Account, AccountStatus, TenantAccountRole
from models.model import AppMode
def _make_account() -> Account:
account = Account(name="tester", email="tester@example.com")
account.status = AccountStatus.ACTIVE
account.role = TenantAccountRole.OWNER
account.id = "account-123" # type: ignore[assignment]
account._current_tenant = SimpleNamespace(id="tenant-123") # type: ignore[attr-defined]
account._get_current_object = lambda: account # type: ignore[attr-defined]
return account
def _make_app(mode: AppMode) -> SimpleNamespace:
return SimpleNamespace(id="app-123", tenant_id="tenant-123", mode=mode.value)
def _patch_console_guards(monkeypatch: pytest.MonkeyPatch, account: Account, app_model: SimpleNamespace) -> None:
# Skip setup and auth guardrails
monkeypatch.setattr("configs.dify_config.EDITION", "CLOUD")
monkeypatch.setattr(login_lib.dify_config, "LOGIN_DISABLED", True)
monkeypatch.setattr(login_lib, "current_user", account)
monkeypatch.setattr(login_lib, "current_account_with_tenant", lambda: (account, account.current_tenant_id))
monkeypatch.setattr(login_lib, "check_csrf_token", lambda *_, **__: None)
monkeypatch.setattr(console_wraps, "current_account_with_tenant", lambda: (account, account.current_tenant_id))
monkeypatch.setattr(app_wraps, "current_account_with_tenant", lambda: (account, account.current_tenant_id))
monkeypatch.setattr(workflow_module, "current_account_with_tenant", lambda: (account, account.current_tenant_id))
monkeypatch.setattr(console_wraps.dify_config, "EDITION", "CLOUD")
monkeypatch.delenv("INIT_PASSWORD", raising=False)
# Avoid hitting the database when resolving the app model
monkeypatch.setattr(app_wraps, "_load_app_model", lambda _app_id: app_model)
@dataclass
class PreviewCase:
resource_cls: type
path: str
mode: AppMode
@pytest.mark.parametrize(
"case",
[
PreviewCase(
resource_cls=workflow_module.AdvancedChatDraftHumanInputFormPreviewApi,
path="/console/api/apps/app-123/advanced-chat/workflows/draft/human-input/nodes/node-42/form/preview",
mode=AppMode.ADVANCED_CHAT,
),
PreviewCase(
resource_cls=workflow_module.WorkflowDraftHumanInputFormPreviewApi,
path="/console/api/apps/app-123/workflows/draft/human-input/nodes/node-42/form/preview",
mode=AppMode.WORKFLOW,
),
],
)
def test_human_input_preview_delegates_to_service(
app: Flask, monkeypatch: pytest.MonkeyPatch, case: PreviewCase
) -> None:
account = _make_account()
app_model = _make_app(case.mode)
_patch_console_guards(monkeypatch, account, app_model)
preview_payload = {
"form_id": "node-42",
"form_content": "<div>example</div>",
"inputs": [{"name": "topic"}],
"actions": [{"id": "continue"}],
}
service_instance = MagicMock()
service_instance.get_human_input_form_preview.return_value = preview_payload
monkeypatch.setattr(workflow_module, "WorkflowService", MagicMock(return_value=service_instance))
with app.test_request_context(case.path, method="POST", json={"inputs": {"topic": "tech"}}):
response = case.resource_cls().post(app_id=app_model.id, node_id="node-42")
assert response == preview_payload
service_instance.get_human_input_form_preview.assert_called_once_with(
app_model=app_model,
account=account,
node_id="node-42",
inputs={"topic": "tech"},
)
@dataclass
class SubmitCase:
resource_cls: type
path: str
mode: AppMode
@pytest.mark.parametrize(
"case",
[
SubmitCase(
resource_cls=workflow_module.AdvancedChatDraftHumanInputFormRunApi,
path="/console/api/apps/app-123/advanced-chat/workflows/draft/human-input/nodes/node-99/form/run",
mode=AppMode.ADVANCED_CHAT,
),
SubmitCase(
resource_cls=workflow_module.WorkflowDraftHumanInputFormRunApi,
path="/console/api/apps/app-123/workflows/draft/human-input/nodes/node-99/form/run",
mode=AppMode.WORKFLOW,
),
],
)
def test_human_input_submit_forwards_payload(app: Flask, monkeypatch: pytest.MonkeyPatch, case: SubmitCase) -> None:
account = _make_account()
app_model = _make_app(case.mode)
_patch_console_guards(monkeypatch, account, app_model)
result_payload = {"node_id": "node-99", "outputs": {"__rendered_content": "<p>done</p>"}, "action": "approve"}
service_instance = MagicMock()
service_instance.submit_human_input_form_preview.return_value = result_payload
monkeypatch.setattr(workflow_module, "WorkflowService", MagicMock(return_value=service_instance))
with app.test_request_context(
case.path,
method="POST",
json={"form_inputs": {"answer": "42"}, "inputs": {"#node-1.result#": "LLM output"}, "action": "approve"},
):
response = case.resource_cls().post(app_id=app_model.id, node_id="node-99")
assert response == result_payload
service_instance.submit_human_input_form_preview.assert_called_once_with(
app_model=app_model,
account=account,
node_id="node-99",
form_inputs={"answer": "42"},
inputs={"#node-1.result#": "LLM output"},
action="approve",
)
@dataclass
class DeliveryTestCase:
resource_cls: type
path: str
mode: AppMode
@pytest.mark.parametrize(
"case",
[
DeliveryTestCase(
resource_cls=workflow_module.WorkflowDraftHumanInputDeliveryTestApi,
path="/console/api/apps/app-123/workflows/draft/human-input/nodes/node-7/delivery-test",
mode=AppMode.ADVANCED_CHAT,
),
DeliveryTestCase(
resource_cls=workflow_module.WorkflowDraftHumanInputDeliveryTestApi,
path="/console/api/apps/app-123/workflows/draft/human-input/nodes/node-7/delivery-test",
mode=AppMode.WORKFLOW,
),
],
)
def test_human_input_delivery_test_calls_service(
app: Flask, monkeypatch: pytest.MonkeyPatch, case: DeliveryTestCase
) -> None:
account = _make_account()
app_model = _make_app(case.mode)
_patch_console_guards(monkeypatch, account, app_model)
service_instance = MagicMock()
monkeypatch.setattr(workflow_module, "WorkflowService", MagicMock(return_value=service_instance))
with app.test_request_context(
case.path,
method="POST",
json={"delivery_method_id": "delivery-123"},
):
response = case.resource_cls().post(app_id=app_model.id, node_id="node-7")
assert response == {}
service_instance.test_human_input_delivery.assert_called_once_with(
app_model=app_model,
account=account,
node_id="node-7",
delivery_method_id="delivery-123",
inputs={},
)
def test_human_input_delivery_test_maps_validation_error(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
account = _make_account()
app_model = _make_app(AppMode.ADVANCED_CHAT)
_patch_console_guards(monkeypatch, account, app_model)
service_instance = MagicMock()
service_instance.test_human_input_delivery.side_effect = ValueError("bad delivery method")
monkeypatch.setattr(workflow_module, "WorkflowService", MagicMock(return_value=service_instance))
with app.test_request_context(
"/console/api/apps/app-123/workflows/draft/human-input/nodes/node-1/delivery-test",
method="POST",
json={"delivery_method_id": "bad"},
):
with pytest.raises(ValueError):
workflow_module.WorkflowDraftHumanInputDeliveryTestApi().post(app_id=app_model.id, node_id="node-1")
def test_human_input_preview_rejects_non_mapping(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
account = _make_account()
app_model = _make_app(AppMode.ADVANCED_CHAT)
_patch_console_guards(monkeypatch, account, app_model)
with app.test_request_context(
"/console/api/apps/app-123/advanced-chat/workflows/draft/human-input/nodes/node-1/form/preview",
method="POST",
json={"inputs": ["not-a-dict"]},
):
with pytest.raises(ValidationError):
workflow_module.AdvancedChatDraftHumanInputFormPreviewApi().post(app_id=app_model.id, node_id="node-1")

View File

@ -1,91 +0,0 @@
from __future__ import annotations
from datetime import datetime
from types import SimpleNamespace
from unittest.mock import Mock
import pytest
from flask import Flask
from controllers.console import wraps as console_wraps
from controllers.console.app import workflow_run as workflow_run_module
from core.workflow.entities.pause_reason import HumanInputRequired
from core.workflow.enums import WorkflowExecutionStatus
from core.workflow.nodes.human_input.entities import FormInput, UserAction
from core.workflow.nodes.human_input.enums import FormInputType
from libs import login as login_lib
from models.account import Account, AccountStatus, TenantAccountRole
from models.workflow import WorkflowRun
def _make_account() -> Account:
account = Account(name="tester", email="tester@example.com")
account.status = AccountStatus.ACTIVE
account.role = TenantAccountRole.OWNER
account.id = "account-123" # type: ignore[assignment]
account._current_tenant = SimpleNamespace(id="tenant-123") # type: ignore[attr-defined]
account._get_current_object = lambda: account # type: ignore[attr-defined]
return account
def _patch_console_guards(monkeypatch: pytest.MonkeyPatch, account: Account) -> None:
monkeypatch.setattr(login_lib.dify_config, "LOGIN_DISABLED", True)
monkeypatch.setattr(login_lib, "current_user", account)
monkeypatch.setattr(login_lib, "current_account_with_tenant", lambda: (account, account.current_tenant_id))
monkeypatch.setattr(login_lib, "check_csrf_token", lambda *_, **__: None)
monkeypatch.setattr(console_wraps, "current_account_with_tenant", lambda: (account, account.current_tenant_id))
monkeypatch.setattr(workflow_run_module, "current_user", account)
monkeypatch.setattr(console_wraps.dify_config, "EDITION", "CLOUD")
class _PauseEntity:
def __init__(self, paused_at: datetime, reasons: list[HumanInputRequired]):
self.paused_at = paused_at
self._reasons = reasons
def get_pause_reasons(self):
return self._reasons
def test_pause_details_returns_backstage_input_url(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
account = _make_account()
_patch_console_guards(monkeypatch, account)
monkeypatch.setattr(workflow_run_module.dify_config, "APP_WEB_URL", "https://web.example.com")
workflow_run = Mock(spec=WorkflowRun)
workflow_run.status = WorkflowExecutionStatus.PAUSED
workflow_run.created_at = datetime(2024, 1, 1, 12, 0, 0)
fake_db = SimpleNamespace(engine=Mock(), session=SimpleNamespace(get=lambda *_: workflow_run))
monkeypatch.setattr(workflow_run_module, "db", fake_db)
reason = HumanInputRequired(
form_id="form-1",
form_content="content",
inputs=[FormInput(type=FormInputType.TEXT_INPUT, output_variable_name="name")],
actions=[UserAction(id="approve", title="Approve")],
node_id="node-1",
node_title="Ask Name",
form_token="backstage-token",
)
pause_entity = _PauseEntity(paused_at=datetime(2024, 1, 1, 12, 0, 0), reasons=[reason])
repo = Mock()
repo.get_workflow_pause.return_value = pause_entity
monkeypatch.setattr(
workflow_run_module.DifyAPIRepositoryFactory,
"create_api_workflow_run_repository",
lambda *_, **__: repo,
)
with app.test_request_context("/console/api/workflow/run-1/pause-details", method="GET"):
response, status = workflow_run_module.ConsoleWorkflowPauseDetailsApi().get(workflow_run_id="run-1")
assert status == 200
assert response["paused_at"] == "2024-01-01T12:00:00Z"
assert response["paused_nodes"][0]["node_id"] == "node-1"
assert response["paused_nodes"][0]["pause_type"]["type"] == "human_input"
assert (
response["paused_nodes"][0]["pause_type"]["backstage_input_url"]
== "https://web.example.com/form/backstage-token"
)
assert "pending_human_inputs" not in response