From aecbd95291eb9adb387d5a0842f527bf810066e4 Mon Sep 17 00:00:00 2001 From: Byron Wang Date: Thu, 8 Oct 2026 18:38:55 +0800 Subject: [PATCH 1/7] fix(api): recover persisted workflow draft file cleanup requests --- api/configs/feature/__init__.py | 8 +++++ api/extensions/ext_celery.py | 6 ++++ .../test_draft_variable_cleanup_schedule.py | 36 +++++++++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 api/tests/unit_tests/extensions/test_draft_variable_cleanup_schedule.py diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index bf05625453dd5b..a98ee5439c2891 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -1432,6 +1432,14 @@ class CeleryScheduleTasksConfig(BaseSettings): description="Maximum seconds to start a trigger-debug worker or renew its running lease", default=600, ) + ENABLE_WORKFLOW_DRAFT_FILE_CLEANUP_TASK: bool = Field( + description="Enable recovery of persisted workflow draft upload cleanup requests", + default=True, + ) + WORKFLOW_DRAFT_FILE_CLEANUP_INTERVAL: PositiveInt = Field( + description="Workflow draft upload cleanup recovery interval in minutes", + default=5, + ) ENABLE_CONVERSATION_CLEANUP_TASK: bool = Field( description="Enable periodic recovery of soft-deleted conversation cleanup", default=True, diff --git a/api/extensions/ext_celery.py b/api/extensions/ext_celery.py index 6f5312afd5cc4b..97aeded6637f0d 100644 --- a/api/extensions/ext_celery.py +++ b/api/extensions/ext_celery.py @@ -209,6 +209,12 @@ def __call__(self, *args: object, **kwargs: object) -> object: "schedule": collection_schedule, "options": {"expires": interval}, } + if dify_config.ENABLE_WORKFLOW_DRAFT_FILE_CLEANUP_TASK: + imports.append("tasks.workflow_draft_var_tasks") + beat_schedule["workflow_draft_file_cleanup"] = { + "task": "tasks.workflow_draft_var_tasks.recover_draft_variable_file_cleanup_task", + "schedule": timedelta(minutes=dify_config.WORKFLOW_DRAFT_FILE_CLEANUP_INTERVAL), + } if dify_config.ENABLE_CONVERSATION_CLEANUP_TASK: imports.append("tasks.delete_conversation_task") beat_schedule["conversation_cleanup_sweeper"] = { diff --git a/api/tests/unit_tests/extensions/test_draft_variable_cleanup_schedule.py b/api/tests/unit_tests/extensions/test_draft_variable_cleanup_schedule.py new file mode 100644 index 00000000000000..fbd461014a8621 --- /dev/null +++ b/api/tests/unit_tests/extensions/test_draft_variable_cleanup_schedule.py @@ -0,0 +1,36 @@ +"""Recovery remains scheduled even when initial cleanup delivery fails.""" + +from datetime import timedelta + +import pytest +from celery import current_app + +from dify_app import DifyApp +from extensions.ext_celery import init_app +from tests.unit_tests.config_override import apply_config_overrides + + +@pytest.mark.parametrize("enabled", [True, False]) +def test_draft_cleanup_recovery_schedule(monkeypatch: pytest.MonkeyPatch, enabled: bool) -> None: + restore_default = current_app.set_default + restore_current = current_app.set_current + apply_config_overrides( + monkeypatch, + ENABLE_WORKFLOW_DRAFT_FILE_CLEANUP_TASK=enabled, + WORKFLOW_DRAFT_FILE_CLEANUP_INTERVAL=7, + ) + application = init_app(DifyApp(__name__)) + try: + schedules = application.conf.beat_schedule + if enabled: + assert schedules["workflow_draft_file_cleanup"] == { + "task": "tasks.workflow_draft_var_tasks.recover_draft_variable_file_cleanup_task", + "schedule": timedelta(minutes=7), + } + assert "tasks.workflow_draft_var_tasks" in application.conf.imports + else: + assert "workflow_draft_file_cleanup" not in schedules + finally: + application.close() + restore_default() + restore_current() From 7ab064227ab78cddd2b8afe33178a968f2162fa5 Mon Sep 17 00:00:00 2001 From: Byron Wang Date: Tue, 27 Oct 2026 00:13:48 +0800 Subject: [PATCH 2/7] Align completion and annotation regression tests with active owners --- .../controllers/web/test_completion.py | 2 +- .../services/annotation/test_reply_service.py | 23 ++++++++++- .../services/test_annotation_service.py | 41 ------------------- 3 files changed, 22 insertions(+), 44 deletions(-) diff --git a/api/tests/unit_tests/controllers/web/test_completion.py b/api/tests/unit_tests/controllers/web/test_completion.py index 930a775bd414fa..6fa4750081c89a 100644 --- a/api/tests/unit_tests/controllers/web/test_completion.py +++ b/api/tests/unit_tests/controllers/web/test_completion.py @@ -9,8 +9,8 @@ import pytest from flask import Flask from sqlalchemy.orm import Session +from werkzeug.exceptions import InternalServerError -from controllers.common.errors import InternalServerError from controllers.web.completion import ChatApi, ChatStopApi, CompletionApi, CompletionStopApi from controllers.web.error import ( AgentNotPublishedError, diff --git a/api/tests/unit_tests/services/annotation/test_reply_service.py b/api/tests/unit_tests/services/annotation/test_reply_service.py index 5cc598e2e0e02c..14d0df35a2d94b 100644 --- a/api/tests/unit_tests/services/annotation/test_reply_service.py +++ b/api/tests/unit_tests/services/annotation/test_reply_service.py @@ -34,7 +34,13 @@ class AnnotationStore: sessions: sessionmaker[Session] -type OwnershipChange = Literal["disable", "foreign_annotation", "foreign_message", "binding"] +type OwnershipChange = Literal[ + "binding", + "disable", + "foreign_annotation", + "foreign_message", + "missing_annotation", +] @pytest.fixture @@ -191,9 +197,18 @@ def test_search_releases_connection_and_commits_hit( hit = session.scalar(select(AppAnnotationHitHistory)) assert hit is not None assert (hit.annotation_id, hit.message_id, hit.source) == (reply.id, "msg-1", source) + assert (hit.question, hit.annotation_question, hit.annotation_content, hit.score) == ( + "hi", + "question", + "answer", + 0.8, + ) -@pytest.mark.parametrize("change", ["disable", "foreign_annotation", "foreign_message", "binding"]) +@pytest.mark.parametrize( + "change", + ["disable", "foreign_annotation", "foreign_message", "missing_annotation", "binding"], +) def test_revalidates_after_remote_search( annotation_store: AnnotationStore, monkeypatch: pytest.MonkeyPatch, @@ -213,6 +228,10 @@ def change_owner() -> None: annotation = session.get(MessageAnnotation, "annotation-1") assert annotation is not None annotation.app_id = "foreign" + elif change == "missing_annotation": + annotation = session.get(MessageAnnotation, "annotation-1") + assert annotation is not None + session.delete(annotation) else: message = session.get(Message, "msg-1") assert message is not None diff --git a/api/tests/unit_tests/services/test_annotation_service.py b/api/tests/unit_tests/services/test_annotation_service.py index be9726fce73d83..fcd4e776272a4c 100644 --- a/api/tests/unit_tests/services/test_annotation_service.py +++ b/api/tests/unit_tests/services/test_annotation_service.py @@ -839,47 +839,6 @@ def test_hit_histories_are_annotation_and_app_scoped( assert total == 2 assert [item.id for item in items] == [old.id] - def test_get_annotation_by_id_uses_real_identity_lookup( - self, sqlite_session: Session, current_user: Account - ) -> None: - app = _persist_app(sqlite_session) - annotation = _persist_annotation(sqlite_session, app) - - assert AppAnnotationService.get_annotation_by_id("missing", sqlite_session) is None - assert AppAnnotationService.get_annotation_by_id(annotation.id, sqlite_session) is annotation - - def test_add_history_increments_count_and_flushes_row( - self, sqlite_session: Session, current_user: Account - ) -> None: - app = _persist_app(sqlite_session) - annotation = _persist_annotation(sqlite_session, app) - - AppAnnotationService.add_annotation_history( - annotation_id=annotation.id, - app_id=app.id, - annotation_question="q", - annotation_content="a", - query="user q", - user_id=current_user.id, - message_id="msg-1", - from_source="chat", - score=0.8, - session=sqlite_session, - ) - - sqlite_session.refresh(annotation) - assert annotation.hit_count == 1 - history = sqlite_session.scalar( - select(AppAnnotationHitHistory).where(AppAnnotationHitHistory.annotation_id == annotation.id) - ) - assert history is not None - assert (history.question, history.annotation_question, history.annotation_content, history.score) == ( - "user q", - "q", - "a", - 0.8, - ) - def test_get_setting_rejects_cross_tenant_app(self, sqlite_session: Session, current_user: Account) -> None: app = _persist_app(sqlite_session, tenant_id=OTHER_TENANT_ID) From ae7d7652b42b2a0a936ce05147a4fdaee19f40be Mon Sep 17 00:00:00 2001 From: Byron Wang Date: Tue, 27 Oct 2026 12:10:39 +0800 Subject: [PATCH 3/7] Assign execution offload relationships after TypeBase construction --- .../repositories/workflow/test_node_execution_repository.py | 4 ++-- .../unit_tests/services/workflow/test_runtime_gateway.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/api/tests/unit_tests/repositories/workflow/test_node_execution_repository.py b/api/tests/unit_tests/repositories/workflow/test_node_execution_repository.py index 069d9a5a51f4c9..8979ae04417fa4 100644 --- a/api/tests/unit_tests/repositories/workflow/test_node_execution_repository.py +++ b/api/tests/unit_tests/repositories/workflow/test_node_execution_repository.py @@ -27,8 +27,8 @@ def test_account_response_requires_execution_tenant_membership( app_id="app-1", created_by="account-1", created_by_role=CreatorUserRole.ACCOUNT, - offload_data=[], ) + execution.offload_data = [] result = WorkflowNodeExecutionRepository(sqlite_session_factory).execution_record(execution, include_details=True) assert result["created_by_end_user"] is None if tenant_id == "tenant-1": @@ -51,8 +51,8 @@ def test_end_user_response_requires_execution_tenant_and_app( app_id="app-1", created_by="end-user-1", created_by_role=CreatorUserRole.END_USER, - offload_data=[], ) + execution.offload_data = [] result = WorkflowNodeExecutionRepository(sqlite_session_factory).execution_record(execution, include_details=True) assert result["created_by_account"] is None if (tenant_id, app_id) == ("tenant-1", "app-1"): diff --git a/api/tests/unit_tests/services/workflow/test_runtime_gateway.py b/api/tests/unit_tests/services/workflow/test_runtime_gateway.py index c9afad74616e91..257cff57b8ebd4 100644 --- a/api/tests/unit_tests/services/workflow/test_runtime_gateway.py +++ b/api/tests/unit_tests/services/workflow/test_runtime_gateway.py @@ -901,8 +901,8 @@ def test_node_response_materializes_actor_extras_and_offload_flags( inputs='{"query":"value"}', outputs="{}", execution_metadata=None, - offload_data=[], ) + execution.offload_data = [] monkeypatch.setattr(gateway._workflows, "run_draft_workflow_node", lambda **_kwargs: execution) def extras(_execution: WorkflowNodeExecutionModel, *, tool_providers: ToolProviderIcons) -> dict[str, object]: @@ -945,7 +945,8 @@ def test_trigger_response_preserves_all_offload_types( # exactly as in the runtime, without resolving file relationships. with sqlite_session_factory() as session: offloads = list(session.scalars(select(WorkflowNodeExecutionOffload))) - execution = WorkflowNodeExecutionModel(id="execution", offload_data=offloads) + execution = WorkflowNodeExecutionModel(id="execution") + execution.offload_data = offloads raw = gateway._execution(execution, include_details=False) assert {item["type_"] for item in raw["offload_data"]} == {"inputs", "outputs", "process_data"} assert raw["offload_data"] == [ From 29201e3c0fb82d3ca7579010be609ad8846a21fd Mon Sep 17 00:00:00 2001 From: Byron Wang Date: Sat, 31 Oct 2026 22:50:08 +0800 Subject: [PATCH 4/7] Fix final canonical test imports and validation stability --- api/.importlinter | 2 +- .../im_integration/adapters/feishu_lark.py | 2 +- .../im_integration/adapters/ms_teams.py | 2 +- .../human_input_v2/im_integration/adapters/slack.py | 2 +- .../im_provider/test_feishu_lark_card_event_flow.py | 2 +- .../im_provider/test_ms_teams_card_event_flow.py | 2 +- .../core/human_input_v2/im_provider/test_slack.py | 2 +- .../console/test_human_input_v2_form_read.py | 3 +-- .../app/test_human_input_form_compatibility.py | 12 +++++++----- .../controllers/web/test_human_input_form.py | 2 +- .../controllers/web/test_human_input_v2_form_read.py | 3 +-- .../core/app/apps/workflow/test_app_generator.py | 9 +++------ .../im_provider/test_feishu_lark_adapter.py | 2 +- .../im_provider/test_feishu_lark_branches.py | 2 +- .../im_provider/test_feishu_lark_card_events.py | 2 +- .../im_provider/test_feishu_lark_sdk_boundary.py | 2 +- .../im_provider/test_lark_sdk_boundary.py | 2 +- .../im_provider/test_ms_teams_adapter.py | 2 +- .../im_provider/test_ms_teams_card_events.py | 2 +- .../im_provider/test_ms_teams_sdk_boundary.py | 2 +- .../human_input_v2/im_provider/test_slack_adapter.py | 2 +- .../im_provider/test_slack_card_events.py | 2 +- .../test_slack_requirement_conformance.py | 2 +- .../im_provider/test_slack_sdk_boundary.py | 2 +- .../core/human_input_v2/test_resolved_form.py | 2 +- .../core/rag/retrieval/test_dataset_retrieval.py | 4 +--- .../services/credentials/test_tool_runtime.py | 5 +++-- .../services/human_input_v2/test_composition.py | 2 +- .../human_input_v2/test_file_upload_service.py | 2 +- .../unit_tests/services/test_github_import_urls.py | 4 +--- .../services/test_message_query_adapters.py | 3 +-- 31 files changed, 41 insertions(+), 48 deletions(-) diff --git a/api/.importlinter b/api/.importlinter index 51871639356e53..b72a188f817b99 100644 --- a/api/.importlinter +++ b/api/.importlinter @@ -287,7 +287,7 @@ forbidden_modules = name = Tool runtime credential persistence is reached only through composition type = forbidden source_modules = - core.tools.tool_manager + services.tools.tool_manager forbidden_modules = repositories services.credentials.tool_runtime_adapters diff --git a/api/core/human_input_v2/im_integration/adapters/feishu_lark.py b/api/core/human_input_v2/im_integration/adapters/feishu_lark.py index c05effd3a1cca4..0f141389fdb72f 100644 --- a/api/core/human_input_v2/im_integration/adapters/feishu_lark.py +++ b/api/core/human_input_v2/im_integration/adapters/feishu_lark.py @@ -36,7 +36,6 @@ from requests.exceptions import Timeout as RequestsTimeout from websockets.asyncio.client import ClientConnection -from enums.human_input import ButtonStyle from core.human_input_v2.entities import IMProvider from core.human_input_v2.im_integration.adapters.avatar import read_avatars from core.human_input_v2.im_integration.adapters.credentials import ( @@ -92,6 +91,7 @@ ResolvedForm, SelectInput, ) +from enums.human_input import ButtonStyle logger = logging.getLogger(__name__) diff --git a/api/core/human_input_v2/im_integration/adapters/ms_teams.py b/api/core/human_input_v2/im_integration/adapters/ms_teams.py index cdd4c5d85a13f3..7be7149dc9b4e3 100644 --- a/api/core/human_input_v2/im_integration/adapters/ms_teams.py +++ b/api/core/human_input_v2/im_integration/adapters/ms_teams.py @@ -39,7 +39,6 @@ from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, JsonValue, ValidationError, field_validator from requests.exceptions import Timeout as RequestsTimeout -from enums.human_input import ButtonStyle from core.human_input_v2.entities import IMProvider from core.human_input_v2.im_integration.adapters.avatar import AvatarReadError, read_avatars from core.human_input_v2.im_integration.adapters.credentials import MSTeamsCredentials @@ -90,6 +89,7 @@ ResolvedForm, SelectInput, ) +from enums.human_input import ButtonStyle logger = logging.getLogger(__name__) diff --git a/api/core/human_input_v2/im_integration/adapters/slack.py b/api/core/human_input_v2/im_integration/adapters/slack.py index a9f15b6c7c9c61..24865a70d896ed 100644 --- a/api/core/human_input_v2/im_integration/adapters/slack.py +++ b/api/core/human_input_v2/im_integration/adapters/slack.py @@ -44,7 +44,6 @@ from slack_sdk.web import WebClient from slack_sdk.web.slack_response import SlackResponse -from enums.human_input import ButtonStyle from core.human_input_v2.entities import IMProvider from core.human_input_v2.im_integration.adapters.avatar import AvatarReadError, read_avatars from core.human_input_v2.im_integration.adapters.credentials import SlackCredentials @@ -97,6 +96,7 @@ ResolvedForm, SelectInput, ) +from enums.human_input import ButtonStyle logger = logging.getLogger(__name__) diff --git a/api/tests/e2e_tests/containers/core/human_input_v2/im_provider/test_feishu_lark_card_event_flow.py b/api/tests/e2e_tests/containers/core/human_input_v2/im_provider/test_feishu_lark_card_event_flow.py index 9706a8bcfc26cd..7cca93b38d60ed 100644 --- a/api/tests/e2e_tests/containers/core/human_input_v2/im_provider/test_feishu_lark_card_event_flow.py +++ b/api/tests/e2e_tests/containers/core/human_input_v2/im_provider/test_feishu_lark_card_event_flow.py @@ -13,7 +13,6 @@ P2CardActionTriggerResponse, ) -from enums.human_input import ButtonStyle from core.human_input_v2.entities import IMProvider from core.human_input_v2.im_integration.adapters import ( AuthenticatedIMEvent, @@ -35,6 +34,7 @@ FeishuIMProviderAdapter, ) from core.human_input_v2.resolved_form import MarkdownFragment, ParagraphInput, ResolvedForm, SelectInput, UserAction +from enums.human_input import ButtonStyle _FIXTURE_DIRECTORY = Path(__file__).resolve().parents[5] / "unit_tests/core/human_input_v2/im_provider/fixtures" _WEBHOOK_FIXTURE = _FIXTURE_DIRECTORY / "feishu_lark_card_action_webhook.json" diff --git a/api/tests/e2e_tests/containers/core/human_input_v2/im_provider/test_ms_teams_card_event_flow.py b/api/tests/e2e_tests/containers/core/human_input_v2/im_provider/test_ms_teams_card_event_flow.py index 127cc8133f50a6..533f4173177806 100644 --- a/api/tests/e2e_tests/containers/core/human_input_v2/im_provider/test_ms_teams_card_event_flow.py +++ b/api/tests/e2e_tests/containers/core/human_input_v2/im_provider/test_ms_teams_card_event_flow.py @@ -6,7 +6,6 @@ import pytest -from enums.human_input import ButtonStyle from core.human_input_v2.entities import IMProvider from core.human_input_v2.im_integration.adapters import ( AuthenticatedIMEvent, @@ -20,6 +19,7 @@ ) from core.human_input_v2.im_integration.adapters.ms_teams import MSTeamsIMProviderAdapter, _MSTeamsCardCodec from core.human_input_v2.resolved_form import MarkdownFragment, ParagraphInput, ResolvedForm, SelectInput, UserAction +from enums.human_input import ButtonStyle _RECEIVED_AT = datetime(2026, 8, 12, 2, 26, 40) diff --git a/api/tests/e2e_tests/containers/core/human_input_v2/im_provider/test_slack.py b/api/tests/e2e_tests/containers/core/human_input_v2/im_provider/test_slack.py index 802b1eb94f1f4d..73ee5acd40b407 100644 --- a/api/tests/e2e_tests/containers/core/human_input_v2/im_provider/test_slack.py +++ b/api/tests/e2e_tests/containers/core/human_input_v2/im_provider/test_slack.py @@ -16,7 +16,6 @@ from slack_sdk.socket_mode.request import SocketModeRequest from slack_sdk.web import WebClient -from enums.human_input import ButtonStyle from core.human_input_v2.entities import IMProvider from core.human_input_v2.im_integration.adapters import ( AuthenticatedIMEvent, @@ -42,6 +41,7 @@ from core.human_input_v2.im_integration.adapters import slack as slack_module from core.human_input_v2.im_integration.adapters.slack import SlackIMProviderAdapter from core.human_input_v2.resolved_form import MarkdownFragment, ParagraphInput, ResolvedForm, SelectInput, UserAction +from enums.human_input import ButtonStyle _SLACK_DIRECTORY_REFERENCE_PAGE_SIZE = 1 _MINIMUM_EXPECTED_SLACK_DIRECTORY_USERS = 2 diff --git a/api/tests/unit_tests/controllers/console/test_human_input_v2_form_read.py b/api/tests/unit_tests/controllers/console/test_human_input_v2_form_read.py index 95aca798a16dfd..da4437bb2fb13d 100644 --- a/api/tests/unit_tests/controllers/console/test_human_input_v2_form_read.py +++ b/api/tests/unit_tests/controllers/console/test_human_input_v2_form_read.py @@ -13,14 +13,13 @@ from sqlalchemy.orm import Session, sessionmaker from controllers.console.human_input_v2 import form as controller -from enums.human_input import ButtonStyle from core.human_input_v2.resolved_form import MarkdownFragment, ParagraphInput, ResolvedForm, UserAction from core.human_input_v2.shared.values import ContactId, TenantId from core.tools.signature import sign_tool_file, verify_tool_file_signature from core.workflow.nodes.human_input_v2.entities import Contact from dify_app import DifyApp from enums import DeploymentEdition -from enums.human_input import HumanInputFormKind, HumanInputFormStatus +from enums.human_input import ButtonStyle, HumanInputFormKind, HumanInputFormStatus from extensions.ext_login import DifyLoginManager from graphon.variables.segments import StringSegment from libs.external_api import ExternalApi diff --git a/api/tests/unit_tests/controllers/service_api/app/test_human_input_form_compatibility.py b/api/tests/unit_tests/controllers/service_api/app/test_human_input_form_compatibility.py index 82a1a892e29626..9dda47e1e405c9 100644 --- a/api/tests/unit_tests/controllers/service_api/app/test_human_input_form_compatibility.py +++ b/api/tests/unit_tests/controllers/service_api/app/test_human_input_form_compatibility.py @@ -26,7 +26,6 @@ from controllers.service_api import wraps from controllers.service_api.app import human_input_form as controller -from enums.human_input import ButtonStyle from core.human_input_v2.resolved_form import ( FileInput, FileListInput, @@ -37,7 +36,7 @@ ) from core.human_input_v2.shared.values import TenantId from core.workflow.nodes.human_input_v2.entities import Initiator -from enums.human_input import HumanInputFormStatus, RecipientType +from enums.human_input import ButtonStyle, HumanInputFormStatus, RecipientType from extensions.ext_application_services import ApplicationServices from extensions.storage.storage_type import StorageType from graphon.enums import WorkflowExecutionStatus @@ -396,8 +395,6 @@ def test_preserves_paragraph_json_values_and_ignores_extra_input_fields(approval ], ) def test_v1_json_paragraph_submission_resumes_the_node(approval: Approval, note: JsonValue, rendered_note: str) -> None: - from core.repositories.human_input_repository import HumanInputFormRepositoryImpl - from core.workflow.nodes.human_input.callback import DifyHITLCallback from core.workflow.nodes.human_input.constants import OUTPUT_FIELD_RENDERED_CONTENT from graphon.engine_events import NodeRunSucceededEvent from graphon.enums import WorkflowNodeExecutionStatus @@ -406,6 +403,8 @@ def test_v1_json_paragraph_submission_resumes_the_node(approval: Approval, note: from graphon.runtime import RuntimeState, VariablePool from graphon.runtime.init_params import InitParams from models.human_input_entities import HumanInputNodeData + from repositories.human_input.form_repository import HumanInputFormRepositoryImpl + from services.workflow.execution.adapters.human_input import DifyHITLCallback response = approval.submit(inputs={"note": note, "choice": "Yes"}) assert response.status_code == 200, response.json @@ -420,7 +419,10 @@ def test_v1_json_paragraph_submission_resumes_the_node(approval: Approval, note: user_actions=definition.user_actions, ) repository = HumanInputFormRepositoryImpl( - tenant_id=approval.tenant_id, app_id=approval.app_id, workflow_execution_id=approval.run_id + tenant_id=approval.tenant_id, + app_id=approval.app_id, + workflow_execution_id=approval.run_id, + sessions=approval.sessions, ) node = HumanInputNode( node_id="approval", diff --git a/api/tests/unit_tests/controllers/web/test_human_input_form.py b/api/tests/unit_tests/controllers/web/test_human_input_form.py index 40ab5805935be5..eb8b111e92dd88 100644 --- a/api/tests/unit_tests/controllers/web/test_human_input_form.py +++ b/api/tests/unit_tests/controllers/web/test_human_input_form.py @@ -15,11 +15,11 @@ import controllers.web.human_input_form as human_input_module from controllers.common.errors import AccessDeniedError, NotFoundError from controllers.web.error import WebFormRateLimitExceededError -from core.repositories.human_input_repository import HumanInputFormRecord from enums.human_input import HumanInputFormKind, HumanInputFormStatus, RecipientType, ValueSourceType from models import Tenant from models.account import TenantStatus from models.enums import CustomizeTokenStrategy +from models.human_input_contracts import HumanInputFormRecord from models.human_input_entities import FormDefinition, ParagraphInputConfig, SelectInputConfig, StringListSource from models.model import App, AppMode, IconType, Site from services.entities.feature_entities import FeatureModel diff --git a/api/tests/unit_tests/controllers/web/test_human_input_v2_form_read.py b/api/tests/unit_tests/controllers/web/test_human_input_v2_form_read.py index d76acab2d8c1c6..462acf6d7d6521 100644 --- a/api/tests/unit_tests/controllers/web/test_human_input_v2_form_read.py +++ b/api/tests/unit_tests/controllers/web/test_human_input_v2_form_read.py @@ -14,7 +14,6 @@ from sqlalchemy.orm import Session, sessionmaker from controllers.web import human_input_form_access_request as controller -from enums.human_input import ButtonStyle from core.human_input_v2.resolved_form import ( FileInput, FileListInput, @@ -27,7 +26,7 @@ from core.human_input_v2.shared.values import ContactId, NormalizedEmail, TenantId from core.tools.signature import sign_tool_file, verify_tool_file_signature from core.workflow.nodes.human_input_v2.entities import Contact, DynamicEmail, OnetimeEmail -from enums.human_input import HumanInputFormKind, HumanInputFormStatus +from enums.human_input import ButtonStyle, HumanInputFormKind, HumanInputFormStatus from graphon.file.enums import FileTransferMethod, FileType from graphon.variables.segments import StringSegment from libs.external_api import ExternalApi diff --git a/api/tests/unit_tests/core/app/apps/workflow/test_app_generator.py b/api/tests/unit_tests/core/app/apps/workflow/test_app_generator.py index bd675fb5ffaaaa..8f674f46c38679 100644 --- a/api/tests/unit_tests/core/app/apps/workflow/test_app_generator.py +++ b/api/tests/unit_tests/core/app/apps/workflow/test_app_generator.py @@ -776,6 +776,7 @@ def inspect_execution( graph_runtime_state=runtime_state, ) assert observed == [(user.id, expected_user_from, expected_system_user_id)] + class TestWorkflowAppGeneratorValidation: def test_generate_stream_joins_worker_after_response_exhaustion( self, @@ -886,9 +887,7 @@ def test_single_loop_generate_validates_args(self, *, workflow_runtime: Workflow workflow=_app_generator_extra_workflow(), node_id="", user=_end_user(), - execution_identity=WorkflowExecutionIdentity( - user_id=_end_user().id, user_from=UserFrom.END_USER - ), + execution_identity=WorkflowExecutionIdentity(user_id=_end_user().id, user_from=UserFrom.END_USER), args={"inputs": {}}, streaming=False, ) @@ -991,9 +990,7 @@ def test_single_loop_generate_includes_trace_session_id_in_extras( workflow=_app_generator_extra_workflow(), node_id="node", user=_end_user(), - execution_identity=WorkflowExecutionIdentity( - user_id=_end_user().id, user_from=UserFrom.END_USER - ), + execution_identity=WorkflowExecutionIdentity(user_id=_end_user().id, user_from=UserFrom.END_USER), args={"inputs": None}, streaming=False, ) diff --git a/api/tests/unit_tests/core/human_input_v2/im_provider/test_feishu_lark_adapter.py b/api/tests/unit_tests/core/human_input_v2/im_provider/test_feishu_lark_adapter.py index c6be9ab8cecccd..b0628a62ed702d 100644 --- a/api/tests/unit_tests/core/human_input_v2/im_provider/test_feishu_lark_adapter.py +++ b/api/tests/unit_tests/core/human_input_v2/im_provider/test_feishu_lark_adapter.py @@ -13,7 +13,6 @@ from pydantic import ValidationError from pytest_mock import MockerFixture -from enums.human_input import ButtonStyle from core.human_input_v2.entities import IMProvider from core.human_input_v2.im_integration.adapters import ( CorrelationToken, @@ -49,6 +48,7 @@ SelectInput, UserAction, ) +from enums.human_input import ButtonStyle class FakeSDKGateway: diff --git a/api/tests/unit_tests/core/human_input_v2/im_provider/test_feishu_lark_branches.py b/api/tests/unit_tests/core/human_input_v2/im_provider/test_feishu_lark_branches.py index 0154a105749c36..6b53b044408e6c 100644 --- a/api/tests/unit_tests/core/human_input_v2/im_provider/test_feishu_lark_branches.py +++ b/api/tests/unit_tests/core/human_input_v2/im_provider/test_feishu_lark_branches.py @@ -9,7 +9,6 @@ import pytest -from enums.human_input import ButtonStyle from core.human_input_v2.entities import IMProvider from core.human_input_v2.im_integration import adapters as adapters_package from core.human_input_v2.im_integration.adapters import ( @@ -34,6 +33,7 @@ SelectInput, UserAction, ) +from enums.human_input import ButtonStyle def test_stream_factory_returns_private_concrete_type_without_stream_contract_stub() -> None: diff --git a/api/tests/unit_tests/core/human_input_v2/im_provider/test_feishu_lark_card_events.py b/api/tests/unit_tests/core/human_input_v2/im_provider/test_feishu_lark_card_events.py index ee579b8b69c514..977b5c6fc67f07 100644 --- a/api/tests/unit_tests/core/human_input_v2/im_provider/test_feishu_lark_card_events.py +++ b/api/tests/unit_tests/core/human_input_v2/im_provider/test_feishu_lark_card_events.py @@ -16,7 +16,6 @@ P2CardActionTriggerResponse, ) -from enums.human_input import ButtonStyle from core.human_input_v2.entities import IMProvider from core.human_input_v2.im_integration.adapters import ( AuthenticatedIMEvent, @@ -40,6 +39,7 @@ SelectInput, UserAction, ) +from enums.human_input import ButtonStyle _FIXTURE_DIRECTORY = Path(__file__).with_name("fixtures") _WEBHOOK_FIXTURE = _FIXTURE_DIRECTORY / "feishu_lark_card_action_webhook.json" diff --git a/api/tests/unit_tests/core/human_input_v2/im_provider/test_feishu_lark_sdk_boundary.py b/api/tests/unit_tests/core/human_input_v2/im_provider/test_feishu_lark_sdk_boundary.py index a737977dff2cc2..87cb8ea012e839 100644 --- a/api/tests/unit_tests/core/human_input_v2/im_provider/test_feishu_lark_sdk_boundary.py +++ b/api/tests/unit_tests/core/human_input_v2/im_provider/test_feishu_lark_sdk_boundary.py @@ -24,7 +24,6 @@ FeishuCredentialsInput as FeishuCredentialRequest, ) from controllers.console.human_input_v2.providers import LarkCredentialsInput as LarkCredentialRequest -from enums.human_input import ButtonStyle from core.human_input_v2.entities import IMProvider from core.human_input_v2.im_integration.adapters import ( AuthenticatedIMEvent, @@ -59,6 +58,7 @@ _SynchronousEventChannel, ) from core.human_input_v2.resolved_form import MarkdownFragment, ParagraphInput, ResolvedForm, UserAction +from enums.human_input import ButtonStyle @dataclass(slots=True) diff --git a/api/tests/unit_tests/core/human_input_v2/im_provider/test_lark_sdk_boundary.py b/api/tests/unit_tests/core/human_input_v2/im_provider/test_lark_sdk_boundary.py index ac85967cfb9bb1..3e6403728f3430 100644 --- a/api/tests/unit_tests/core/human_input_v2/im_provider/test_lark_sdk_boundary.py +++ b/api/tests/unit_tests/core/human_input_v2/im_provider/test_lark_sdk_boundary.py @@ -19,7 +19,6 @@ from lark_oapi.ws.pb.pbbp2_pb2 import Frame from pydantic import BaseModel, ConfigDict, JsonValue, TypeAdapter -from enums.human_input import ButtonStyle from core.human_input_v2.entities import IMProvider from core.human_input_v2.im_integration.adapters import feishu_lark as adapter_module from core.human_input_v2.im_integration.adapters.credentials import FeishuCredentials, LarkCredentials @@ -46,6 +45,7 @@ ) from core.human_input_v2.im_integration.adapters.feishu_lark import FeishuIMProviderAdapter, LarkIMProviderAdapter from core.human_input_v2.resolved_form import MarkdownFragment, ParagraphInput, ResolvedForm, SelectInput, UserAction +from enums.human_input import ButtonStyle _FIXTURE_PATH = Path(__file__).with_name("fixtures") / "lark" / "sanitized_protocol.json" _JSON_OBJECT = TypeAdapter(dict[str, JsonValue]) diff --git a/api/tests/unit_tests/core/human_input_v2/im_provider/test_ms_teams_adapter.py b/api/tests/unit_tests/core/human_input_v2/im_provider/test_ms_teams_adapter.py index c296f6f220bf3e..46c6cdbc9c189a 100644 --- a/api/tests/unit_tests/core/human_input_v2/im_provider/test_ms_teams_adapter.py +++ b/api/tests/unit_tests/core/human_input_v2/im_provider/test_ms_teams_adapter.py @@ -24,7 +24,6 @@ from msrest.exceptions import HttpOperationError from pytest_mock import MockerFixture -from enums.human_input import ButtonStyle from core.human_input_v2.entities import IMProvider from core.human_input_v2.im_integration.adapters import ( AuthenticatedIMEvent, @@ -61,6 +60,7 @@ SelectInput, UserAction, ) +from enums.human_input import ButtonStyle _RECEIVED_AT = datetime(2026, 8, 6, 8, 0, 0) _WEBHOOK_FIXTURE_PATH = Path(__file__).parents[4] / "fixtures" / "im_provider" / "ms_teams_webhook_activity.json" diff --git a/api/tests/unit_tests/core/human_input_v2/im_provider/test_ms_teams_card_events.py b/api/tests/unit_tests/core/human_input_v2/im_provider/test_ms_teams_card_events.py index 7d253560ebce15..7964070fd80157 100644 --- a/api/tests/unit_tests/core/human_input_v2/im_provider/test_ms_teams_card_events.py +++ b/api/tests/unit_tests/core/human_input_v2/im_provider/test_ms_teams_card_events.py @@ -10,7 +10,6 @@ import pytest -from enums.human_input import ButtonStyle from core.human_input_v2.entities import IMProvider from core.human_input_v2.im_integration.adapters import ( AuthenticatedIMEvent, @@ -24,6 +23,7 @@ ms_teams, ) from core.human_input_v2.resolved_form import MarkdownFragment, ParagraphInput, ResolvedForm, SelectInput, UserAction +from enums.human_input import ButtonStyle _CALLBACK_FIXTURE_PATH = Path(__file__).parent / "fixtures" / "ms_teams_card_submit.json" _RECEIVED_AT = datetime(2026, 8, 12, 2, 26, 40) diff --git a/api/tests/unit_tests/core/human_input_v2/im_provider/test_ms_teams_sdk_boundary.py b/api/tests/unit_tests/core/human_input_v2/im_provider/test_ms_teams_sdk_boundary.py index 9c2d2925347c49..3e3a58c6d6ec35 100644 --- a/api/tests/unit_tests/core/human_input_v2/im_provider/test_ms_teams_sdk_boundary.py +++ b/api/tests/unit_tests/core/human_input_v2/im_provider/test_ms_teams_sdk_boundary.py @@ -27,7 +27,6 @@ from msrest.authentication import BasicTokenAuthentication from core.file import remote_fetcher -from enums.human_input import ButtonStyle from core.human_input_v2.entities import IMProvider from core.human_input_v2.im_integration.adapters import ( AuthenticatedIMEvent, @@ -60,6 +59,7 @@ SelectInput, UserAction, ) +from enums.human_input import ButtonStyle _PUBLIC_SERVICE_URL = "https://smba.trafficmanager.net/teams/" _RECEIVED_AT = datetime(2026, 8, 6, 8) diff --git a/api/tests/unit_tests/core/human_input_v2/im_provider/test_slack_adapter.py b/api/tests/unit_tests/core/human_input_v2/im_provider/test_slack_adapter.py index 79180cf6f3f186..66f6a1a6a2d28b 100644 --- a/api/tests/unit_tests/core/human_input_v2/im_provider/test_slack_adapter.py +++ b/api/tests/unit_tests/core/human_input_v2/im_provider/test_slack_adapter.py @@ -20,7 +20,6 @@ from slack_sdk.web import WebClient from slack_sdk.web.slack_response import SlackResponse -from enums.human_input import ButtonStyle from core.human_input_v2.entities import IMProvider from core.human_input_v2.im_integration.adapters import ( AuthenticatedIMEvent, @@ -57,6 +56,7 @@ SelectInput, UserAction, ) +from enums.human_input import ButtonStyle _RECEIVED_AT = datetime(2026, 8, 6, 8, 0, 0) _REQUEST_TIMESTAMP = str(int(_RECEIVED_AT.replace(tzinfo=UTC).timestamp())) diff --git a/api/tests/unit_tests/core/human_input_v2/im_provider/test_slack_card_events.py b/api/tests/unit_tests/core/human_input_v2/im_provider/test_slack_card_events.py index 8c7b35dca70184..7ea25bd8f94b1c 100644 --- a/api/tests/unit_tests/core/human_input_v2/im_provider/test_slack_card_events.py +++ b/api/tests/unit_tests/core/human_input_v2/im_provider/test_slack_card_events.py @@ -12,7 +12,6 @@ import pytest from pydantic import JsonValue, TypeAdapter -from enums.human_input import ButtonStyle from core.human_input_v2.entities import IMProvider from core.human_input_v2.im_integration.adapters import ( AuthenticatedIMEvent, @@ -26,6 +25,7 @@ from core.human_input_v2.im_integration.adapters import slack as slack_module from core.human_input_v2.im_integration.adapters.slack import SlackIMProviderAdapter, _SlackCardCodec from core.human_input_v2.resolved_form import MarkdownFragment, ParagraphInput, ResolvedForm, SelectInput, UserAction +from enums.human_input import ButtonStyle _FIXTURE_DIRECTORY = Path(__file__).with_name("fixtures") _WEBHOOK_FIXTURE = _FIXTURE_DIRECTORY / "slack_block_actions_webhook.json" diff --git a/api/tests/unit_tests/core/human_input_v2/im_provider/test_slack_requirement_conformance.py b/api/tests/unit_tests/core/human_input_v2/im_provider/test_slack_requirement_conformance.py index 249411812cc2eb..7c361c2a286584 100644 --- a/api/tests/unit_tests/core/human_input_v2/im_provider/test_slack_requirement_conformance.py +++ b/api/tests/unit_tests/core/human_input_v2/im_provider/test_slack_requirement_conformance.py @@ -16,7 +16,6 @@ from slack_sdk.socket_mode.request import SocketModeRequest from slack_sdk.web import WebClient -from enums.human_input import ButtonStyle from core.human_input_v2.entities import IMProvider from core.human_input_v2.im_integration.adapters import ( AuthenticatedIMEvent, @@ -49,6 +48,7 @@ SelectInput, UserAction, ) +from enums.human_input import ButtonStyle _SIGNING_SECRET = "sanitized-signing-material" diff --git a/api/tests/unit_tests/core/human_input_v2/im_provider/test_slack_sdk_boundary.py b/api/tests/unit_tests/core/human_input_v2/im_provider/test_slack_sdk_boundary.py index 1c3487b53b828f..11d8bed30d0e45 100644 --- a/api/tests/unit_tests/core/human_input_v2/im_provider/test_slack_sdk_boundary.py +++ b/api/tests/unit_tests/core/human_input_v2/im_provider/test_slack_sdk_boundary.py @@ -18,7 +18,6 @@ from slack_sdk.socket_mode.request import SocketModeRequest from slack_sdk.web import WebClient -from enums.human_input import ButtonStyle from core.human_input_v2.entities import IMProvider from core.human_input_v2.im_integration.adapters import ( AuthenticatedIMEvent, @@ -54,6 +53,7 @@ SelectInput, UserAction, ) +from enums.human_input import ButtonStyle _SIGNING_SECRET = "sanitized-signing-material" _RECEIVED_AT = datetime(2026, 8, 6, 8) diff --git a/api/tests/unit_tests/core/human_input_v2/test_resolved_form.py b/api/tests/unit_tests/core/human_input_v2/test_resolved_form.py index 6222dea7402719..8fe8e1f6c1dcbb 100644 --- a/api/tests/unit_tests/core/human_input_v2/test_resolved_form.py +++ b/api/tests/unit_tests/core/human_input_v2/test_resolved_form.py @@ -5,7 +5,6 @@ import pytest from pydantic import BaseModel, ValidationError -from enums.human_input import ButtonStyle from core.human_input_v2.resolved_form import ( FileInput, FileListInput, @@ -15,6 +14,7 @@ SelectInput, UserAction, ) +from enums.human_input import ButtonStyle from graphon.file.enums import FileTransferMethod, FileType diff --git a/api/tests/unit_tests/core/rag/retrieval/test_dataset_retrieval.py b/api/tests/unit_tests/core/rag/retrieval/test_dataset_retrieval.py index fd114243e45b71..489cdd7317b576 100644 --- a/api/tests/unit_tests/core/rag/retrieval/test_dataset_retrieval.py +++ b/api/tests/unit_tests/core/rag/retrieval/test_dataset_retrieval.py @@ -5118,9 +5118,7 @@ def capture_statement(*args: object) -> None: event.listen(sqlite_engine, "before_execute", capture_statement) try: with ( - patch( - "services.knowledge.retrieval.dataset_retrieval.ReactMultiDatasetRouter" - ) as mock_router_cls, + patch("services.knowledge.retrieval.dataset_retrieval.ReactMultiDatasetRouter") as mock_router_cls, patch.object( retrieval, "_external_retrieve", diff --git a/api/tests/unit_tests/services/credentials/test_tool_runtime.py b/api/tests/unit_tests/services/credentials/test_tool_runtime.py index d5e45dd97e5fdb..7d8ac839a476ae 100644 --- a/api/tests/unit_tests/services/credentials/test_tool_runtime.py +++ b/api/tests/unit_tests/services/credentials/test_tool_runtime.py @@ -101,8 +101,9 @@ def test_concurrent_and_sequential_calls_refresh_once() -> None: assert lock.keys == [("tool", "workspace", "credential")] * 4 -@pytest.mark.parametrize("expires_at", [-1, int(time.time()) + 3600]) -def test_valid_credentials_do_not_acquire_a_lock(expires_at: int) -> None: +@pytest.mark.parametrize("expires_in", [None, 3600], ids=["no-expiry", "future"]) +def test_valid_credentials_do_not_acquire_a_lock(expires_in: int | None) -> None: + expires_at = -1 if expires_in is None else int(time.time()) + expires_in store, runtime, lock = ( CredentialStore(_record(expires_at=expires_at)), CredentialRuntime(), diff --git a/api/tests/unit_tests/services/human_input_v2/test_composition.py b/api/tests/unit_tests/services/human_input_v2/test_composition.py index 46ea8c9ce7fa64..176639587595bb 100644 --- a/api/tests/unit_tests/services/human_input_v2/test_composition.py +++ b/api/tests/unit_tests/services/human_input_v2/test_composition.py @@ -12,7 +12,6 @@ from core.db.session_factory import session_factory from core.helper import encrypter from core.human_input_v2.shared.values import AccountId, TenantId -from core.workflow.node_factory import DifyNodeFactory from core.workflow.nodes.human_input_v2.events import NodeRunHumanInputV2FormRequiredEvent from core.workflow.system_variables import SystemVariableKey, system_variable_selector from enums import DeploymentEdition @@ -29,6 +28,7 @@ from models.human_input_v2 import ContactSubjectType, HumanInputContactIdentity, HumanInputForm from models.workflow import WorkflowPause, WorkflowRun, WorkflowType from services.human_input_v2.submission_service import HumanInputSubmissionService +from services.workflow.execution.adapters.node_factory import DifyNodeFactory from tasks.app_generate.workflow_execute_task import resume_app_execution from tasks.human_input_im_card_tasks import update_human_input_im_card_task diff --git a/api/tests/unit_tests/services/human_input_v2/test_file_upload_service.py b/api/tests/unit_tests/services/human_input_v2/test_file_upload_service.py index 91503876a1487b..3b82d771cf2f80 100644 --- a/api/tests/unit_tests/services/human_input_v2/test_file_upload_service.py +++ b/api/tests/unit_tests/services/human_input_v2/test_file_upload_service.py @@ -17,10 +17,10 @@ from configs import dify_config from controllers.web import human_input_form_access_request as token_controller from controllers.web import human_input_v2_file_upload as controller -from enums.human_input import ButtonStyle from core.human_input_v2.resolved_form import FileInput, ResolvedForm, UserAction from core.human_input_v2.shared.values import AccountId, NormalizedEmail, RecipientId, TenantId from core.workflow.nodes.human_input_v2.entities import OnetimeEmail +from enums.human_input import ButtonStyle from extensions.ext_storage import storage from graphon.enums import WorkflowExecutionStatus from graphon.file.enums import FileTransferMethod, FileType diff --git a/api/tests/unit_tests/services/test_github_import_urls.py b/api/tests/unit_tests/services/test_github_import_urls.py index ea5bfc5ce007ad..46875ce47ff045 100644 --- a/api/tests/unit_tests/services/test_github_import_urls.py +++ b/api/tests/unit_tests/services/test_github_import_urls.py @@ -54,9 +54,7 @@ def fetch(method: str, url: str, **_kwargs: object) -> httpx.Response: session=unbound_session, drafts=create_autospec(WorkflowDraftService, instance=True, spec_set=True), overwrites=create_autospec(AppDslOverwriteStore, instance=True, spec_set=True), - ).import_app( - account=account, import_mode="yaml-url", yaml_url=url - ) + ).import_app(account=account, import_mode="yaml-url", yaml_url=url) else: result = RagPipelineDslService(session=unbound_session).import_rag_pipeline( account=account, import_mode="yaml-url", yaml_url=url diff --git a/api/tests/unit_tests/services/test_message_query_adapters.py b/api/tests/unit_tests/services/test_message_query_adapters.py index a9f1df71a95c5c..87a7a66705a033 100644 --- a/api/tests/unit_tests/services/test_message_query_adapters.py +++ b/api/tests/unit_tests/services/test_message_query_adapters.py @@ -19,11 +19,10 @@ from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom from core.app.file_access import FileAccessScope, bind_file_access_scope from core.helper import encrypter, http_client_pooling -from enums.human_input import ButtonStyle from core.human_input_v2.resolved_form import MarkdownFragment, ParagraphInput, ResolvedForm, UserAction from core.tools.signature import verify_tool_file_signature from core.workflow.file_reference import build_file_reference, resolve_file_record_id -from enums.human_input import HumanInputFormStatus +from enums.human_input import ButtonStyle, HumanInputFormStatus from extensions.storage.storage_type import StorageType from graphon.enums import WorkflowExecutionStatus, WorkflowNodeExecutionStatus from graphon.file import FILE_MODEL_IDENTITY, File, FileTransferMethod, FileType From 990553e7efc8709dde4c31eb73cbc9362f41cfa5 Mon Sep 17 00:00:00 2001 From: Byron Wang Date: Sat, 31 Oct 2026 22:54:16 +0800 Subject: [PATCH 5/7] Keep invocation identity annotation behind service boundary --- .../workflow/execution/adapters/workflow/app_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/services/workflow/execution/adapters/workflow/app_generator.py b/api/services/workflow/execution/adapters/workflow/app_generator.py index 71a0043fbc97b9..14e0a92fadf3b6 100644 --- a/api/services/workflow/execution/adapters/workflow/app_generator.py +++ b/api/services/workflow/execution/adapters/workflow/app_generator.py @@ -42,7 +42,6 @@ from models.enums import WorkflowRunTriggeredFrom from models.model import App, EndUser from models.workflow import WorkflowNodeExecutionTriggeredFrom -from repositories.entities.workflow_execution import WorkflowExecutionIdentity from services.app.file_input_defaults import grant_file_default_access from services.app.generation.input_adapter import AppInputAdapter from services.workflow.contracts import WorkflowSnapshot @@ -60,6 +59,7 @@ if TYPE_CHECKING: from services.workflow.execution.contracts import WorkflowRunDriver + from services.workflow_execution_identity import WorkflowExecutionIdentity logger = logging.getLogger(__name__) From bd67ac5b72c5db41644bb40cedc67a8233cf821d Mon Sep 17 00:00:00 2001 From: Byron Wang Date: Sat, 31 Oct 2026 22:59:37 +0800 Subject: [PATCH 6/7] Align workflow regression fixtures with canonical ports and identities --- .../workflow/test_definition_repository.py | 3 ++- .../workflow/test_debug_worker_lease.py | 3 ++- .../workflow/test_node_persistence.py | 21 +++++++++---------- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/api/tests/unit_tests/repositories/workflow/test_definition_repository.py b/api/tests/unit_tests/repositories/workflow/test_definition_repository.py index 6f7bf900553bd1..1ec2eabe829ff5 100644 --- a/api/tests/unit_tests/repositories/workflow/test_definition_repository.py +++ b/api/tests/unit_tests/repositories/workflow/test_definition_repository.py @@ -72,6 +72,7 @@ from services.workflow.draft_service import DraftAgentRetirement, WorkflowDraftService from services.workflow.environment_variable_service import prepare_environment_variables from services.workflow_service import WorkflowService +from tests.helpers import billing_builders as builders from tests.helpers.unit.model_factories import make_account, make_app, make_tenant, make_workflow CONTEXT = RequestContext("request", None, "account-1", "tenant-1") @@ -1095,7 +1096,7 @@ def test_publication_retains_blocking_domain_validations( else: config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD) monkeypatch.setattr( - workflow_module.BillingService, "get_info", lambda *_args: {"subscription": {"plan": "sandbox"}} + workflow_module.BillingService, "get_info", lambda *_args: builders.subscription_info("sandbox") ) draft.graph = json.dumps( {"nodes": [{"id": str(index), "data": {"type": "trigger-webhook"}} for index in range(3)], "edges": []} diff --git a/api/tests/unit_tests/services/workflow/test_debug_worker_lease.py b/api/tests/unit_tests/services/workflow/test_debug_worker_lease.py index 62d175a87dfa88..069cf4afa50978 100644 --- a/api/tests/unit_tests/services/workflow/test_debug_worker_lease.py +++ b/api/tests/unit_tests/services/workflow/test_debug_worker_lease.py @@ -227,7 +227,7 @@ def test_resumed_runner_does_not_treat_previous_pause_as_current_completion( from time import monotonic from core.app.apps.base_app_queue_manager import AppQueueManager - from core.app.entities.app_invoke_entities import InvokeFrom, WorkflowAppGenerateEntity + from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom, WorkflowAppGenerateEntity from core.repositories.factory import WorkflowExecutionRepository, WorkflowNodeExecutionRepository from graphon.runtime import RuntimeState, VariablePool from graphon.variable_loader import DUMMY_VARIABLE_LOADER @@ -245,6 +245,7 @@ def test_resumed_runner_does_not_treat_previous_pause_as_current_completion( entity = WorkflowAppGenerateEntity.model_construct( app_config=WorkflowAppConfig.model_construct(app_id="app", app_mode=AppMode.WORKFLOW), invoke_from=InvokeFrom.DEBUGGER, + user_from=UserFrom.ACCOUNT, user_id="account", task_id="task", extras={}, diff --git a/api/tests/unit_tests/services/workflow/test_node_persistence.py b/api/tests/unit_tests/services/workflow/test_node_persistence.py index 4f757bd185da66..79ecba58a10a97 100644 --- a/api/tests/unit_tests/services/workflow/test_node_persistence.py +++ b/api/tests/unit_tests/services/workflow/test_node_persistence.py @@ -29,6 +29,7 @@ from models.enums import ConversationFromSource from models.model import App, Conversation, Message, MessageFile, UploadFile from models.oauth import DatasourceProvider +from services.data_source.entities.credential import DatasourceCredentialRecord from services.workflow.execution.adapters.node_factory import DifyNodeFactory from tests.helpers.unit.model_factories import ( make_app, @@ -268,22 +269,20 @@ def test_datasource_node_reads_injected_credential_before_plugin_stream( session.add(record) calls: list[str] = [] - def decrypt( - _self: object, - *, - tenant_id: str, - datasource_provider: DatasourceProvider, - plugin_id: str, - provider: str, - ) -> dict[str, str]: - assert (tenant_id, plugin_id, provider) == ("tenant", "vendor/notion", "notion") + def decrypt(_self: object, datasource_provider: DatasourceCredentialRecord) -> dict[str, str]: assert databases.kw["bind"].pool.checkedout() == 0 - assert datasource_provider.id == record.id + assert ( + datasource_provider.id, + datasource_provider.workspace_id, + datasource_provider.plugin_id, + datasource_provider.provider, + datasource_provider.encrypted_credentials, + ) == (record.id, "tenant", "vendor/notion", "notion", {"secret": "encrypted"}) calls.append("decrypt") return {"secret": "clear"} monkeypatch.setattr( - "services.data_source.provider_service.DatasourceProviderService.decrypt_datasource_provider_credentials", + "services.data_source.credential_adapters.PluginDatasourceCredentialCodec.decrypt", decrypt, ) node = factory( From 500e69459ce5e010654a5011145c71df2048a8e2 Mon Sep 17 00:00:00 2001 From: Byron Wang Date: Sat, 31 Oct 2026 23:02:58 +0800 Subject: [PATCH 7/7] Provide shared workflow fixture in Service API contracts --- .../e2e_tests/contracts/controllers/service_api/test_conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/tests/e2e_tests/contracts/controllers/service_api/test_conftest.py b/api/tests/e2e_tests/contracts/controllers/service_api/test_conftest.py index f26ba315481f07..5b7027f9f74eb3 100644 --- a/api/tests/e2e_tests/contracts/controllers/service_api/test_conftest.py +++ b/api/tests/e2e_tests/contracts/controllers/service_api/test_conftest.py @@ -9,6 +9,7 @@ persist_service_api_dataset_owner, persist_service_api_tenant_owner, ) +from tests.unit_tests.conftest import workflow_variables as workflow_variables # noqa: PLC0414 from tests.unit_tests.controllers.conftest import app_query_services as app_query_services # noqa: PLC0414 from tests.unit_tests.controllers.conftest import ( datasource_application_dependencies as datasource_application_dependencies, # noqa: PLC0414