From bd27f38bd1484f494026da1b510a6faaffb09f16 Mon Sep 17 00:00:00 2001 From: Byron Wang Date: Thu, 8 Oct 2026 18:35:00 +0800 Subject: [PATCH 1/3] refactor(api): wire tool provider management through application services --- api/.importlinter | 2 +- api/controllers/console/workspace/plugin.py | 13 +- .../console/workspace/tool_providers.py | 89 +-- api/core/mcp/auth_client.py | 2 +- api/core/plugin/impl/datasource.py | 2 +- api/core/tools/mcp_tool/provider.py | 2 +- api/extensions/ext_application_services.py | 3 + .../plugin/plugin_parameter_service.py | 2 +- .../console/workspace/test_tool_provider.py | 2 +- .../console/workspace/test_plugin.py | 8 +- .../workspace/test_tool_provider_apis.py | 21 +- .../console/workspace/test_tool_providers.py | 52 +- .../unit_tests/core/mcp/test_mcp_client.py | 8 +- .../core/tools/test_custom_tool_provider.py | 153 ++--- .../core/tools/test_tool_label_manager.py | 115 +--- .../core/tools/test_tool_manager.py | 549 +++++------------- .../tools/workflow_as_tool/test_provider.py | 93 +-- .../tools/test_api_tools_manage_service.py | 4 +- .../test_builtin_tools_manage_service.py | 25 +- .../test_mcp_tools_manage_service_lookup.py | 2 +- .../tools/test_mcp_tools_transform.py | 77 +-- .../tools/test_tools_manage_service.py | 44 +- .../tools/test_tools_transform_service.py | 58 +- 23 files changed, 456 insertions(+), 870 deletions(-) diff --git a/api/.importlinter b/api/.importlinter index 0816768c1911b2..4db00db0276cd4 100644 --- a/api/.importlinter +++ b/api/.importlinter @@ -67,7 +67,7 @@ ignore_imports = core.app.workflow.layers.persistence -> services.workflow.inspector_events core.helper.credential_utils -> services.enterprise.plugin_manager_service core.helper.credential_utils -> services.system_feature_service - core.mcp.auth_client -> services.tools.legacy_mcp_tools_manage_service + core.mcp.auth_client -> services.tools.mcp_tools_manage_service core.provider_manager -> services.credit_pool_service core.provider_manager -> services.feature_service core.rag.datasource.retrieval_service -> services.knowledge.external.service diff --git a/api/controllers/console/workspace/plugin.py b/api/controllers/console/workspace/plugin.py index e724c10706d312..d99700f469d107 100644 --- a/api/controllers/console/workspace/plugin.py +++ b/api/controllers/console/workspace/plugin.py @@ -7,9 +7,9 @@ from flask_restx import Resource from pydantic import BaseModel, ConfigDict, Field, RootModel from werkzeug.datastructures import FileStorage -from werkzeug.exceptions import Forbidden from configs import dify_config +from controllers.common.errors import AccessDeniedError from controllers.common.fields import BinaryFileResponse, SuccessResponse from controllers.common.rbac import RBACCheck, Workspace from controllers.common.schema import ( @@ -47,7 +47,7 @@ from core.tools.entities.api_entities import ToolProviderApiEntity from core.tools.entities.common_entities import I18nObject from core.tools.entities.tool_entities import ToolProviderType -from core.tools.tool_manager import ToolManager +from extensions.ext_application_services import application_services from extensions.ext_database import db from fields.base import ResponseModel from graphon.model_runtime.utils.encoders import jsonable_encoder @@ -67,7 +67,8 @@ from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService from services.plugin.plugin_parameter_service import PluginParameterService from services.plugin.plugin_permission_service import PluginPermissionService -from services.tools.legacy_tools_transform_service import ToolTransformService +from services.tools.tool_manager import ToolManager +from services.tools.tools_transform_service import ToolTransformService _PLUGIN_PACKAGE_UPLOAD_PARAMS = { "pkg": { @@ -533,7 +534,7 @@ def _list_hardcoded_builtin_tool_providers( """List builtin providers using the same search and tag semantics as category plugins.""" db_builtin_providers = { str(ToolProviderID(provider.provider)): provider - for provider in ToolManager.list_default_builtin_providers(tenant_id) + for provider in application_services().tools.tool_providers.default_builtin(tenant_id=tenant_id) } builtin_providers = [] @@ -1095,7 +1096,7 @@ class PluginChangePermissionApi(Resource): @model_validate(ParserPermissionChange) def post(self, req_data: ParserPermissionChange, tenant_id: str, user: Account): if not user.is_admin_or_owner: - raise Forbidden() + raise AccessDeniedError() set_permission_result = PluginPermissionService.change_permission( tenant_id, req_data.install_permission, req_data.debug_permission, session=db.session() @@ -1207,7 +1208,7 @@ class PluginChangeAutoUpgradeApi(Resource): @model_validate(ParserAutoUpgradeChange) def post(self, req_data: ParserAutoUpgradeChange, tenant_id: str, user: Account): if not dify_config.RBAC_ENABLED and not user.is_admin_or_owner: - raise Forbidden() + raise AccessDeniedError() auto_upgrade = req_data.auto_upgrade set_auto_upgrade_strategy_result = PluginAutoUpgradeService.change_strategy( diff --git a/api/controllers/console/workspace/tool_providers.py b/api/controllers/console/workspace/tool_providers.py index ac93bd6918bb92..01aaf6d11296be 100644 --- a/api/controllers/console/workspace/tool_providers.py +++ b/api/controllers/console/workspace/tool_providers.py @@ -16,9 +16,9 @@ model_validator, ) from sqlalchemy.orm import sessionmaker -from werkzeug.exceptions import Forbidden from configs import dify_config +from controllers.common.errors import AccessDeniedError from controllers.common.fields import SimpleResultResponse from controllers.common.rbac import RBACCheck, Workspace from controllers.common.schema import ( @@ -73,13 +73,11 @@ # from models.provider_ids import ToolProviderID from services.plugin.oauth_service import OAuthProxyService -from services.tools.legacy_api_tools_manage_service import ApiToolManageService, ApiToolPreviewResult -from services.tools.legacy_builtin_tools_manage_service import BuiltinToolManageService -from services.tools.legacy_mcp_tools_manage_service import MCPToolManageService, OAuthDataType -from services.tools.legacy_tools_manage_service import ToolCommonService -from services.tools.legacy_tools_transform_service import ToolTransformService -from services.tools.legacy_workflow_tools_manage_service import WorkflowToolManageService +from services.tools.api_tools_manage_service import ApiToolManageService, ApiToolPreviewResult +from services.tools.builtin_tools_manage_service import BuiltinToolManageService +from services.tools.mcp_tools_manage_service import MCPToolManageService, OAuthDataType from services.tools.tool_labels_service import ToolLabelsService +from services.tools.tools_manage_service import ToolCommonService logger = logging.getLogger(__name__) @@ -508,7 +506,11 @@ def get(self, tenant_id: str, user: Account): return _dump_tool_provider_payload_list( ToolCommonService.list_tool_providers( - user.id, tenant_id, cast(ToolProviderTypeApiLiteral | None, query.type) + user.id, + tenant_id, + cast(ToolProviderTypeApiLiteral | None, query.type), + workflow_queries=application_services().tools.workflow_queries, + tool_providers=application_services().tools.tool_providers, ), ) @@ -756,9 +758,7 @@ def get(self, tenant_id: str, user: Account): return dump_response( ToolApiListResponse, ApiToolManageService.list_api_tool_provider_tools( - user.id, - tenant_id, - query.provider, + user.id, tenant_id, query.provider, tool_providers=application_services().tools.tool_providers ), ) @@ -834,9 +834,7 @@ def get(self, tenant_id: str, user: Account): return dump_response( ApiProviderDetailResponse, ApiToolManageService.get_api_tool_provider( - user.id, - tenant_id, - query.provider, + user.id, tenant_id, query.provider, tool_providers=application_services().tools.tool_providers ), ) @@ -917,7 +915,7 @@ class ToolWorkflowProviderCreateApi(Resource): def post(self, req_data: WorkflowToolCreatePayload, tenant_id: str, user: Account): return dump_response( SimpleResultResponse, - WorkflowToolManageService.create_workflow_tool( + application_services().tools.workflows.create_workflow_tool( user_id=user.id, tenant_id=tenant_id, workflow_app_id=req_data.workflow_app_id, @@ -947,7 +945,7 @@ class ToolWorkflowProviderUpdateApi(Resource): def post(self, req_data: WorkflowToolUpdatePayload, tenant_id: str, user: Account): return dump_response( SimpleResultResponse, - WorkflowToolManageService.update_workflow_tool( + application_services().tools.workflows.update_workflow_tool( user.id, tenant_id, req_data.workflow_tool_id, @@ -977,7 +975,7 @@ class ToolWorkflowProviderDeleteApi(Resource): def post(self, req_data: WorkflowToolDeletePayload, tenant_id: str, user: Account): return dump_response( SimpleResultResponse, - WorkflowToolManageService.delete_workflow_tool( + application_services().tools.workflows.delete_workflow_tool( user.id, tenant_id, req_data.workflow_tool_id, @@ -1000,13 +998,13 @@ def get(self, tenant_id: str, user: Account): query = query_params_from_request(WorkflowToolGetQuery) if query.workflow_tool_id: - tool = WorkflowToolManageService.get_workflow_tool_by_tool_id( + tool = application_services().tools.workflows.get_workflow_tool_by_tool_id( user.id, tenant_id, query.workflow_tool_id, ) elif query.workflow_app_id: - tool = WorkflowToolManageService.get_workflow_tool_by_app_id( + tool = application_services().tools.workflows.get_workflow_tool_by_app_id( user.id, tenant_id, query.workflow_app_id, @@ -1033,7 +1031,7 @@ def get(self, tenant_id: str, user: Account): return dump_response( ToolApiListResponse, - WorkflowToolManageService.list_single_workflow_tools( + application_services().tools.workflows.list_single_workflow_tools( user.id, tenant_id, query.workflow_tool_id, @@ -1056,8 +1054,7 @@ def get(self, tenant_id: str, user: Account): [ provider.to_dict() for provider in BuiltinToolManageService.list_builtin_tools( - user.id, - tenant_id, + user.id, tenant_id, tool_providers=application_services().tools.tool_providers ) ], ) @@ -1076,7 +1073,7 @@ def get(self, tenant_id: str): [ provider.to_dict() for provider in ApiToolManageService.list_api_tools( - tenant_id, + tenant_id, tool_providers=application_services().tools.tool_providers ) ], ) @@ -1096,7 +1093,7 @@ def get(self, tenant_id: str, user: Account): return _dump_tool_provider_payload_list( [ provider.to_dict() - for provider in WorkflowToolManageService.list_tenant_workflow_tools( + for provider in application_services().tools.workflows.list_tenant_workflow_tools( user.id, tenant_id, ) @@ -1135,9 +1132,11 @@ def get(self, tenant_id: str, user: Account, provider: str): plugin_id = tool_provider.plugin_id provider_name = tool_provider.provider_name - oauth_client_params = BuiltinToolManageService.get_oauth_client(tenant_id=tenant_id, provider=provider) + oauth_client_params = BuiltinToolManageService.get_oauth_client( + tenant_id=tenant_id, provider=provider, tool_providers=application_services().tools.tool_providers + ) if oauth_client_params is None: - raise Forbidden("no oauth available client config found for this tool provider") + raise AccessDeniedError("no oauth available client config found for this tool provider") # Visibility is chosen by the user in the frontend before the redirect, # then read back in the callback below when the credential is created. @@ -1187,11 +1186,11 @@ class ToolOAuthCallback(Resource): def get(self, provider: str): context_id = request.cookies.get("context_id") if not context_id: - raise Forbidden("context_id not found") + raise AccessDeniedError("context_id not found") context = OAuthProxyService.use_proxy_context(context_id) if context is None: - raise Forbidden("Invalid context_id") + raise AccessDeniedError("Invalid context_id") tool_provider = ToolProviderID(provider) plugin_id = tool_provider.plugin_id @@ -1200,9 +1199,11 @@ def get(self, provider: str): tenant_id: str = context["tenant_id"] oauth_handler = OAuthHandler() - oauth_client_params = BuiltinToolManageService.get_oauth_client(tenant_id, provider) + oauth_client_params = BuiltinToolManageService.get_oauth_client( + tenant_id, provider, tool_providers=application_services().tools.tool_providers + ) if oauth_client_params is None: - raise Forbidden("no oauth available client config found for this tool provider") + raise AccessDeniedError("no oauth available client config found for this tool provider") redirect_uri = f"{dify_config.CONSOLE_API_URL}/console/api/oauth/plugin/{provider}/tool/callback" credentials_response = oauth_handler.get_credentials( @@ -1391,7 +1392,7 @@ def post(self, req_data: MCPProviderCreatePayload, tenant_id: str, user: Account # 1) Create provider in a short transaction (no network I/O inside) with session_factory.create_session() as session, session.begin(): service = MCPToolManageService(session=session) - result = service.create_provider( + provider_id = service.create_provider( tenant_id=tenant_id, user_id=user.id, server_url=req_data.server_url, @@ -1406,6 +1407,10 @@ def post(self, req_data: MCPProviderCreatePayload, tenant_id: str, user: Account identity_mode=_resolve_identity_mode(req_data.identity_mode, current=IdentityMode.OFF), ) + result = MCPToolManageService.provider_response( + tenant_id=tenant_id, provider_id=provider_id, tool_providers=application_services().tools.tool_providers + ) + # 2) Try to fetch tools immediately after creation so they appear without a second save. # Perform network I/O outside any DB session to avoid holding locks. try: @@ -1422,7 +1427,9 @@ def post(self, req_data: MCPProviderCreatePayload, tenant_id: str, user: Account db_provider.authed = reconnect.authed db_provider.tools = reconnect.tools - result = ToolTransformService.mcp_provider_to_user_provider(db_provider) + result = MCPToolManageService.provider_response( + tenant_id=tenant_id, provider_id=result.id, tool_providers=application_services().tools.tool_providers + ) except Exception: # Best-effort: if initial fetch fails (e.g., auth required), return created provider as-is logger.warning("Failed to fetch MCP tools after creation", exc_info=True) @@ -1586,10 +1593,10 @@ class ToolMCPDetailApi(Resource): @rbac_permission_required(RBACCheck(RBACPermission.MCP_MANAGE, Workspace())) @with_current_tenant_id def get(self, tenant_id: str, provider_id: str): - with sessionmaker(db.engine).begin() as session: - service = MCPToolManageService(session=session) - provider = service.get_provider_by_id(provider_id=provider_id, tenant_id=tenant_id) - return _dump_tool_provider_payload(ToolTransformService.mcp_provider_to_user_provider(provider).to_dict()) + provider = MCPToolManageService.provider_response( + tenant_id=tenant_id, provider_id=provider_id, tool_providers=application_services().tools.tool_providers + ) + return _dump_tool_provider_payload(provider.to_dict()) @console_ns.route("/workspaces/current/tools/mcp") @@ -1601,12 +1608,10 @@ class ToolMCPListAllApi(Resource): @with_current_tenant_id def get(self, tenant_id: str): - with sessionmaker(db.engine).begin() as session: - service = MCPToolManageService(session=session) - # Skip sensitive data decryption for list view to improve performance - tools = service.list_providers(tenant_id=tenant_id, include_sensitive=False) - - return _dump_tool_provider_payload_list([tool.to_dict() for tool in tools]) + tools = MCPToolManageService.list_providers( + tenant_id=tenant_id, include_sensitive=False, tool_providers=application_services().tools.tool_providers + ) + return _dump_tool_provider_payload_list([tool.to_dict() for tool in tools]) @console_ns.route("/workspaces/current/tool-provider/mcp/update/") diff --git a/api/core/mcp/auth_client.py b/api/core/mcp/auth_client.py index da2e04d12c2cc5..999c636c12b688 100644 --- a/api/core/mcp/auth_client.py +++ b/api/core/mcp/auth_client.py @@ -87,7 +87,7 @@ def _handle_auth_error(self, error: MCPAuthError) -> None: # Create a temporary session only for auth retry # This session is short-lived and only exists during the auth operation - from services.tools.legacy_mcp_tools_manage_service import MCPToolManageService + from services.tools.mcp_tools_manage_service import MCPToolManageService with Session(db.engine) as session, session.begin(): mcp_service = MCPToolManageService(session=session) diff --git a/api/core/plugin/impl/datasource.py b/api/core/plugin/impl/datasource.py index d9e675c39c6f6e..56c08addbab3cf 100644 --- a/api/core/plugin/impl/datasource.py +++ b/api/core/plugin/impl/datasource.py @@ -17,7 +17,7 @@ from core.plugin.impl.base import BasePluginClient from core.schemas.resolver import resolve_dify_schema_refs from models.provider_ids import DatasourceProviderID, GenericProviderID -from services.tools.legacy_tools_transform_service import ToolTransformService +from services.tools.tools_transform_service import ToolTransformService class PluginDatasourceManager(BasePluginClient): diff --git a/api/core/tools/mcp_tool/provider.py b/api/core/tools/mcp_tool/provider.py index edcb0c88505d3c..d1a0da0fbe6072 100644 --- a/api/core/tools/mcp_tool/provider.py +++ b/api/core/tools/mcp_tool/provider.py @@ -15,7 +15,7 @@ ) from core.tools.mcp_tool.tool import MCPTool from models.tools import MCPToolProvider -from services.tools.legacy_tools_transform_service import ToolTransformService +from services.tools.tools_transform_service import ToolTransformService class MCPToolProviderController(ToolProviderController[ToolProviderEntityWithPlugin, MCPTool]): diff --git a/api/extensions/ext_application_services.py b/api/extensions/ext_application_services.py index 8419c7cc6d9d24..016d414d04cf18 100644 --- a/api/extensions/ext_application_services.py +++ b/api/extensions/ext_application_services.py @@ -35,6 +35,7 @@ build_dataset_api_key_service, build_knowledge_services, ) +from extensions.application_services.tools import ToolServices, build_tool_services from extensions.application_services.trial_app import TrialAppServices, build_trial_app_services from extensions.application_services.workflow import build_workflow_execution_dependencies, build_workflow_suggestions from extensions.application_services.workflow_variables import ( @@ -247,6 +248,7 @@ class AppScopedEndUserServices: @dataclass(frozen=True, slots=True) class ApplicationServices: + tools: ToolServices workflow_runtime: AppGenerationRuntime workflow_variables: WorkflowVariableService console_workflow_variables: ConsoleWorkflowVariableService @@ -505,6 +507,7 @@ def build_application_services( ) workflow_variables = build_workflow_variable_service(database_client=database_client) return ApplicationServices( + tools=build_tool_services(database_client), workflow_runtime=build_workflow_execution_dependencies(database_client), workflow_variables=workflow_variables, console_workflow_variables=build_console_workflow_variables( diff --git a/api/services/plugin/plugin_parameter_service.py b/api/services/plugin/plugin_parameter_service.py index 09d016e381ad2e..109ba1040c0cbb 100644 --- a/api/services/plugin/plugin_parameter_service.py +++ b/api/services/plugin/plugin_parameter_service.py @@ -7,13 +7,13 @@ from core.plugin.entities.parameters import PluginParameterOption from core.plugin.entities.plugin_daemon import CredentialType from core.plugin.impl.dynamic_select import DynamicSelectClient -from core.tools.tool_manager import ToolManager from core.tools.utils.encryption import create_tool_provider_encrypter from core.trigger.entities.api_entities import TriggerProviderSubscriptionApiEntity from core.trigger.entities.entities import SubscriptionBuilder from extensions.ext_database import db from models.provider_ids import TriggerProviderID from models.tools import BuiltinToolProvider +from services.tools.tool_manager import ToolManager from services.trigger.trigger_provider_service import TriggerProviderService from services.trigger.trigger_subscription_builder_service import TriggerSubscriptionBuilderService diff --git a/api/tests/test_containers_integration_tests/controllers/console/workspace/test_tool_provider.py b/api/tests/test_containers_integration_tests/controllers/console/workspace/test_tool_provider.py index a4447093e3b411..35c8eb684f7474 100644 --- a/api/tests/test_containers_integration_tests/controllers/console/workspace/test_tool_provider.py +++ b/api/tests/test_containers_integration_tests/controllers/console/workspace/test_tool_provider.py @@ -82,7 +82,7 @@ def test_create_mcp_provider_populates_tools( with ( patch("controllers.console.workspace.tool_providers.MCPToolManageService", return_value=service, autospec=True), patch( - "services.tools.legacy_tools_transform_service.ToolTransformService.mcp_provider_to_user_provider", + "services.tools.tools_transform_service.ToolTransformService.mcp_provider_to_user_provider", return_value=_provider_entity(), autospec=True, ), diff --git a/api/tests/unit_tests/controllers/console/workspace/test_plugin.py b/api/tests/unit_tests/controllers/console/workspace/test_plugin.py index 5424dc65634749..291907c2a4be1e 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_plugin.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_plugin.py @@ -7,8 +7,8 @@ import pytest from flask import Flask from werkzeug.datastructures import FileStorage -from werkzeug.exceptions import Forbidden +from controllers.common.errors import AccessDeniedError from controllers.console.workspace.plugin import ( ParserAsset, ParserAutoUpgradeChange, @@ -503,7 +503,7 @@ def test_plugin_category_list(self, app: Flask): language="zh_Hans", ) - def test_builtin_tool_providers_use_the_category_list_filters(self): + def test_builtin_tool_providers_use_the_category_list_filters(self, *, workflow_application): search_provider = ToolProviderApiEntity( id="search-provider", author="dify", @@ -526,7 +526,7 @@ def test_builtin_tool_providers_use_the_category_list_filters(self): ) with ( - patch("controllers.console.workspace.plugin.ToolManager.list_default_builtin_providers", return_value=[]), + patch.object(workflow_application.tools.tool_providers, "default_builtin", return_value=[]), patch( "controllers.console.workspace.plugin.ToolManager.list_hardcoded_providers", return_value=[MagicMock(), MagicMock()], @@ -720,7 +720,7 @@ def test_change_permission_forbidden(self, app: Flask): with ( app.test_request_context("/", json=payload), ): - with pytest.raises(Forbidden): + with pytest.raises(AccessDeniedError): method(api, ParserPermissionChange(), "t1", user) def test_change_permission_success(self, app: Flask): diff --git a/api/tests/unit_tests/controllers/console/workspace/test_tool_provider_apis.py b/api/tests/unit_tests/controllers/console/workspace/test_tool_provider_apis.py index 1d078671a5390b..77bc41cc023152 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_tool_provider_apis.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_tool_provider_apis.py @@ -9,8 +9,8 @@ import pytest from flask import Flask -from werkzeug.exceptions import Forbidden +from controllers.common.errors import AccessDeniedError from controllers.console.workspace.tool_providers import ( ApiToolProviderAddPayload, ApiToolProviderDeletePayload, @@ -236,6 +236,7 @@ def test_is_valid_url(self) -> None: assert not is_valid_url(None) # type: ignore[arg-type] +@pytest.mark.usefixtures("workflow_application") class TestToolProviderListApi: def test_get_success(self, app: Flask) -> None: api = ToolProviderListApi() @@ -258,6 +259,7 @@ def test_get_success(self, app: Flask) -> None: class TestBuiltinProviderApis: + @pytest.mark.usefixtures("workflow_application") def test_list_tools(self, app: Flask) -> None: api = ToolBuiltinProviderListToolsApi() method = unwrap(api.get) @@ -472,6 +474,7 @@ def test_remote_schema(self, app: Flask) -> None: ): assert method(api, "t", make_account()) == {"schema": openapi_schema} + @pytest.mark.usefixtures("workflow_application") def test_list_tools(self, app: Flask) -> None: api = ToolApiProviderListToolsApi() method = unwrap(api.get) @@ -526,6 +529,7 @@ def test_delete(self, app: Flask) -> None: req = ApiToolProviderDeletePayload(provider="p") assert method(api, req, "t", make_account())["result"] == "success" + @pytest.mark.usefixtures("workflow_application") def test_get(self, app: Flask) -> None: api = ToolApiProviderGetApi() method = unwrap(api.get) @@ -540,6 +544,7 @@ def test_get(self, app: Flask) -> None: assert method(api, "t", make_account())["schema"] == "{}" +@pytest.mark.usefixtures("workflow_application") class TestWorkflowApis: def test_create(self, app: Flask) -> None: api = ToolWorkflowProviderCreateApi() @@ -557,7 +562,7 @@ def test_create(self, app: Flask) -> None: with ( app.test_request_context("/", json=payload), patch( - "controllers.console.workspace.tool_providers.WorkflowToolManageService.create_workflow_tool", + "services.tools.workflow_tools_manage_service.WorkflowToolManageService.create_workflow_tool", return_value={"result": "success"}, ) as create_workflow_tool, ): @@ -581,7 +586,7 @@ def test_update_invalid(self, app: Flask) -> None: with ( app.test_request_context("/", json=payload), patch( - "controllers.console.workspace.tool_providers.WorkflowToolManageService.update_workflow_tool", + "services.tools.workflow_tools_manage_service.WorkflowToolManageService.update_workflow_tool", return_value={"result": "success"}, ) as update_workflow_tool, ): @@ -598,7 +603,7 @@ def test_delete(self, app: Flask) -> None: with ( app.test_request_context("/", json={"workflow_tool_id": "123e4567-e89b-12d3-a456-426614174000"}), patch( - "controllers.console.workspace.tool_providers.WorkflowToolManageService.delete_workflow_tool", + "services.tools.workflow_tools_manage_service.WorkflowToolManageService.delete_workflow_tool", return_value={"result": "success"}, ), ): @@ -616,6 +621,7 @@ def test_get_error(self, app: Flask) -> None: method(api, "t", make_account()) +@pytest.mark.usefixtures("workflow_application") class TestLists: def test_builtin_list(self, app: Flask) -> None: api = ToolBuiltinListApi() @@ -650,7 +656,7 @@ def test_workflow_list(self, app: Flask) -> None: with ( app.test_request_context("/"), patch( - "controllers.console.workspace.tool_providers.WorkflowToolManageService.list_tenant_workflow_tools", + "services.tools.workflow_tools_manage_service.WorkflowToolManageService.list_tenant_workflow_tools", return_value=[provider_entity(provider_id="workflow-1", provider_type="workflow")], ), ): @@ -672,6 +678,7 @@ def test_labels(self, app: Flask) -> None: assert method(api)[0]["name"] == "utilities" +@pytest.mark.usefixtures("workflow_application") class TestOAuth: def test_oauth_no_client(self, app: Flask) -> None: api = ToolPluginOAuthApi() @@ -684,7 +691,7 @@ def test_oauth_no_client(self, app: Flask) -> None: return_value=None, ), ): - with pytest.raises(Forbidden): + with pytest.raises(AccessDeniedError): method(api, "t", make_account(), "provider") def test_oauth_callback_no_cookie(self, app: Flask) -> None: @@ -692,7 +699,7 @@ def test_oauth_callback_no_cookie(self, app: Flask) -> None: method = unwrap(api.get) with app.test_request_context("/"): - with pytest.raises(Forbidden): + with pytest.raises(AccessDeniedError): method(api, "provider") diff --git a/api/tests/unit_tests/controllers/console/workspace/test_tool_providers.py b/api/tests/unit_tests/controllers/console/workspace/test_tool_providers.py index ff6709cf822e48..905d26fa1b5894 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_tool_providers.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_tool_providers.py @@ -6,9 +6,10 @@ import importlib from collections.abc import Callable from contextlib import ExitStack, contextmanager +from dataclasses import dataclass from inspect import unwrap -from types import ModuleType, SimpleNamespace -from unittest.mock import MagicMock, patch +from types import ModuleType +from unittest.mock import MagicMock, create_autospec, patch from uuid import NAMESPACE_URL, uuid5 import pytest @@ -20,6 +21,7 @@ from core.tools.entities.common_entities import I18nObject from core.tools.entities.tool_entities import ToolParameter from enums import DeploymentEdition +from extensions.application_services.tools import ToolServices, build_tool_services from models import Account, BuiltinToolProvider, Tenant, TenantAccountJoin from models.account import TenantAccountRole from models.credential_permission import CredentialPermission @@ -31,6 +33,12 @@ builtins.MethodView = MethodView # type: ignore[attr-defined] +@dataclass(frozen=True) +class ProviderApplicationStub: + tools: ToolServices + credential_queries: CredentialQuery | CredentialQueryRepository + + _CONTROLLER_MODULE: ModuleType | None = None _WRAPS_MODULE: ModuleType | None = None @@ -43,7 +51,7 @@ def app() -> Flask: @pytest.fixture -def controller_module(monkeypatch: pytest.MonkeyPatch, config_overrides: Callable[..., None]): +def controller_module(monkeypatch: pytest.MonkeyPatch, config_overrides: Callable[..., None], workflow_application): """ Import the controller with auth decorators neutralized only during import. @@ -81,9 +89,10 @@ def _noop(func): login_module = importlib.import_module("libs.login") monkeypatch.setattr(login_module, "check_csrf_token", lambda *args, **kwargs: None) - monkeypatch.setattr( - module, "application_services", lambda: SimpleNamespace(credential_queries=MagicMock(spec=CredentialQuery)) + registry = ProviderApplicationStub( + tools=workflow_application.tools, credential_queries=create_autospec(CredentialQuery, instance=True) ) + monkeypatch.setattr(module, "application_services", lambda: registry) return module @@ -140,8 +149,9 @@ def _bind_database_session(session: Session): patch("extensions.ext_database.db.session", database_session), patch( "controllers.console.workspace.tool_providers.application_services", - return_value=SimpleNamespace( - credential_queries=CredentialQueryRepository(session_factory=session_factory) + return_value=ProviderApplicationStub( + tools=build_tool_services(session_factory), + credential_queries=CredentialQueryRepository(session_factory=session_factory), ), ), ): @@ -157,7 +167,7 @@ def _mock_credential_encryption(controller_module: ModuleType): encrypter.mask_plugin_credentials.return_value = {"api_key": "[__HIDDEN__]"} with ( patch( - "services.tools.legacy_builtin_tools_manage_service.ToolManager.get_builtin_provider", + "services.tools.builtin_tools_manage_service.ToolManager.get_builtin_provider", return_value=MagicMock(), ), patch.object( @@ -340,7 +350,13 @@ def test_tool_provider_list_calls_service_with_query( response = controller_module.ToolProviderListApi().get() assert response == [expected_response] - service_mock.assert_called_once_with(user.id, "tenant-456", "builtin") + service_mock.assert_called_once_with( + user.id, + "tenant-456", + "builtin", + workflow_queries=controller_module.application_services().tools.workflow_queries, + tool_providers=controller_module.application_services().tools.tool_providers, + ) def test_builtin_provider_add_passes_payload( @@ -574,7 +590,9 @@ def test_api_provider_list_tools_get(app: Flask, controller_module, monkeypatch: resp = controller_module.ToolApiProviderListToolsApi().get() assert resp == [expected_response] - service_mock.assert_called_once_with(user.id, "tenant-11", "foo") + service_mock.assert_called_once_with( + user.id, "tenant-11", "foo", tool_providers=controller_module.application_services().tools.tool_providers + ) def test_api_provider_get(app: Flask, controller_module, monkeypatch: pytest.MonkeyPatch): @@ -588,7 +606,9 @@ def test_api_provider_get(app: Flask, controller_module, monkeypatch: pytest.Mon resp = controller_module.ToolApiProviderGetApi().get() assert resp == expected_response - service_mock.assert_called_once_with(user.id, "tenant-12", "foo") + service_mock.assert_called_once_with( + user.id, "tenant-12", "foo", tool_providers=controller_module.application_services().tools.tool_providers + ) def test_builtin_provider_credentials_schema_get(app: Flask, controller_module, monkeypatch: pytest.MonkeyPatch): @@ -617,7 +637,7 @@ def test_workflow_provider_get_by_tool(app: Flask, controller_module, monkeypatc service_payload, expected_response = _workflow_detail_response(controller_module) tool_service = MagicMock(return_value=service_payload) monkeypatch.setattr( - controller_module.WorkflowToolManageService, + controller_module.application_services().tools.workflows, "get_workflow_tool_by_tool_id", tool_service, ) @@ -636,7 +656,7 @@ def test_workflow_provider_get_by_app(app: Flask, controller_module, monkeypatch service_payload, expected_response = _workflow_detail_response(controller_module) service_mock = MagicMock(return_value=service_payload) monkeypatch.setattr( - controller_module.WorkflowToolManageService, + controller_module.application_services().tools.workflows, "get_workflow_tool_by_app_id", service_mock, ) @@ -654,7 +674,9 @@ def test_workflow_provider_list_tools(app: Flask, controller_module, monkeypatch _set_current_account(monkeypatch, controller_module, user, "tenant-wf3") service_payload, expected_response = _tool_response(controller_module, "workflow-tool") service_mock = MagicMock(return_value=[service_payload]) - monkeypatch.setattr(controller_module.WorkflowToolManageService, "list_single_workflow_tools", service_mock) + monkeypatch.setattr( + controller_module.application_services().tools.workflows, "list_single_workflow_tools", service_mock + ) tool_id = "00000000-0000-0000-0000-000000000003" with app.test_request_context(f"/workflow/tools?workflow_tool_id={tool_id}"): @@ -704,7 +726,7 @@ def test_workflow_tools_list(app: Flask, controller_module, monkeypatch: pytest. provider, expected_response = _provider_entity_response(controller_module, "wf", "workflow") monkeypatch.setattr( - controller_module.WorkflowToolManageService, + controller_module.application_services().tools.workflows, "list_tenant_workflow_tools", MagicMock(return_value=[provider]), ) diff --git a/api/tests/unit_tests/core/mcp/test_mcp_client.py b/api/tests/unit_tests/core/mcp/test_mcp_client.py index 1561fcd4c01d63..507ad07f20f110 100644 --- a/api/tests/unit_tests/core/mcp/test_mcp_client.py +++ b/api/tests/unit_tests/core/mcp/test_mcp_client.py @@ -571,7 +571,7 @@ def test_init(self, provider): assert client.authorization_code == "initial-code" assert client._has_retried is False - @patch("services.tools.legacy_mcp_tools_manage_service.MCPToolManageService") + @patch("services.tools.mcp_tools_manage_service.MCPToolManageService") def test_handle_auth_error_success( self, mock_service_class, @@ -640,7 +640,7 @@ def test_handle_auth_error_already_retried(self, auth_client): assert exc_info.value == error - @patch("services.tools.legacy_mcp_tools_manage_service.MCPToolManageService") + @patch("services.tools.mcp_tools_manage_service.MCPToolManageService") def test_handle_auth_error_no_token( self, mock_service_class, @@ -663,7 +663,7 @@ def test_handle_auth_error_no_token( assert "Authentication failed - no token received" in str(exc_info.value) - @patch("services.tools.legacy_mcp_tools_manage_service.MCPToolManageService") + @patch("services.tools.mcp_tools_manage_service.MCPToolManageService") def test_handle_auth_error_generic_exception( self, mock_service_class, @@ -695,7 +695,7 @@ def execute_statement(*_args, **_kwargs): assert "Authentication retry failed: DB error" in str(exc_info.value) - @patch("services.tools.legacy_mcp_tools_manage_service.MCPToolManageService") + @patch("services.tools.mcp_tools_manage_service.MCPToolManageService") def test_handle_auth_error_mcp_auth_error_propagation( self, mock_service_class, diff --git a/api/tests/unit_tests/core/tools/test_custom_tool_provider.py b/api/tests/unit_tests/core/tools/test_custom_tool_provider.py index cd299047da37dc..8a5c5e680c82b8 100644 --- a/api/tests/unit_tests/core/tools/test_custom_tool_provider.py +++ b/api/tests/unit_tests/core/tools/test_custom_tool_provider.py @@ -1,126 +1,67 @@ -"""Tests for custom API tool providers with persisted provider lookup state.""" - -from __future__ import annotations - -import json -from dataclasses import dataclass -from unittest.mock import MagicMock -from uuid import uuid4 +"""API provider construction consumes already loaded configuration.""" import pytest -from sqlalchemy import delete -from sqlalchemy.orm import Session -from core.tools.custom_tool import provider as provider_module -from core.tools.custom_tool.provider import ApiToolProviderController from core.tools.custom_tool.tool import ApiTool from core.tools.entities.tool_bundle import ApiToolBundle -from core.tools.entities.tool_entities import ApiProviderAuthType, ApiProviderSchemaType, ToolProviderType -from models.tools import ApiToolProvider - - -@dataclass(frozen=True) -class _Database: - session: Session - - -def _tool_bundle() -> ApiToolBundle: - return ApiToolBundle( - server_url="https://api.example.com/items", - method="GET", - summary="List items", - operation_id="list_items", - parameters=[], - author="author", - openapi={"parameters": []}, - ) +from core.tools.entities.tool_entities import ApiProviderAuthType, ToolProviderType +from services.tools.api.contracts import ApiToolProviderRecord +from services.tools.api.provider import ApiToolProviderController -def _db_provider() -> ApiToolProvider: - provider = ApiToolProvider( - name="provider-a", - icon="icon.svg", +def provider_record() -> ApiToolProviderRecord: + return ApiToolProviderRecord( + schema_type="openapi", schema="{}", - schema_type_str=ApiProviderSchemaType.OPENAPI, - user_id="", + privacy_policy="", + custom_disclaimer="", + id="provider-id", tenant_id="tenant-1", - description="desc", - tools_str=json.dumps([_tool_bundle().model_dump(mode="json")]), - credentials_str='{"auth_type":"none"}', - ) - provider.id = "provider-id" - return provider - - -def _persist_provider(session: Session, *, tenant_id: str, name: str = "provider-a") -> ApiToolProvider: - bundle = _tool_bundle() - provider = ApiToolProvider( - name=name, + name="provider-a", icon="icon.svg", - schema="{}", - schema_type_str=ApiProviderSchemaType.OPENAPI, - user_id=str(uuid4()), - tenant_id=tenant_id, description="desc", - tools_str=json.dumps([bundle.model_dump(mode="json")]), - credentials_str='{"auth_type":"none"}', + author="Alice", + credentials={"auth_type": "none"}, + tools=[ + ApiToolBundle( + server_url="https://api.example.com/items", + method="GET", + summary="List items", + operation_id="list_items", + parameters=[], + author="Alice", + openapi={"parameters": []}, + ) + ], ) - session.add(provider) - session.commit() - return provider -def test_api_tool_provider_from_db_and_parse_tool_bundle() -> None: - controller = ApiToolProviderController.from_db( - _db_provider(), ApiProviderAuthType.API_KEY_HEADER, session=MagicMock() - ) +@pytest.mark.parametrize( + ("auth_type", "credential"), + [ + (ApiProviderAuthType.API_KEY_HEADER, "api_key_header"), + (ApiProviderAuthType.API_KEY_QUERY, "api_key_query_param"), + (ApiProviderAuthType.NONE, "auth_type"), + ], +) +def test_provider_constructs_auth_schema_and_tools_from_loaded_record(auth_type, credential): + provider = provider_record() + controller = ApiToolProviderController.from_provider(provider, auth_type, author=provider.author) assert controller.provider_type == ToolProviderType.API - assert any(c.name == "api_key_value" for c in controller.entity.credentials_schema) - - tool = controller._parse_tool_bundle(_db_provider().tools[0]) + assert controller.entity.identity.author == "Alice" + assert any(item.name == credential for item in controller.entity.credentials_schema) + tool = controller.get_tool("list_items") assert isinstance(tool, ApiTool) - assert tool.entity.identity.provider == "provider-id" - - -def test_api_tool_provider_from_db_query_auth_and_none_auth() -> None: - query_controller = ApiToolProviderController.from_db( - _db_provider(), ApiProviderAuthType.API_KEY_QUERY, session=MagicMock() - ) - assert any(c.name == "api_key_query_param" for c in query_controller.entity.credentials_schema) - - none_controller = ApiToolProviderController.from_db(_db_provider(), ApiProviderAuthType.NONE, session=MagicMock()) - assert [c.name for c in none_controller.entity.credentials_schema] == ["auth_type"] - - -def test_api_tool_provider_load_get_tools_and_get_tool( - monkeypatch: pytest.MonkeyPatch, sqlite_session: Session -) -> None: - controller = ApiToolProviderController.from_db(_db_provider(), ApiProviderAuthType.NONE, session=MagicMock()) - loaded = controller.load_bundled_tools(_db_provider().tools) - assert len(loaded) == 1 - - assert isinstance(controller.get_tool("list_items"), ApiTool) - + assert tool.entity.identity.provider == provider.id + assert controller.get_tools(provider.tenant_id) == [tool] with pytest.raises(ValueError, match="not found"): controller.get_tool("missing") - # Return cached tools without querying database. - cached = controller.get_tools("tenant-1") - assert len(cached) == 1 - # Force DB fetch branch. - controller.tools = [] - tenant_id = str(uuid4()) - provider_with_tools = _persist_provider(sqlite_session, tenant_id=tenant_id) - _persist_provider(sqlite_session, tenant_id=str(uuid4())) - controller.tenant_id = tenant_id - monkeypatch.setattr(provider_module, "db", _Database(session=sqlite_session)) - - tools = controller.get_tools(tenant_id) - assert len(tools) == 1 - assert tools[0].entity.identity.provider == controller.provider_id - - sqlite_session.execute(delete(ApiToolProvider).where(ApiToolProvider.id == provider_with_tools.id)) - sqlite_session.commit() - controller.tools = [] - assert controller.get_tools(tenant_id) == [] +def test_empty_tools_do_not_trigger_implicit_provider_lookup(): + provider = provider_record() + provider.tools.clear() + controller = ApiToolProviderController.from_provider(provider, ApiProviderAuthType.NONE, author=provider.author) + assert controller.get_tools(provider.tenant_id) == [] + with pytest.raises(ValueError, match="not found"): + controller.get_tool("list_items") diff --git a/api/tests/unit_tests/core/tools/test_tool_label_manager.py b/api/tests/unit_tests/core/tools/test_tool_label_manager.py index 2ddde3216d96d2..1c04ade63bb3c0 100644 --- a/api/tests/unit_tests/core/tools/test_tool_label_manager.py +++ b/api/tests/unit_tests/core/tools/test_tool_label_manager.py @@ -1,35 +1,12 @@ from __future__ import annotations -from typing import Any, override -from unittest.mock import PropertyMock, patch - import pytest from sqlalchemy import select -from sqlalchemy.engine import Engine from sqlalchemy.orm import Session -import core.tools.tool_label_manager as tool_label_manager_module -from core.tools.builtin_tool.provider import BuiltinToolProviderController -from core.tools.custom_tool.provider import ApiToolProviderController -from core.tools.tool_label_manager import ToolLabelManager -from core.tools.workflow_as_tool.provider import WorkflowToolProviderController from models.tools import ToolLabelBinding - - -class _DatabaseBinding: - """Expose the SQLite engine to code that owns its session lifecycle.""" - - engine: Engine - - def __init__(self, engine: Engine) -> None: - self.engine = engine - - -# Create a mock class for testing abstract/base classes -class _ConcreteBuiltinToolProviderController(BuiltinToolProviderController): - @override - def _validate_credentials(self, user_id: str, credentials: dict[str, Any]): - return None +from services.tools.api.provider import ApiToolProviderController +from services.tools.tool_label_manager import ToolLabelManager # Factory function to create a "lightweight" controller for testing @@ -39,12 +16,6 @@ def _api_controller(provider_id: str = "api-1") -> ApiToolProviderController: return controller -def _workflow_controller(provider_id: str = "wf-1") -> WorkflowToolProviderController: - controller = object.__new__(WorkflowToolProviderController) - controller.provider_id = provider_id - return controller - - # Test pure logic: filtering and deduplication def test_tool_label_manager_filter_tool_labels(): filtered = ToolLabelManager.filter_tool_labels(["search", "search", "invalid", "news"]) @@ -78,84 +49,6 @@ def test_tool_label_manager_update_tool_labels_db(sqlite_session: Session): # Test error handling -def test_tool_label_manager_update_tool_labels_unsupported(): - with pytest.raises(ValueError, match="Unsupported tool type"): - ToolLabelManager.update_tool_labels(object(), ["search"]) # type: ignore[arg-type] - - -# Test retrieval logic -@pytest.mark.parametrize("sqlite_session", [(ToolLabelBinding,)], indirect=True) -def test_tool_label_manager_get_tool_labels_for_builtin_and_db( - monkeypatch: pytest.MonkeyPatch, sqlite_engine: Engine, sqlite_session: Session -): - # Mocking a property (@property) using PropertyMock - with patch.object( - _ConcreteBuiltinToolProviderController, - "tool_labels", - new_callable=PropertyMock, - return_value=["search", "news"], - ): - builtin = object.__new__(_ConcreteBuiltinToolProviderController) - assert ToolLabelManager.get_tool_labels(builtin) == ["search", "news"] - - api = _api_controller("api-1") - sqlite_session.add_all( - [ - ToolLabelBinding(tool_id=api.provider_id, tool_type=api.provider_type, label_name="search"), - ToolLabelBinding(tool_id=api.provider_id, tool_type=api.provider_type, label_name="news"), - ] - ) - sqlite_session.commit() - monkeypatch.setattr(tool_label_manager_module, "db", _DatabaseBinding(sqlite_engine)) - - labels = ToolLabelManager.get_tool_labels(api) - assert set(labels) == {"search", "news"} - - -def test_tool_label_manager_get_tool_labels_unsupported(): - """ - Negative Test: Ensure get_tool_labels raises ValueError for unsupported controller types. - This protects the internal API contract against accidental regressions during refactoring. - """ - # Passing a generic object() which doesn't match Api, Workflow, or Builtin controllers. - with pytest.raises(ValueError, match="Unsupported tool type"): - ToolLabelManager.get_tool_labels(object()) # type: ignore[arg-type] - - -# Test batch processing and mapping -@pytest.mark.parametrize("sqlite_session", [(ToolLabelBinding,)], indirect=True) -def test_tool_label_manager_get_tools_labels_batch( - monkeypatch: pytest.MonkeyPatch, sqlite_engine: Engine, sqlite_session: Session -): - assert ToolLabelManager.get_tools_labels([]) == {} - - api = _api_controller("api-1") - wf = _workflow_controller("wf-1") - - sqlite_session.add_all( - [ - ToolLabelBinding(tool_id=api.provider_id, tool_type=api.provider_type, label_name="search"), - ToolLabelBinding(tool_id=api.provider_id, tool_type=api.provider_type, label_name="news"), - ToolLabelBinding(tool_id=wf.provider_id, tool_type=wf.provider_type, label_name="utilities"), - ] - ) - sqlite_session.commit() - monkeypatch.setattr(tool_label_manager_module, "db", _DatabaseBinding(sqlite_engine)) - - labels = ToolLabelManager.get_tools_labels([api, wf]) - - assert labels.keys() == {"api-1", "wf-1"} - assert set(labels["api-1"]) == {"search", "news"} - assert labels["wf-1"] == ["utilities"] - - -def test_tool_label_manager_get_tools_labels_unsupported(): - """ - Negative Test: Ensure get_tools_labels raises ValueError if the list contains - unsupported controller types, even alongside valid ones. - """ - api = _api_controller("api-1") - - # Passing a list with one valid controller and one invalid object() +def test_tool_label_manager_update_tool_labels_unsupported(sqlite_session: Session): with pytest.raises(ValueError, match="Unsupported tool type"): - ToolLabelManager.get_tools_labels([api, object()]) # type: ignore[list-item] + ToolLabelManager.update_tool_labels(object(), ["search"], session=sqlite_session) # type: ignore[arg-type] diff --git a/api/tests/unit_tests/core/tools/test_tool_manager.py b/api/tests/unit_tests/core/tools/test_tool_manager.py index 0d1fa814d45fd6..86e53eb86aae7d 100644 --- a/api/tests/unit_tests/core/tools/test_tool_manager.py +++ b/api/tests/unit_tests/core/tools/test_tool_manager.py @@ -6,31 +6,30 @@ import threading from collections.abc import Callable, Iterator from dataclasses import dataclass -from datetime import datetime from types import SimpleNamespace from typing import Any -from unittest.mock import ANY, Mock, patch +from unittest.mock import Mock, patch import pytest -from sqlalchemy import event from sqlalchemy.engine import Engine -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, sessionmaker from core.app.entities.app_invoke_entities import InvokeFrom from core.plugin.entities.plugin_daemon import CredentialType from core.plugin.impl.exc import PluginDaemonNotFoundError, PluginNotFoundError from core.tools.__base.tool_runtime import ToolRuntime from core.tools.entities.tool_entities import ( - ApiProviderAuthType, ToolInvokeFrom, ToolParameter, ToolProviderType, ) from core.tools.errors import ToolProviderCredentialValidationError, ToolProviderNotFoundError from core.tools.plugin_tool.provider import PluginToolProviderController -from core.tools.tool_manager import ToolManager +from models.account import Account from models.base import TypeBase from models.tools import ApiToolProvider, BuiltinToolProvider, WorkflowToolProvider +from repositories.tools.provider_repository import ToolProviderRepository +from services.tools.tool_manager import ToolManager class _CallableSessionProxy: @@ -79,13 +78,18 @@ def tool_database(sqlite_engine: Engine) -> Iterator[_ToolDatabase]: tables = [ TypeBase.metadata.tables[model.__tablename__] - for model in (BuiltinToolProvider, ApiToolProvider, WorkflowToolProvider) + for model in (Account, BuiltinToolProvider, ApiToolProvider, WorkflowToolProvider) ] TypeBase.metadata.create_all(sqlite_engine, tables=tables) with Session(sqlite_engine, expire_on_commit=False) as session: yield _ToolDatabase(engine=sqlite_engine, session=_CallableSessionProxy(session)) +@pytest.fixture +def tool_providers(tool_database): + return ToolProviderRepository(sessionmaker(tool_database.engine, expire_on_commit=False)) + + def _builtin_provider( *, provider_id: str, @@ -168,70 +172,6 @@ def set(self, value: Any): self._is_set = True -def _setup_list_providers_from_api_mocks( - monkeypatch, - *, - tool_database: _ToolDatabase, - hardcoded_controller: SimpleNamespace, - plugin_controller: PluginToolProviderController, - api_controller: SimpleNamespace, - workflow_controller: SimpleNamespace, -): - monkeypatch.setattr("core.tools.tool_manager.db", tool_database) - monkeypatch.setattr( - "core.tools.tool_manager.dify_config", - SimpleNamespace( - SQLALCHEMY_DATABASE_URI_SCHEME="mysql", - POSITION_TOOL_INCLUDES_SET=None, - POSITION_TOOL_EXCLUDES_SET=None, - ), - ) - monkeypatch.setattr( - ToolManager, - "list_builtin_providers", - Mock(return_value=[hardcoded_controller, plugin_controller]), - ) - monkeypatch.setattr( - ToolManager, - "list_default_builtin_providers", - Mock(return_value=[SimpleNamespace(provider="hardcoded")]), - ) - monkeypatch.setattr("core.tools.tool_manager.is_filtered", lambda *args, **kwargs: False) - monkeypatch.setattr( - "core.tools.tool_manager.ToolTransformService.builtin_provider_to_user_provider", - lambda **kwargs: SimpleNamespace(name=kwargs["provider_controller"].entity.identity.name), - ) - monkeypatch.setattr( - "core.tools.tool_manager.ToolTransformService.api_provider_to_controller", - Mock(side_effect=[api_controller, RuntimeError("invalid")]), - ) - monkeypatch.setattr( - "core.tools.tool_manager.ToolTransformService.api_provider_to_user_provider", - Mock(return_value=SimpleNamespace(name="api-provider")), - ) - monkeypatch.setattr( - "core.tools.tool_manager.ToolTransformService.workflow_provider_to_controller", - Mock(side_effect=[workflow_controller, RuntimeError("deleted app")]), - ) - monkeypatch.setattr( - "core.tools.tool_manager.ToolTransformService.workflow_provider_to_user_provider", - Mock(return_value=SimpleNamespace(name="workflow-provider")), - ) - monkeypatch.setattr( - "core.tools.tool_manager.ToolLabelManager.get_tools_labels", - Mock( - side_effect=[ - {api_controller.provider_id: ["search"]}, - {workflow_controller.provider_id: ["utility"]}, - ] - ), - ) - mock_mcp_service = Mock() - mock_mcp_service.list_providers.return_value = [SimpleNamespace(name="mcp-provider")] - monkeypatch.setattr("core.tools.tool_manager.MCPToolManageService", Mock(return_value=mock_mcp_service)) - monkeypatch.setattr("core.tools.tool_manager.BuiltinToolProviderSort.sort", lambda providers: providers) - - @pytest.fixture(autouse=True) def _reset_tool_manager_state(): old_hardcoded = ToolManager._hardcoded_providers.copy() @@ -274,12 +214,12 @@ def test_get_plugin_provider_uses_context_cache(): lock_context.set(threading.Lock()) provider_entity = SimpleNamespace(declaration=Mock(), plugin_id="pid", plugin_unique_identifier="uid") - with patch("core.tools.tool_manager.contexts.plugin_tool_providers", provider_context): - with patch("core.tools.tool_manager.contexts.plugin_tool_providers_lock", lock_context): - with patch("core.tools.tool_manager.PluginToolManager") as mock_manager_cls: + with patch("services.tools.tool_manager.contexts.plugin_tool_providers", provider_context): + with patch("services.tools.tool_manager.contexts.plugin_tool_providers_lock", lock_context): + with patch("services.tools.tool_manager.PluginToolManager") as mock_manager_cls: mock_manager_cls.return_value.fetch_tool_provider.return_value = provider_entity controller = SimpleNamespace(name="controller") - with patch("core.tools.tool_manager.PluginToolProviderController", return_value=controller): + with patch("services.tools.tool_manager.PluginToolProviderController", return_value=controller): built = ToolManager.get_plugin_provider("provider-a", "tenant-1") cached = ToolManager.get_plugin_provider("provider-a", "tenant-1") @@ -293,9 +233,9 @@ def test_get_plugin_provider_raises_when_provider_missing(): lock_context = _SimpleContextVar() lock_context.set(threading.Lock()) - with patch("core.tools.tool_manager.contexts.plugin_tool_providers", provider_context): - with patch("core.tools.tool_manager.contexts.plugin_tool_providers_lock", lock_context): - with patch("core.tools.tool_manager.PluginToolManager") as mock_manager_cls: + with patch("services.tools.tool_manager.contexts.plugin_tool_providers", provider_context): + with patch("services.tools.tool_manager.contexts.plugin_tool_providers_lock", lock_context): + with patch("services.tools.tool_manager.PluginToolManager") as mock_manager_cls: mock_manager_cls.return_value.fetch_tool_provider.return_value = None with pytest.raises(ToolProviderNotFoundError, match="plugin provider provider-a not found"): ToolManager.get_plugin_provider("provider-a", "tenant-1") @@ -326,15 +266,15 @@ def test_get_plugin_provider_translates_plugin_not_found_to_domain_error(daemon_ lock_context = _SimpleContextVar() lock_context.set(threading.Lock()) - with patch("core.tools.tool_manager.contexts.plugin_tool_providers", provider_context): - with patch("core.tools.tool_manager.contexts.plugin_tool_providers_lock", lock_context): - with patch("core.tools.tool_manager.PluginToolManager") as mock_manager_cls: + with patch("services.tools.tool_manager.contexts.plugin_tool_providers", provider_context): + with patch("services.tools.tool_manager.contexts.plugin_tool_providers_lock", lock_context): + with patch("services.tools.tool_manager.PluginToolManager") as mock_manager_cls: mock_manager_cls.return_value.fetch_tool_provider.side_effect = daemon_error with pytest.raises(ToolProviderNotFoundError, match="plugin provider provider-a not found"): ToolManager.get_plugin_provider("provider-a", "tenant-1") -def test_get_tool_runtime_builtin_without_credentials(): +def test_get_tool_runtime_builtin_without_credentials(*, tool_providers, workflow_queries): tool = Mock() tool.fork_tool_runtime.return_value = "runtime-tool" controller = SimpleNamespace(get_tool=Mock(return_value=tool), need_credentials=False) @@ -345,6 +285,8 @@ def test_get_tool_runtime_builtin_without_credentials(): provider_id="time", tool_name="current_time", tenant_id="tenant-1", + tool_providers=tool_providers, + workflow_queries=workflow_queries, ) assert result == "runtime-tool" @@ -353,7 +295,7 @@ def test_get_tool_runtime_builtin_without_credentials(): assert runtime.credentials == {} -def test_get_tool_runtime_builtin_missing_tool_raises(): +def test_get_tool_runtime_builtin_missing_tool_raises(*, tool_providers, workflow_queries): controller = SimpleNamespace(get_tool=Mock(return_value=None), need_credentials=False) with patch.object(ToolManager, "get_builtin_provider", return_value=controller): @@ -363,11 +305,13 @@ def test_get_tool_runtime_builtin_missing_tool_raises(): provider_id="time", tool_name="missing", tenant_id="tenant-1", + tool_providers=tool_providers, + workflow_queries=workflow_queries, ) def test_get_tool_runtime_builtin_with_credentials_decrypts_and_forks( - monkeypatch: pytest.MonkeyPatch, tool_database: _ToolDatabase + tool_database: _ToolDatabase, *, tool_providers, workflow_queries ): tool = Mock() tool.fork_tool_runtime.return_value = "runtime-tool" @@ -383,19 +327,20 @@ def test_get_tool_runtime_builtin_with_credentials_decrypts_and_forks( ) tool_database.session.add(builtin_provider) tool_database.session.commit() - monkeypatch.setattr("core.tools.tool_manager.db", tool_database) with patch.object(ToolManager, "get_builtin_provider", return_value=controller): with patch("core.helper.credential_utils.check_credential_policy_compliance"): encrypter = Mock() encrypter.decrypt.return_value = {"api_key": "secret"} cache = Mock() - with patch("core.tools.tool_manager.create_provider_encrypter", return_value=(encrypter, cache)): + with patch("services.tools.builtin.credentials.create_provider_encrypter", return_value=(encrypter, cache)): result = ToolManager.get_tool_runtime( provider_type=ToolProviderType.BUILT_IN, provider_id="time", tool_name="weekday", tenant_id=tenant_id, + tool_providers=tool_providers, + workflow_queries=workflow_queries, ) assert result == "runtime-tool" @@ -404,13 +349,13 @@ def test_get_tool_runtime_builtin_with_credentials_decrypts_and_forks( assert runtime.credential_type == CredentialType.API_KEY -@patch("core.tools.tool_manager.create_provider_encrypter") -@patch("core.plugin.impl.oauth.OAuthHandler") +@patch("services.tools.builtin.credentials.create_provider_encrypter") +@patch("services.tools.builtin.credentials.OAuthHandler") @patch( - "services.tools.legacy_builtin_tools_manage_service.BuiltinToolManageService.get_oauth_client", + "services.tools.builtin.credentials.resolve_oauth_client", return_value={"client_id": "id"}, ) -@patch("core.tools.tool_manager.time.time", return_value=1000) +@patch("services.tools.builtin.credentials.time.time", return_value=1000) @patch("core.helper.credential_utils.check_credential_policy_compliance") def test_get_tool_runtime_builtin_refreshes_expired_oauth_credentials( mock_check, @@ -418,8 +363,10 @@ def test_get_tool_runtime_builtin_refreshes_expired_oauth_credentials( mock_get_oauth_client, mock_oauth_handler_cls, mock_create_provider_encrypter, - monkeypatch: pytest.MonkeyPatch, tool_database: _ToolDatabase, + *, + tool_providers, + workflow_queries, ): tool = Mock() tool.fork_tool_runtime.return_value = "runtime-tool" @@ -440,7 +387,6 @@ def test_get_tool_runtime_builtin_refreshes_expired_oauth_credentials( tool_database.session.add(builtin_provider) tool_database.session.commit() - monkeypatch.setattr("core.tools.tool_manager.db", tool_database) encrypter = Mock() encrypter.decrypt.return_value = {"token": "old"} encrypter.encrypt.return_value = {"token": "encrypted"} @@ -454,11 +400,11 @@ def test_get_tool_runtime_builtin_refreshes_expired_oauth_credentials( provider_id="time", tool_name="weekday", tenant_id=tenant_id, + tool_providers=tool_providers, + workflow_queries=workflow_queries, ) assert result == "runtime-tool" - assert builtin_provider.expires_at == refreshed.expires_at - assert builtin_provider.encrypted_credentials == json.dumps({"token": "encrypted"}) tool_database.session.expire_all() persisted = tool_database.session.get(BuiltinToolProvider, provider_id) assert persisted is not None @@ -468,7 +414,7 @@ def test_get_tool_runtime_builtin_refreshes_expired_oauth_credentials( def test_get_tool_runtime_builtin_maps_oauth_refresh_failure_to_credential_error( - monkeypatch: pytest.MonkeyPatch, tool_database: _ToolDatabase + tool_database: _ToolDatabase, *, tool_providers, workflow_queries ): tool = Mock() controller = SimpleNamespace( @@ -485,19 +431,18 @@ def test_get_tool_runtime_builtin_maps_oauth_refresh_failure_to_credential_error ) tool_database.session.add(builtin_provider) tool_database.session.commit() - monkeypatch.setattr("core.tools.tool_manager.db", tool_database) encrypter = Mock() encrypter.decrypt.return_value = {"token": "expired"} with ( patch.object(ToolManager, "get_builtin_provider", return_value=controller), - patch("core.tools.tool_manager.create_provider_encrypter", return_value=(encrypter, Mock())), - patch("core.tools.tool_manager.time.time", return_value=1000), + patch("services.tools.builtin.credentials.create_provider_encrypter", return_value=(encrypter, Mock())), + patch("services.tools.builtin.credentials.time.time", return_value=1000), patch( - "services.tools.legacy_builtin_tools_manage_service.BuiltinToolManageService.get_oauth_client", + "services.tools.builtin.credentials.resolve_oauth_client", return_value={"client_id": "id"}, ), - patch("core.plugin.impl.oauth.OAuthHandler") as oauth_handler_cls, + patch("services.tools.builtin.credentials.OAuthHandler") as oauth_handler_cls, ): oauth_handler_cls.return_value.refresh_credentials.side_effect = ValueError("refresh token revoked") with pytest.raises(ToolProviderCredentialValidationError, match="could not be refreshed"): @@ -506,18 +451,17 @@ def test_get_tool_runtime_builtin_maps_oauth_refresh_failure_to_credential_error provider_id="time", tool_name="weekday", tenant_id=tenant_id, + tool_providers=tool_providers, + workflow_queries=workflow_queries, ) -def test_get_tool_runtime_builtin_plugin_credential_deleted_raises( - monkeypatch: pytest.MonkeyPatch, tool_database: _ToolDatabase -): +def test_get_tool_runtime_builtin_plugin_credential_deleted_raises(*, tool_providers, workflow_queries): plugin_controller = object.__new__(PluginToolProviderController) plugin_controller.entity = SimpleNamespace(credentials_schema=[{"name": "k"}], oauth_schema=None) plugin_controller.get_tool = Mock(return_value=Mock()) plugin_controller.get_credentials_schema_by_type = Mock(return_value=[]) - monkeypatch.setattr("core.tools.tool_manager.db", tool_database) with patch.object(ToolManager, "get_builtin_provider", return_value=plugin_controller): with pytest.raises(ToolProviderCredentialValidationError, match="credential .* has been deleted"): ToolManager.get_tool_runtime( @@ -526,37 +470,35 @@ def test_get_tool_runtime_builtin_plugin_credential_deleted_raises( tool_name="weekday", tenant_id="00000000-0000-0000-0000-000000000001", credential_id="00000000-0000-0000-0000-000000000002", + tool_providers=tool_providers, + workflow_queries=workflow_queries, ) -def test_get_tool_runtime_builtin_plugin_without_workspace_credential_raises( - monkeypatch: pytest.MonkeyPatch, tool_database: _ToolDatabase -): +def test_get_tool_runtime_builtin_plugin_without_workspace_credential_raises(*, tool_providers, workflow_queries): plugin_controller = object.__new__(PluginToolProviderController) plugin_controller.entity = SimpleNamespace(credentials_schema=[{"name": "k"}], oauth_schema=None) plugin_controller.get_tool = Mock(return_value=Mock()) plugin_controller.get_credentials_schema_by_type = Mock(return_value=[]) - monkeypatch.setattr("core.tools.tool_manager.db", tool_database) with patch.object(ToolManager, "get_builtin_provider", return_value=plugin_controller): - with pytest.raises(ToolProviderCredentialValidationError, match="No workspace credential is configured"): + with pytest.raises(ToolProviderCredentialValidationError, match="No credential is configured"): ToolManager.get_tool_runtime( provider_type=ToolProviderType.BUILT_IN, provider_id="langgenius/dify-gmail/dify-gmail", tool_name="send_draft", tenant_id="00000000-0000-0000-0000-000000000001", + tool_providers=tool_providers, + workflow_queries=workflow_queries, ) -def test_get_tool_runtime_hardcoded_provider_without_credential_raises( - monkeypatch: pytest.MonkeyPatch, tool_database: _ToolDatabase -): +def test_get_tool_runtime_hardcoded_provider_without_credential_raises(*, tool_providers, workflow_queries): controller = SimpleNamespace( get_tool=Mock(return_value=Mock()), need_credentials=True, ) - monkeypatch.setattr("core.tools.tool_manager.db", tool_database) with patch.object(ToolManager, "get_builtin_provider", return_value=controller): with pytest.raises(ToolProviderCredentialValidationError, match="No credential is configured"): ToolManager.get_tool_runtime( @@ -564,10 +506,12 @@ def test_get_tool_runtime_hardcoded_provider_without_credential_raises( provider_id="legacy-provider", tool_name="legacy-tool", tenant_id="00000000-0000-0000-0000-000000000001", + tool_providers=tool_providers, + workflow_queries=workflow_queries, ) -def test_get_tool_runtime_api_path(): +def test_get_tool_runtime_api_path(*, tool_providers, workflow_queries): api_tool = Mock() api_tool.fork_tool_runtime.return_value = "api-runtime" api_provider = Mock() @@ -576,31 +520,34 @@ def test_get_tool_runtime_api_path(): with patch.object(ToolManager, "get_api_provider_controller", return_value=(api_provider, {"c": "enc"})): encrypter = Mock() encrypter.decrypt.return_value = {"c": "dec"} - with patch("core.tools.tool_manager.create_tool_provider_encrypter", return_value=(encrypter, Mock())): + with patch("services.tools.tool_manager.create_tool_provider_encrypter", return_value=(encrypter, Mock())): assert ( ToolManager.get_tool_runtime( provider_type=ToolProviderType.API, provider_id="api-1", tool_name="search", tenant_id="tenant-1", + tool_providers=tool_providers, + workflow_queries=workflow_queries, ) == "api-runtime" ) -def test_get_tool_runtime_workflow_path(monkeypatch: pytest.MonkeyPatch, tool_database: _ToolDatabase): +def test_get_tool_runtime_workflow_path( + monkeypatch: pytest.MonkeyPatch, tool_database: _ToolDatabase, *, tool_providers, workflow_queries +): tenant_id = "00000000-0000-0000-0000-000000000001" provider_id = "00000000-0000-0000-0000-000000000002" workflow_provider = _workflow_provider(provider_id=provider_id, tenant_id=tenant_id) tool_database.session.add(workflow_provider) tool_database.session.commit() - monkeypatch.setattr("core.tools.tool_manager.db", tool_database) workflow_tool = Mock() workflow_tool.fork_tool_runtime.return_value = "wf-runtime" workflow_controller = Mock() workflow_controller.get_tools.return_value = [workflow_tool] with patch( - "core.tools.tool_manager.ToolTransformService.workflow_provider_to_controller", + "services.tools.tool_manager.ToolTransformService.workflow_provider_to_controller", return_value=workflow_controller, ): assert ( @@ -609,12 +556,14 @@ def test_get_tool_runtime_workflow_path(monkeypatch: pytest.MonkeyPatch, tool_da provider_id=provider_id, tool_name="wf", tenant_id=tenant_id, + tool_providers=tool_providers, + workflow_queries=workflow_queries, ) == "wf-runtime" ) -def test_get_tool_runtime_plugin_path(): +def test_get_tool_runtime_plugin_path(*, tool_providers, workflow_queries): with patch.object( ToolManager, "get_plugin_provider", @@ -626,12 +575,14 @@ def test_get_tool_runtime_plugin_path(): provider_id="plugin-1", tool_name="p", tenant_id="tenant-1", + tool_providers=tool_providers, + workflow_queries=workflow_queries, ) == "plugin-tool" ) -def test_get_tool_runtime_mcp_path(): +def test_get_tool_runtime_mcp_path(*, tool_providers, workflow_queries): with patch.object( ToolManager, "get_mcp_provider_controller", @@ -643,22 +594,27 @@ def test_get_tool_runtime_mcp_path(): provider_id="mcp-1", tool_name="m", tenant_id="tenant-1", + tool_providers=tool_providers, + workflow_queries=workflow_queries, ) == "mcp-tool" ) -def test_get_tool_runtime_app_not_implemented(): +def test_get_tool_runtime_app_not_implemented(*, tool_providers, workflow_queries): with pytest.raises(NotImplementedError, match="app provider not implemented"): ToolManager.get_tool_runtime( provider_type=ToolProviderType.APP, provider_id="app", tool_name="x", tenant_id="tenant-1", + tool_providers=tool_providers, + workflow_queries=workflow_queries, ) -def test_get_agent_runtime_apply_runtime_parameters(): +def test_get_agent_runtime_apply_runtime_parameters(*, tool_providers, workflow_queries): + saver = Mock() parameter = ToolParameter.get_simple_instance( name="query", llm_description="query", @@ -674,7 +630,7 @@ def test_get_agent_runtime_apply_runtime_parameters(): with patch.object(ToolManager, "_convert_tool_parameters_type", return_value={"query": "hello"}): manager = Mock() manager.decrypt_tool_parameters.return_value = {"query": "decrypted"} - with patch("core.tools.tool_manager.ToolParameterConfigurationManager", return_value=manager): + with patch("services.tools.tool_manager.ToolParameterConfigurationManager", return_value=manager): agent_tool = SimpleNamespace( provider_type=ToolProviderType.API, provider_id="api-1", @@ -683,17 +639,23 @@ def test_get_agent_runtime_apply_runtime_parameters(): credential_id=None, ) result = ToolManager.get_agent_tool_runtime( + draft_variable_saver=saver, + workflow_runtime=None, tenant_id="tenant-1", app_id="app-1", agent_tool=agent_tool, user_id="user-1", invoke_from=InvokeFrom.DEBUGGER, variable_pool=None, + tool_providers=tool_providers, + workflow_queries=workflow_queries, ) assert result is tool_runtime assert tool_runtime.runtime.runtime_parameters["query"] == "decrypted" mock_get_tool_runtime.assert_called_once_with( + tool_providers=tool_providers, + workflow_queries=workflow_queries, provider_type=ToolProviderType.API, provider_id="api-1", tool_name="search", @@ -702,10 +664,13 @@ def test_get_agent_runtime_apply_runtime_parameters(): invoke_from=InvokeFrom.DEBUGGER, tool_invoke_from=ToolInvokeFrom.AGENT, credential_id=None, + draft_variable_saver=saver, + workflow_runtime=None, ) -def test_get_workflow_runtime_apply_runtime_parameters(): +def test_get_workflow_runtime_apply_runtime_parameters(*, tool_providers, workflow_queries): + saver = Mock() parameter = ToolParameter.get_simple_instance( name="query", llm_description="query", @@ -727,8 +692,10 @@ def test_get_workflow_runtime_apply_runtime_parameters(): with patch.object(ToolManager, "_convert_tool_parameters_type", return_value={"query": "workflow"}): manager = Mock() manager.decrypt_tool_parameters.return_value = {"query": "workflow-dec"} - with patch("core.tools.tool_manager.ToolParameterConfigurationManager", return_value=manager): + with patch("services.tools.tool_manager.ToolParameterConfigurationManager", return_value=manager): workflow_result = ToolManager.get_workflow_tool_runtime( + draft_variable_saver=saver, + workflow_runtime=None, tenant_id="tenant-1", app_id="app-1", node_id="node-1", @@ -736,11 +703,15 @@ def test_get_workflow_runtime_apply_runtime_parameters(): user_id="user-1", invoke_from=InvokeFrom.DEBUGGER, variable_pool=None, + tool_providers=tool_providers, + workflow_queries=workflow_queries, ) assert workflow_result is tool_runtime2 assert tool_runtime2.runtime.runtime_parameters["query"] == "workflow-dec" mock_get_tool_runtime.assert_called_once_with( + tool_providers=tool_providers, + workflow_queries=workflow_queries, provider_type=ToolProviderType.API, provider_id="api-1", tool_name="search", @@ -749,10 +720,12 @@ def test_get_workflow_runtime_apply_runtime_parameters(): invoke_from=InvokeFrom.DEBUGGER, tool_invoke_from=ToolInvokeFrom.WORKFLOW, credential_id=None, + draft_variable_saver=saver, + workflow_runtime=None, ) -def test_get_agent_runtime_raises_when_runtime_missing(): +def test_get_agent_runtime_raises_when_runtime_missing(*, tool_providers, workflow_queries): tool_runtime = SimpleNamespace(runtime=None, get_merged_runtime_parameters=lambda: []) agent_tool = SimpleNamespace( provider_type=ToolProviderType.API, @@ -763,16 +736,18 @@ def test_get_agent_runtime_raises_when_runtime_missing(): ) with patch.object(ToolManager, "get_tool_runtime", return_value=tool_runtime): with patch.object(ToolManager, "_convert_tool_parameters_type", return_value={}): - with patch("core.tools.tool_manager.ToolParameterConfigurationManager", return_value=Mock()): + with patch("services.tools.tool_manager.ToolParameterConfigurationManager", return_value=Mock()): with pytest.raises(ValueError, match="runtime not found"): ToolManager.get_agent_tool_runtime( tenant_id="tenant-1", app_id="app-1", agent_tool=agent_tool, + tool_providers=tool_providers, + workflow_queries=workflow_queries, ) -def test_get_tool_runtime_from_plugin_only_uses_form_parameters(): +def test_get_tool_runtime_from_plugin_only_uses_form_parameters(workflow_runtime, workflow_variables): form_param = ToolParameter.get_simple_instance( name="q", llm_description="query", @@ -793,6 +768,8 @@ def test_get_tool_runtime_from_plugin_only_uses_form_parameters(): with patch.object(ToolManager, "get_tool_runtime", return_value=tool_entity) as mock_get_tool_runtime: result = ToolManager.get_tool_runtime_from_plugin( + workflow_runtime=workflow_runtime, + draft_variable_saver=workflow_variables.saver_factory, tool_type=ToolProviderType.API, tenant_id="tenant-1", provider="api-1", @@ -804,6 +781,8 @@ def test_get_tool_runtime_from_plugin_only_uses_form_parameters(): assert result is tool_entity assert tool_entity.runtime.runtime_parameters == {"q": "hello"} mock_get_tool_runtime.assert_called_once_with( + tool_providers=workflow_runtime.tool_providers, + workflow_queries=workflow_runtime.tools, provider_type=ToolProviderType.API, provider_id="api-1", tool_name="search", @@ -812,14 +791,16 @@ def test_get_tool_runtime_from_plugin_only_uses_form_parameters(): invoke_from=InvokeFrom.SERVICE_API, tool_invoke_from=ToolInvokeFrom.PLUGIN, credential_id=None, + workflow_runtime=workflow_runtime, + draft_variable_saver=workflow_variables.saver_factory, ) def test_hardcoded_provider_icon_success(): provider = SimpleNamespace(entity=SimpleNamespace(identity=SimpleNamespace(icon="icon.svg"))) with patch.object(ToolManager, "get_hardcoded_provider", return_value=provider): - with patch("core.tools.tool_manager.path.exists", return_value=True): - with patch("core.tools.tool_manager.mimetypes.guess_type", return_value=("image/svg+xml", None)): + with patch("services.tools.tool_manager.path.exists", return_value=True): + with patch("services.tools.tool_manager.mimetypes.guess_type", return_value=("image/svg+xml", None)): icon_path, mime = ToolManager.get_hardcoded_provider_icon("time") assert icon_path.endswith("icon.svg") assert mime == "image/svg+xml" @@ -828,7 +809,7 @@ def test_hardcoded_provider_icon_success(): def test_hardcoded_provider_icon_missing_raises(): provider = SimpleNamespace(entity=SimpleNamespace(identity=SimpleNamespace(icon="icon.svg"))) with patch.object(ToolManager, "get_hardcoded_provider", return_value=provider): - with patch("core.tools.tool_manager.path.exists", return_value=False): + with patch("services.tools.tool_manager.path.exists", return_value=False): with pytest.raises(ToolProviderNotFoundError, match="icon not found"): ToolManager.get_hardcoded_provider_icon("time") @@ -854,10 +835,10 @@ def test_list_hardcoded_providers_internal_loader(): ) provider_class = Mock(return_value=good_provider) - with patch("core.tools.tool_manager.listdir", return_value=["good", "bad", "__skip"]): - with patch("core.tools.tool_manager.path.isdir", side_effect=lambda p: "good" in p or "bad" in p): + with patch("services.tools.tool_manager.listdir", return_value=["good", "bad", "__skip"]): + with patch("services.tools.tool_manager.path.isdir", side_effect=lambda p: "good" in p or "bad" in p): with patch( - "core.tools.tool_manager.load_single_subclass_from_source", + "services.tools.tool_manager.load_single_subclass_from_source", side_effect=[provider_class, RuntimeError("boom")], ): ToolManager._hardcoded_providers = {} @@ -881,182 +862,26 @@ def _load(): assert ToolManager.get_tool_label("missing") is None -@pytest.mark.parametrize("database_scheme", ["mysql", "postgresql"]) -def test_list_default_builtin_providers_uses_persisted_defaults( - monkeypatch: pytest.MonkeyPatch, - tool_database: _ToolDatabase, - database_scheme: str, -): - tenant_id = "00000000-0000-0000-0000-000000000001" - default_provider = _builtin_provider( - provider_id="00000000-0000-0000-0000-000000000002", - tenant_id=tenant_id, - name="default", - is_default=True, - ) - older_provider = _builtin_provider( - provider_id="00000000-0000-0000-0000-000000000003", - tenant_id=tenant_id, - name="older", - is_default=False, - ) - other_tenant_provider = _builtin_provider( - provider_id="00000000-0000-0000-0000-000000000004", - tenant_id="00000000-0000-0000-0000-000000000005", - name="foreign", - ) - default_provider.created_at = datetime(2026, 1, 2) - older_provider.created_at = datetime(2026, 1, 1) - tool_database.session.add_all([default_provider, older_provider, other_tenant_provider]) - tool_database.session.commit() - monkeypatch.setattr("core.tools.tool_manager.db", tool_database) - monkeypatch.setattr( - "core.tools.tool_manager.dify_config", - SimpleNamespace(SQLALCHEMY_DATABASE_URI_SCHEME=database_scheme), - ) - - postgresql_statements = [] - - def translate_postgresql_distinct_on(_connection, _cursor, statement, parameters, _context, _executemany): - if "SELECT DISTINCT ON (tenant_id, provider) id" not in statement: - return statement, parameters - - postgresql_statements.append(statement) - sqlite_statement = """ - SELECT id FROM ( - SELECT id, - ROW_NUMBER() OVER ( - PARTITION BY tenant_id, provider - ORDER BY is_default DESC, created_at DESC - ) AS rn - FROM tool_builtin_providers - WHERE tenant_id = ? - ) ranked WHERE rn = 1 - """ - return sqlite_statement, parameters - - if database_scheme == "postgresql": - event.listen( - tool_database.engine, - "before_cursor_execute", - translate_postgresql_distinct_on, - retval=True, - ) - - try: - providers = ToolManager.list_default_builtin_providers(tenant_id) - finally: - if database_scheme == "postgresql": - event.remove( - tool_database.engine, - "before_cursor_execute", - translate_postgresql_distinct_on, - ) - - assert [provider.id for provider in providers] == [default_provider.id] - if database_scheme == "postgresql": - assert postgresql_statements - - -def test_list_providers_from_api_covers_builtin_api_workflow_and_mcp( - monkeypatch: pytest.MonkeyPatch, tool_database: _ToolDatabase -): - tenant_id = "00000000-0000-0000-0000-000000000001" - hardcoded_controller = SimpleNamespace(entity=SimpleNamespace(identity=SimpleNamespace(name="hardcoded"))) - plugin_controller = object.__new__(PluginToolProviderController) - plugin_controller.entity = SimpleNamespace(identity=SimpleNamespace(name="plugin-provider")) - - api_db_provider_good = _api_provider( - provider_id="00000000-0000-0000-0000-000000000002", tenant_id=tenant_id, name="api-good" - ) - api_db_provider_bad = _api_provider( - provider_id="00000000-0000-0000-0000-000000000003", tenant_id=tenant_id, name="api-bad" - ) - api_controller = SimpleNamespace(provider_id=api_db_provider_good.id) - - workflow_db_provider_good = _workflow_provider( - provider_id="00000000-0000-0000-0000-000000000004", tenant_id=tenant_id, name="workflow-good" - ) - workflow_db_provider_bad = _workflow_provider( - provider_id="00000000-0000-0000-0000-000000000005", tenant_id=tenant_id, name="workflow-bad" - ) - workflow_controller = SimpleNamespace(provider_id=workflow_db_provider_good.id) - tool_database.session.add_all( - [ - api_db_provider_good, - api_db_provider_bad, - _api_provider( - provider_id="00000000-0000-0000-0000-000000000006", - tenant_id="00000000-0000-0000-0000-000000000099", - name="foreign-api", - ), - workflow_db_provider_good, - workflow_db_provider_bad, - _workflow_provider( - provider_id="00000000-0000-0000-0000-000000000007", - tenant_id="00000000-0000-0000-0000-000000000099", - name="foreign-workflow", - ), - ] - ) - tool_database.session.commit() - - _setup_list_providers_from_api_mocks( - monkeypatch, - tool_database=tool_database, - hardcoded_controller=hardcoded_controller, - plugin_controller=plugin_controller, - api_controller=api_controller, - workflow_controller=workflow_controller, - ) - providers = ToolManager.list_providers_from_api(user_id="user-1", tenant_id=tenant_id, typ="") - - names = {provider.name for provider in providers} - assert {"hardcoded", "plugin-provider", "api-provider", "workflow-provider", "mcp-provider"} <= names - - -def test_get_api_provider_controller_returns_controller_and_credentials( - monkeypatch: pytest.MonkeyPatch, tool_database: _ToolDatabase -): +def test_get_api_provider_controller_returns_controller_and_credentials(tool_database: _ToolDatabase, tool_providers): tenant_id = "00000000-0000-0000-0000-000000000001" provider = _api_provider(provider_id="00000000-0000-0000-0000-000000000002", tenant_id=tenant_id) tool_database.session.add(provider) tool_database.session.commit() - monkeypatch.setattr("core.tools.tool_manager.db", tool_database) - controller = Mock() - - with patch("core.tools.tool_manager.ApiToolProviderController.from_db", return_value=controller) as mock_from_db: - built_controller, credentials = ToolManager.get_api_provider_controller(tenant_id, provider.id) - - assert built_controller is controller + controller, credentials = ToolManager.get_api_provider_controller( + tenant_id, provider.id, tool_providers=tool_providers + ) + assert controller.provider_id == provider.id + assert controller.entity.identity.name == provider.name + assert controller.entity.identity.author == "" assert credentials == provider.credentials - mock_from_db.assert_called_with(provider, ApiProviderAuthType.API_KEY_QUERY, session=ANY) - controller.load_bundled_tools.assert_called_once_with(provider.tools) + assert [tool.entity.identity.name for tool in controller.get_tools(tenant_id)] == [ + tool.operation_id for tool in provider.tools + ] -def test_user_get_api_provider_masks_credentials_and_adds_labels( - monkeypatch: pytest.MonkeyPatch, tool_database: _ToolDatabase +def test_get_api_provider_controller_not_found_raises( + monkeypatch: pytest.MonkeyPatch, tool_database: _ToolDatabase, tool_providers ): - tenant_id = "00000000-0000-0000-0000-000000000001" - provider = _api_provider(provider_id="00000000-0000-0000-0000-000000000002", tenant_id=tenant_id) - tool_database.session.add(provider) - tool_database.session.commit() - monkeypatch.setattr("core.tools.tool_manager.db", tool_database) - controller = Mock() - - with patch("core.tools.tool_manager.ApiToolProviderController.from_db", return_value=controller): - encrypter = Mock() - encrypter.decrypt.return_value = {"api_key_value": "secret"} - encrypter.mask_plugin_credentials.return_value = {"api_key_value": "***"} - with patch("core.tools.tool_manager.create_tool_provider_encrypter", return_value=(encrypter, Mock())): - with patch("core.tools.tool_manager.ToolLabelManager.get_tool_labels", return_value=["search"]): - user_payload = ToolManager.user_get_api_provider(provider.name, tenant_id) - - assert user_payload["credentials"]["api_key_value"] == "***" - assert user_payload["labels"] == ["search"] - - -def test_get_api_provider_controller_not_found_raises(monkeypatch: pytest.MonkeyPatch, tool_database: _ToolDatabase): provider_id = "00000000-0000-0000-0000-000000000002" tool_database.session.add( _api_provider( @@ -1065,43 +890,11 @@ def test_get_api_provider_controller_not_found_raises(monkeypatch: pytest.Monkey ) ) tool_database.session.commit() - monkeypatch.setattr("core.tools.tool_manager.db", tool_database) with pytest.raises(ToolProviderNotFoundError, match=f"api provider {provider_id} not found"): - ToolManager.get_api_provider_controller("00000000-0000-0000-0000-000000000001", provider_id) - - -def test_get_mcp_provider_controller_returns_controller(monkeypatch: pytest.MonkeyPatch, tool_database: _ToolDatabase): - provider_entity = SimpleNamespace(provider_icon={"background": "#111", "content": "M"}) - controller = Mock() - monkeypatch.setattr("core.tools.tool_manager.db", tool_database) - with patch("core.tools.tool_manager.MCPToolManageService") as mock_service_cls: - mock_service = mock_service_cls.return_value - mock_service.get_provider_by_persisted_reference.return_value = provider_entity - with patch("core.tools.tool_manager.MCPToolProviderController.from_db", return_value=controller): - built = ToolManager.get_mcp_provider_controller("tenant-1", "mcp-1") - assert built is controller - assert isinstance(mock_service_cls.call_args.kwargs["session"], Session) - - -def test_generate_mcp_tool_icon_url_returns_provider_icon( - monkeypatch: pytest.MonkeyPatch, tool_database: _ToolDatabase -): - provider_entity = SimpleNamespace(provider_icon={"background": "#111", "content": "M"}) - monkeypatch.setattr("core.tools.tool_manager.db", tool_database) - with patch("core.tools.tool_manager.MCPToolManageService") as mock_service_cls: - mock_service = mock_service_cls.return_value - mock_service.get_provider_entity_by_persisted_reference.return_value = provider_entity - assert ToolManager.generate_mcp_tool_icon_url("tenant-1", "mcp-1") == provider_entity.provider_icon - assert isinstance(mock_service_cls.call_args.kwargs["session"], Session) - - -def test_get_mcp_provider_controller_missing_raises(monkeypatch: pytest.MonkeyPatch, tool_database: _ToolDatabase): - monkeypatch.setattr("core.tools.tool_manager.db", tool_database) - with patch("core.tools.tool_manager.MCPToolManageService") as mock_service_cls: - mock_service_cls.return_value.get_provider_by_persisted_reference.side_effect = ValueError("missing") - with pytest.raises(ToolProviderNotFoundError, match="mcp provider mcp-1 not found"): - ToolManager.get_mcp_provider_controller("tenant-1", "mcp-1") + ToolManager.get_api_provider_controller( + "00000000-0000-0000-0000-000000000001", provider_id, tool_providers=tool_providers + ) def test_generate_tool_icon_urls_for_builtin_and_plugin(config_overrides: Callable[..., None]): @@ -1113,88 +906,42 @@ def test_generate_tool_icon_urls_for_builtin_and_plugin(config_overrides: Callab assert "/plugin/icon" in plugin_url -def test_generate_tool_icon_urls_for_workflow_and_api(monkeypatch: pytest.MonkeyPatch, tool_database: _ToolDatabase): - tenant_id = "00000000-0000-0000-0000-000000000001" - workflow_provider = _workflow_provider( - provider_id="00000000-0000-0000-0000-000000000002", - tenant_id=tenant_id, - ) - api_provider = _api_provider( - provider_id="00000000-0000-0000-0000-000000000003", - tenant_id=tenant_id, - icon='{"background":"#333","content":"A"}', - ) - tool_database.session.add_all([workflow_provider, api_provider]) - tool_database.session.commit() - monkeypatch.setattr("core.tools.tool_manager.db", tool_database) - - assert ToolManager.generate_workflow_tool_icon_url(tenant_id, workflow_provider.id) == { - "background": "#222", - "content": "W", - } - assert ToolManager.generate_api_tool_icon_url(tenant_id, api_provider.id) == { - "background": "#333", - "content": "A", - } - - -def test_generate_tool_icon_urls_missing_workflow_and_api_use_default( - monkeypatch: pytest.MonkeyPatch, tool_database: _ToolDatabase -): - tenant_id = "00000000-0000-0000-0000-000000000001" - foreign_tenant_id = "00000000-0000-0000-0000-000000000099" - workflow_provider_id = "00000000-0000-0000-0000-000000000002" - api_provider_id = "00000000-0000-0000-0000-000000000003" - tool_database.session.add_all( - [ - _workflow_provider(provider_id=workflow_provider_id, tenant_id=foreign_tenant_id), - _api_provider(provider_id=api_provider_id, tenant_id=foreign_tenant_id), - ] - ) - tool_database.session.commit() - monkeypatch.setattr("core.tools.tool_manager.db", tool_database) - - assert ToolManager.generate_workflow_tool_icon_url(tenant_id, workflow_provider_id)["background"] == "#252525" - assert ToolManager.generate_api_tool_icon_url(tenant_id, api_provider_id)["background"] == "#252525" - - -def test_get_tool_icon_for_builtin_provider_variants(): +def test_get_tool_icon_for_builtin_provider_variants(*, tool_providers): plugin_provider = object.__new__(PluginToolProviderController) plugin_provider.entity = SimpleNamespace(identity=SimpleNamespace(icon="plugin.svg")) with patch.object(ToolManager, "get_builtin_provider", return_value=plugin_provider): with patch.object(ToolManager, "generate_plugin_tool_icon_url", return_value="plugin-icon"): - assert ToolManager.get_tool_icon("tenant-1", ToolProviderType.BUILT_IN, "plugin-provider") == "plugin-icon" + assert ( + ToolManager.get_tool_icon( + "tenant-1", ToolProviderType.BUILT_IN, "plugin-provider", tool_providers=tool_providers + ) + == "plugin-icon" + ) with patch.object(ToolManager, "get_builtin_provider", return_value=SimpleNamespace()): with patch.object(ToolManager, "generate_builtin_tool_icon_url", return_value="builtin-icon"): - assert ToolManager.get_tool_icon("tenant-1", ToolProviderType.BUILT_IN, "time") == "builtin-icon" - - -def test_get_tool_icon_for_api_workflow_and_mcp(): - with patch.object(ToolManager, "generate_api_tool_icon_url", return_value={"background": "#000"}): - assert ToolManager.get_tool_icon("tenant-1", ToolProviderType.API, "api-1") == {"background": "#000"} - - with patch.object(ToolManager, "generate_workflow_tool_icon_url", return_value={"background": "#111"}): - assert ToolManager.get_tool_icon("tenant-1", ToolProviderType.WORKFLOW, "wf-1") == {"background": "#111"} - - with patch.object(ToolManager, "generate_mcp_tool_icon_url", return_value={"background": "#222"}): - assert ToolManager.get_tool_icon("tenant-1", ToolProviderType.MCP, "mcp-1") == {"background": "#222"} + assert ( + ToolManager.get_tool_icon("tenant-1", ToolProviderType.BUILT_IN, "time", tool_providers=tool_providers) + == "builtin-icon" + ) -def test_get_tool_icon_plugin_error_returns_default(): +def test_get_tool_icon_plugin_error_returns_default(*, tool_providers): plugin_provider = object.__new__(PluginToolProviderController) plugin_provider.entity = SimpleNamespace(identity=SimpleNamespace(icon="plugin.svg")) with patch.object(ToolManager, "get_plugin_provider", return_value=plugin_provider): with patch.object(ToolManager, "generate_plugin_tool_icon_url", side_effect=RuntimeError("fail")): - icon = ToolManager.get_tool_icon("tenant-1", ToolProviderType.PLUGIN, "plugin-provider") + icon = ToolManager.get_tool_icon( + "tenant-1", ToolProviderType.PLUGIN, "plugin-provider", tool_providers=tool_providers + ) assert icon["background"] == "#252525" -def test_get_tool_icon_invalid_provider_type_raises(): +def test_get_tool_icon_invalid_provider_type_raises(*, tool_providers): with pytest.raises(ValueError, match="provider type"): - ToolManager.get_tool_icon("tenant-1", "invalid", "x") # type: ignore[arg-type] + ToolManager.get_tool_icon("tenant-1", "invalid", "x", tool_providers=tool_providers) # type: ignore[arg-type] def test_convert_tool_parameters_type_agent_and_workflow_branches(): @@ -1232,7 +979,7 @@ def test_convert_tool_parameters_type_agent_and_workflow_branches(): variable_pool = Mock() variable_pool.get.return_value = SimpleNamespace(value="from-variable") - with patch("core.tools.tool_manager.convert_template", return_value=SimpleNamespace(text="from-template")): + with patch("services.tools.tool_manager.convert_template", return_value=SimpleNamespace(text="from-template")): mixed = ToolManager._convert_tool_parameters_type( parameters=[text_param], variable_pool=variable_pool, diff --git a/api/tests/unit_tests/core/tools/workflow_as_tool/test_provider.py b/api/tests/unit_tests/core/tools/workflow_as_tool/test_provider.py index efac03fa7ae8b1..d926a66aff73e6 100644 --- a/api/tests/unit_tests/core/tools/workflow_as_tool/test_provider.py +++ b/api/tests/unit_tests/core/tools/workflow_as_tool/test_provider.py @@ -5,13 +5,13 @@ from collections.abc import Iterator from types import SimpleNamespace from typing import Any, cast -from unittest.mock import PropertyMock, patch +from unittest.mock import Mock, patch import pytest from sqlalchemy import Engine from sqlalchemy.orm import Session, sessionmaker -from core.db.session_factory import session_factory +from core.app.apps.draft_variable_saver import DraftVariableSaverFactory from core.tools.__base.tool_runtime import ToolRuntime from core.tools.entities.common_entities import I18nObject from core.tools.entities.tool_entities import ( @@ -23,15 +23,15 @@ ToolProviderIdentity, ToolProviderType, ) -from core.tools.workflow_as_tool.provider import WorkflowToolProviderController -from core.tools.workflow_as_tool.tool import WorkflowTool -from extensions.ext_database import db from graphon.variables.input_entities import VariableEntity, VariableEntityType -from models.account import Account +from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole from models.base import TypeBase from models.model import App, AppMode from models.tools import WorkflowToolProvider from models.workflow import Workflow +from services.tools.workflow.provider import WorkflowToolProviderController +from services.tools.workflow.tool import WorkflowTool +from services.workflow.execution.ports import WorkflowRuntime from tests.unit_tests.model_factories import make_account, make_app, make_workflow @@ -42,15 +42,11 @@ def database_session(sqlite_engine: Engine) -> Iterator[Session]: TypeBase.metadata.create_all(sqlite_engine, tables=tables) session_maker = sessionmaker(bind=sqlite_engine, expire_on_commit=False) - with ( - patch.object(session_factory, "create_session", session_maker), - patch.object(type(db), "engine", new_callable=PropertyMock, return_value=sqlite_engine), - ): - with session_maker() as session: - yield session + with session_maker() as session: + yield session -def _controller(provider_id: str = "provider-1") -> WorkflowToolProviderController: +def _controller(provider_id: str = "provider-1", *, workflow_queries) -> WorkflowToolProviderController: entity = ToolProviderEntity( identity=ToolProviderIdentity( author="author", @@ -61,7 +57,12 @@ def _controller(provider_id: str = "provider-1") -> WorkflowToolProviderControll ), credentials_schema=[], ) - return WorkflowToolProviderController(entity=entity, provider_id=provider_id) + return WorkflowToolProviderController( + draft_variable_saver=Mock(return_value=Mock(spec=DraftVariableSaverFactory)), + entity=entity, + provider_id=provider_id, + queries=workflow_queries, + ) def _app(*, tenant_id: str | None = None) -> App: @@ -108,10 +109,13 @@ def _db_provider( ) -def _workflow_tool(name: str = "workflow_tool", *, tenant_id: str | None = None) -> WorkflowTool: +def _workflow_tool( + name: str = "workflow_tool", *, tenant_id: str | None = None, workflow_runtime: WorkflowRuntime +) -> WorkflowTool: app = _app(tenant_id=tenant_id) workflow = _workflow(app) return WorkflowTool( + draft_variable_saver=Mock(return_value=Mock(spec=DraftVariableSaverFactory)), workflow_as_tool_id="provider-1", entity=ToolEntity( identity=ToolIdentity( @@ -128,6 +132,8 @@ def _workflow_tool(name: str = "workflow_tool", *, tenant_id: str | None = None) workflow_entities={"app": app, "workflow": workflow}, version="1", workflow_call_depth=0, + workflow_runtime=workflow_runtime, + queries=workflow_runtime.tools, ) @@ -143,7 +149,16 @@ def _persist_provider_graph( workflow = _workflow(app, account) db_provider = _db_provider(app, account, parameter_configuration=parameter_configuration) - session.add_all([account, db_provider]) + tenant = Tenant(name="Tenant") + tenant.id = app.tenant_id + session.add_all( + [ + account, + db_provider, + tenant, + TenantAccountJoin(account_id=account.id, tenant_id=app.tenant_id, role=TenantAccountRole.OWNER), + ] + ) if include_app: session.add(app) if include_workflow: @@ -152,7 +167,7 @@ def _persist_provider_graph( return db_provider, app, account, workflow -def test_get_db_provider_tool_builds_entity(database_session: Session): +def test_get_tools_builds_entity(database_session: Session, *, workflow_queries): db_provider, app, user, _ = _persist_provider_graph( database_session, parameter_configuration=json.dumps( @@ -162,7 +177,7 @@ def test_get_db_provider_tool_builds_entity(database_session: Session): ] ), ) - controller = _controller(db_provider.id) + controller = _controller(db_provider.id, workflow_queries=workflow_queries) variables = [ VariableEntity( variable="country", @@ -180,19 +195,19 @@ def test_get_db_provider_tool_builds_entity(database_session: Session): with ( patch( - "core.tools.workflow_as_tool.provider.WorkflowAppConfigManager.convert_features", + "services.tools.workflow.provider.BaseAppConfigManager.convert_features", return_value=SimpleNamespace(file_upload=True), ), patch( - "core.tools.workflow_as_tool.provider.WorkflowToolConfigurationUtils.get_workflow_graph_variables", + "services.tools.workflow.provider.WorkflowToolConfigurationUtils.get_workflow_graph_variables", return_value=variables, ), patch( - "core.tools.workflow_as_tool.provider.WorkflowToolConfigurationUtils.get_workflow_graph_output", + "services.tools.workflow.provider.WorkflowToolConfigurationUtils.get_workflow_graph_output", return_value=outputs, ), ): - tool = controller._get_db_provider_tool(db_provider, app, session=database_session, user=user) + tool = controller.get_tools(db_provider.tenant_id)[0] assert tool.entity.identity.name == "workflow_tool" # "json" output is reserved for ToolInvokeMessage.VariableMessage and filtered out. @@ -204,41 +219,45 @@ def test_get_db_provider_tool_builds_entity(database_session: Session): assert controller.provider_type == ToolProviderType.WORKFLOW -def test_get_tool_returns_hit_or_none(): - controller = _controller() - tool = _workflow_tool() +def test_get_tool_returns_hit_or_none(*, workflow_runtime: WorkflowRuntime, workflow_queries): + controller = _controller(workflow_queries=workflow_queries) + tool = _workflow_tool(workflow_runtime=workflow_runtime) controller.tools = [tool] assert controller.get_tool("workflow_tool") is tool assert controller.get_tool("missing") is None -def test_get_tools_returns_cached(): - controller = _controller() - cached_tools = [_workflow_tool("wf-cached", tenant_id="tenant-1")] +def test_get_tools_returns_cached(*, workflow_runtime: WorkflowRuntime, workflow_queries): + controller = _controller(workflow_queries=workflow_queries) + cached_tools = [_workflow_tool("wf-cached", tenant_id="tenant-1", workflow_runtime=workflow_runtime)] controller.tools = cached_tools assert controller.get_tools("tenant-1") == cached_tools -def test_from_db_builds_controller(database_session: Session): +def test_from_db_builds_controller(database_session: Session, *, workflow_queries): db_provider, app, user, workflow = _persist_provider_graph(database_session) with ( patch( - "core.tools.workflow_as_tool.provider.WorkflowAppConfigManager.convert_features", + "services.tools.workflow.provider.BaseAppConfigManager.convert_features", return_value=SimpleNamespace(file_upload=False), ), patch( - "core.tools.workflow_as_tool.provider.WorkflowToolConfigurationUtils.get_workflow_graph_variables", + "services.tools.workflow.provider.WorkflowToolConfigurationUtils.get_workflow_graph_variables", return_value=[], ), patch( - "core.tools.workflow_as_tool.provider.WorkflowToolConfigurationUtils.get_workflow_graph_output", + "services.tools.workflow.provider.WorkflowToolConfigurationUtils.get_workflow_graph_output", return_value=[], ), ): - built = WorkflowToolProviderController.from_db(db_provider) + built = WorkflowToolProviderController.from_db( + db_provider, + draft_variable_saver=Mock(return_value=Mock(spec=DraftVariableSaverFactory)), + queries=workflow_queries, + ) assert isinstance(built, WorkflowToolProviderController) assert built.entity.identity.author == user.name @@ -248,21 +267,21 @@ def test_from_db_builds_controller(database_session: Session): assert built.tools[0].workflow_entities["workflow"].id == workflow.id -def test_get_tools_returns_empty_when_provider_missing(database_session: Session): +def test_get_tools_returns_empty_when_provider_missing(database_session: Session, *, workflow_queries): db_provider, _, _, _ = _persist_provider_graph(database_session) - controller = _controller(db_provider.id) + controller = _controller(db_provider.id, workflow_queries=workflow_queries) controller.tools = None assert controller.get_tools(str(uuid.uuid4())) == [] -def test_get_tools_raises_when_app_missing(database_session: Session): +def test_get_tools_raises_when_app_missing(database_session: Session, *, workflow_queries): db_provider, _, _, _ = _persist_provider_graph( database_session, include_app=False, include_workflow=False, ) - controller = _controller(db_provider.id) + controller = _controller(db_provider.id, workflow_queries=workflow_queries) controller.tools = None with pytest.raises(ValueError, match="app not found"): diff --git a/api/tests/unit_tests/services/tools/test_api_tools_manage_service.py b/api/tests/unit_tests/services/tools/test_api_tools_manage_service.py index 89e183b4b47721..5c4825019f7bee 100644 --- a/api/tests/unit_tests/services/tools/test_api_tools_manage_service.py +++ b/api/tests/unit_tests/services/tools/test_api_tools_manage_service.py @@ -2,7 +2,7 @@ import pytest -from services.tools.legacy_api_tools_manage_service import ApiToolManageService +from services.tools.api_tools_manage_service import ApiToolManageService def test_get_api_tool_provider_remote_schema_uses_ssrf_proxy_get(monkeypatch: pytest.MonkeyPatch) -> None: @@ -23,7 +23,7 @@ def test_get_api_tool_provider_remote_schema_uses_ssrf_proxy_get(monkeypatch: py mock_get = Mock(return_value=Mock(status_code=200, text=schema)) mock_parser = Mock() - monkeypatch.setattr("services.tools.legacy_api_tools_manage_service.ssrf_proxy.get", mock_get) + monkeypatch.setattr("services.tools.api_tools_manage_service.ssrf_proxy.get", mock_get) monkeypatch.setattr(ApiToolManageService, "parser_api_schema", mock_parser) result = ApiToolManageService.get_api_tool_provider_remote_schema("user-1", "tenant-1", url) diff --git a/api/tests/unit_tests/services/tools/test_builtin_tools_manage_service.py b/api/tests/unit_tests/services/tools/test_builtin_tools_manage_service.py index 3e0b96ebd2e238..da1608c501b4ea 100644 --- a/api/tests/unit_tests/services/tools/test_builtin_tools_manage_service.py +++ b/api/tests/unit_tests/services/tools/test_builtin_tools_manage_service.py @@ -11,8 +11,10 @@ from core.plugin.entities.plugin_daemon import CredentialType from models.tools import BuiltinToolProvider, ToolOAuthSystemClient, ToolOAuthTenantClient from repositories.credentials.query_repository import CredentialQueryRepository +from repositories.tools.provider_repository import ToolProviderRepository from services.tools import builtin_tools_manage_service as service_module -from services.tools.legacy_builtin_tools_manage_service import BuiltinToolManageService +from services.tools.builtin import credentials as credential_module +from services.tools.builtin_tools_manage_service import BuiltinToolManageService @pytest.fixture @@ -97,12 +99,13 @@ def test_transforms_each_tool(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(service_module.ToolManager, "get_builtin_provider", MagicMock(return_value=controller)) convert = MagicMock(return_value=MagicMock()) monkeypatch.setattr(service_module.ToolTransformService, "convert_tool_entity_to_api_entity", convert) - monkeypatch.setattr(service_module.ToolLabelManager, "get_tool_labels", MagicMock(return_value=[])) + controller.tool_labels = ["search"] result = BuiltinToolManageService.list_builtin_tool_provider_tools("tenant-1", "google") assert len(result) == 2 assert convert.call_count == 2 + assert all(call.kwargs["labels"] == ["search"] for call in convert.call_args_list) def test_empty_tools(self, monkeypatch: pytest.MonkeyPatch) -> None: controller = MagicMock() @@ -321,10 +324,14 @@ def test_returns_tenant_client_params_when_exists( encrypter = MagicMock() encrypter.decrypt.return_value = {"client_id": "id", "client_secret": "secret"} monkeypatch.setattr( - service_module, "create_provider_encrypter", MagicMock(return_value=(encrypter, MagicMock())) + credential_module, "create_provider_encrypter", MagicMock(return_value=(encrypter, MagicMock())) ) - result = BuiltinToolManageService.get_oauth_client("tenant-1", "google") + result = BuiltinToolManageService.get_oauth_client( + "tenant-1", + "google", + tool_providers=ToolProviderRepository(sessionmaker(repository_session.get_bind(), expire_on_commit=False)), + ) assert result == {"client_id": "id", "client_secret": "secret"} encrypter.decrypt.assert_called_once_with({"encrypted": "data"}) @@ -339,12 +346,16 @@ def test_falls_back_to_system_client( controller.get_oauth_client_schema.return_value = list[object]() monkeypatch.setattr(service_module.ToolManager, "get_builtin_provider", MagicMock(return_value=controller)) monkeypatch.setattr( - service_module, "create_provider_encrypter", MagicMock(return_value=(MagicMock(), MagicMock())) + credential_module, "create_provider_encrypter", MagicMock(return_value=(MagicMock(), MagicMock())) ) decrypt = MagicMock(return_value={"sys_key": "sys_val"}) - monkeypatch.setattr(service_module, "decrypt_system_params", decrypt) + monkeypatch.setattr(credential_module, "decrypt_system_params", decrypt) - result = BuiltinToolManageService.get_oauth_client("tenant-1", "google") + result = BuiltinToolManageService.get_oauth_client( + "tenant-1", + "google", + tool_providers=ToolProviderRepository(sessionmaker(repository_session.get_bind(), expire_on_commit=False)), + ) assert result == {"sys_key": "sys_val"} decrypt.assert_called_once_with("enc") diff --git a/api/tests/unit_tests/services/tools/test_mcp_tools_manage_service_lookup.py b/api/tests/unit_tests/services/tools/test_mcp_tools_manage_service_lookup.py index bd4b15d3055f40..ef073a6bda50cb 100644 --- a/api/tests/unit_tests/services/tools/test_mcp_tools_manage_service_lookup.py +++ b/api/tests/unit_tests/services/tools/test_mcp_tools_manage_service_lookup.py @@ -10,7 +10,7 @@ import pytest from models.tools import MCPToolProvider -from services.tools.legacy_mcp_tools_manage_service import MCPToolManageService +from services.tools.mcp_tools_manage_service import MCPToolManageService PROVIDER_UUID = "0b2bd1e4-3a7d-4a0b-9f2a-9b2d6f3c1e55" SERVER_IDENTIFIER = "monday_mcp" diff --git a/api/tests/unit_tests/services/tools/test_mcp_tools_transform.py b/api/tests/unit_tests/services/tools/test_mcp_tools_transform.py index 79c48e64edc708..c6b62c528de3b3 100644 --- a/api/tests/unit_tests/services/tools/test_mcp_tools_transform.py +++ b/api/tests/unit_tests/services/tools/test_mcp_tools_transform.py @@ -9,51 +9,13 @@ from core.tools.entities.api_entities import ToolApiEntity, ToolProviderApiEntity from core.tools.entities.common_entities import I18nObject from core.tools.entities.tool_entities import ToolParameter, ToolProviderType -from models.account import Account from models.tools import MCPToolProvider -from services.tools.legacy_tools_transform_service import ToolTransformService +from services.tools.provider_queries import MCPProviderRecord +from services.tools.tools_transform_service import ToolTransformService @pytest.fixture -def mock_user(): - """Provides a real mapped account returned by the provider lookup.""" - return Account(name="Test User", email="user@example.com") - - -@pytest.fixture -def mock_provider(mock_user, mocker: MockerFixture): - """Provides a mock MCPToolProvider with a loaded user.""" - provider = MCPToolProvider( - name="Test Provider", - server_identifier="test-provider", - server_url="https://example.com", - server_url_hash="hash", - icon="icon", - tenant_id="tenant-id", - user_id="user-id", - ) - mocker.patch.object(provider, "load_user", return_value=mock_user) - return provider - - -@pytest.fixture -def mock_provider_no_user(mocker: MockerFixture): - """Provides a mock MCPToolProvider with no user.""" - provider = MCPToolProvider( - name="Test Provider", - server_identifier="test-provider", - server_url="https://example.com", - server_url_hash="hash", - icon="icon", - tenant_id="tenant-id", - user_id="user-id", - ) - mocker.patch.object(provider, "load_user", return_value=None) - return provider - - -@pytest.fixture -def mock_provider_full(mock_user, mocker: MockerFixture): +def mock_provider_full(): """Provides a fully configured mock MCPToolProvider for detailed tests.""" provider = MCPToolProvider( name="Test MCP Provider", @@ -78,7 +40,6 @@ def mock_provider_full(mock_user, mocker: MockerFixture): mock_updated_at.timestamp.return_value = 1234567890 provider.updated_at = mock_updated_at - mocker.patch.object(provider, "load_user", return_value=mock_user) return provider @@ -109,7 +70,7 @@ def sample_mcp_tools(): class TestMCPToolTransform: """Test cases for MCP tool transformation methods.""" - def test_mcp_tool_to_user_tool_with_none_description(self, mock_provider): + def test_mcp_tool_to_user_tool_with_none_description(self): """Test that mcp_tool_to_user_tool handles None description correctly.""" # Create MCP tools with None description tools = [ @@ -129,7 +90,7 @@ def test_mcp_tool_to_user_tool_with_none_description(self, mock_provider): ] # Call the method - result = ToolTransformService.mcp_tool_to_user_tool(mock_provider, tools) + result = ToolTransformService.mcp_tool_to_user_tool(tools, user_name="Test User") # Verify the result assert len(result) == 2 @@ -149,7 +110,7 @@ def test_mcp_tool_to_user_tool_with_none_description(self, mock_provider): assert result[1].description.en_US == "" assert result[1].description.zh_Hans == "" - def test_mcp_tool_to_user_tool_with_description(self, mock_provider): + def test_mcp_tool_to_user_tool_with_description(self): """Test that mcp_tool_to_user_tool handles normal description correctly.""" # Create MCP tools with description tools = [ @@ -161,7 +122,7 @@ def test_mcp_tool_to_user_tool_with_description(self, mock_provider): ] # Call the method - result = ToolTransformService.mcp_tool_to_user_tool(mock_provider, tools) + result = ToolTransformService.mcp_tool_to_user_tool(tools, user_name="Test User") # Verify the result assert len(result) == 1 @@ -170,25 +131,25 @@ def test_mcp_tool_to_user_tool_with_description(self, mock_provider): assert result[0].description.en_US == "This is a test tool that does something useful" assert result[0].description.zh_Hans == "This is a test tool that does something useful" - def test_mcp_tool_to_user_tool_with_no_user(self, mock_provider_no_user): + def test_mcp_tool_to_user_tool_with_no_user(self): """Test that mcp_tool_to_user_tool handles None user correctly.""" # Create MCP tool tools = [MCPTool(name="tool1", description="Test tool", inputSchema={"type": "object", "properties": {}})] # Call the method - result = ToolTransformService.mcp_tool_to_user_tool(mock_provider_no_user, tools) + result = ToolTransformService.mcp_tool_to_user_tool(tools, user_name=None) # Verify the result assert len(result) == 1 assert result[0].author == "Anonymous" - def test_mcp_tool_to_user_tool_with_complex_schema(self, mock_provider, sample_mcp_tools): + def test_mcp_tool_to_user_tool_with_complex_schema(self, sample_mcp_tools): """Test that mcp_tool_to_user_tool correctly converts complex input schemas.""" # Use complex tool from fixtures tools = [sample_mcp_tools["complex"]] # Call the method - result = ToolTransformService.mcp_tool_to_user_tool(mock_provider, tools) + result = ToolTransformService.mcp_tool_to_user_tool(tools, user_name="Test User") # Verify the result assert len(result) == 1 @@ -352,7 +313,9 @@ def test_mcp_provider_to_user_provider_minimal_response(self, mock_provider_full } mocker.patch.object(mock_provider_full, "to_entity", return_value=mock_entity) - result = ToolTransformService.mcp_provider_to_user_provider(mock_provider_full) + result = ToolTransformService.mcp_provider_to_user_provider( + MCPProviderRecord(provider=mock_provider_full, author="Test User") + ) # Verify the result assert isinstance(result, ToolProviderApiEntity) @@ -390,7 +353,9 @@ def test_mcp_provider_to_user_provider_full_response(self, mock_provider_full, m } mocker.patch.object(mock_provider_full, "to_entity", return_value=mock_entity) - result = ToolTransformService.mcp_provider_to_user_provider(mock_provider_full) + result = ToolTransformService.mcp_provider_to_user_provider( + MCPProviderRecord(provider=mock_provider_full, author="Test User") + ) # Verify the result assert isinstance(result, ToolProviderApiEntity) @@ -403,7 +368,7 @@ def test_mcp_provider_to_user_provider_full_response(self, mock_provider_full, m assert len(result.tools) == 1 assert result.tools[0].description.en_US == "Tool description" - def test_mcp_tool_to_user_tool_falls_back_to_name_when_title_is_null(self, mock_provider): + def test_mcp_tool_to_user_tool_falls_back_to_name_when_title_is_null(self): """Tools where the server returned ``title: null`` should fall back to the tool name. Regression test for langgenius/dify#42453: Exa's MCP server returns @@ -424,7 +389,7 @@ def test_mcp_tool_to_user_tool_falls_back_to_name_when_title_is_null(self, mock_ ), ] - result = ToolTransformService.mcp_tool_to_user_tool(mock_provider, tools) + result = ToolTransformService.mcp_tool_to_user_tool(tools, user_name="Test User") assert len(result) == 2 # title is explicitly null -> must fall back to ``name`` @@ -436,7 +401,7 @@ def test_mcp_tool_to_user_tool_falls_back_to_name_when_title_is_null(self, mock_ assert result[1].label.en_US == "get_content" assert result[1].label.zh_Hans == "get_content" - def test_mcp_tool_to_user_tool_uses_title_when_provided(self, mock_provider): + def test_mcp_tool_to_user_tool_uses_title_when_provided(self): """When the server provides a non-null title, it should be used for the label.""" tools = [ MCPTool( @@ -447,7 +412,7 @@ def test_mcp_tool_to_user_tool_uses_title_when_provided(self, mock_provider): ) ] - result = ToolTransformService.mcp_tool_to_user_tool(mock_provider, tools) + result = ToolTransformService.mcp_tool_to_user_tool(tools, user_name="Test User") assert len(result) == 1 assert result[0].name == "search_web" diff --git a/api/tests/unit_tests/services/tools/test_tools_manage_service.py b/api/tests/unit_tests/services/tools/test_tools_manage_service.py index 5967a4ac3a8228..636841e3147c96 100644 --- a/api/tests/unit_tests/services/tools/test_tools_manage_service.py +++ b/api/tests/unit_tests/services/tools/test_tools_manage_service.py @@ -1,40 +1,54 @@ from unittest.mock import MagicMock, patch -from services.tools.legacy_tools_manage_service import ToolCommonService +from services.tools.tools_manage_service import ToolCommonService class TestToolCommonService: - @patch("services.tools.legacy_tools_manage_service.ToolTransformService") - @patch("services.tools.legacy_tools_manage_service.ToolManager") - def test_list_tool_providers_transforms_and_returns(self, mock_manager, mock_transform): + @patch("services.tools.tools_manage_service.ToolTransformService") + @patch("services.tools.tools_manage_service.ToolManager") + def test_list_tool_providers_transforms_and_returns( + self, mock_manager, mock_transform, *, workflow_queries, tool_providers + ): mock_provider1 = MagicMock() mock_provider1.to_dict.return_value = {"name": "provider1"} mock_provider2 = MagicMock() mock_provider2.to_dict.return_value = {"name": "provider2"} mock_manager.list_providers_from_api.return_value = [mock_provider1, mock_provider2] - result = ToolCommonService.list_tool_providers("user-1", "tenant-1") + result = ToolCommonService.list_tool_providers( + "user-1", "tenant-1", workflow_queries=workflow_queries, tool_providers=tool_providers + ) - mock_manager.list_providers_from_api.assert_called_once_with("user-1", "tenant-1", None) + mock_manager.list_providers_from_api.assert_called_once_with( + "user-1", "tenant-1", None, workflow_queries=workflow_queries, tool_providers=tool_providers + ) assert mock_transform.repack_provider.call_count == 2 assert result == [{"name": "provider1"}, {"name": "provider2"}] - @patch("services.tools.legacy_tools_manage_service.ToolTransformService") - @patch("services.tools.legacy_tools_manage_service.ToolManager") - def test_list_tool_providers_with_type_filter(self, mock_manager, mock_transform): + @patch("services.tools.tools_manage_service.ToolTransformService") + @patch("services.tools.tools_manage_service.ToolManager") + def test_list_tool_providers_with_type_filter( + self, mock_manager, mock_transform, *, workflow_queries, tool_providers + ): mock_manager.list_providers_from_api.return_value = [] - result = ToolCommonService.list_tool_providers("user-1", "tenant-1", typ="builtin") + result = ToolCommonService.list_tool_providers( + "user-1", "tenant-1", typ="builtin", workflow_queries=workflow_queries, tool_providers=tool_providers + ) - mock_manager.list_providers_from_api.assert_called_once_with("user-1", "tenant-1", "builtin") + mock_manager.list_providers_from_api.assert_called_once_with( + "user-1", "tenant-1", "builtin", workflow_queries=workflow_queries, tool_providers=tool_providers + ) assert result == [] - @patch("services.tools.legacy_tools_manage_service.ToolTransformService") - @patch("services.tools.legacy_tools_manage_service.ToolManager") - def test_list_tool_providers_empty(self, mock_manager, mock_transform): + @patch("services.tools.tools_manage_service.ToolTransformService") + @patch("services.tools.tools_manage_service.ToolManager") + def test_list_tool_providers_empty(self, mock_manager, mock_transform, *, workflow_queries, tool_providers): mock_manager.list_providers_from_api.return_value = [] - result = ToolCommonService.list_tool_providers("u", "t") + result = ToolCommonService.list_tool_providers( + "u", "t", workflow_queries=workflow_queries, tool_providers=tool_providers + ) assert result == [] mock_transform.repack_provider.assert_not_called() diff --git a/api/tests/unit_tests/services/tools/test_tools_transform_service.py b/api/tests/unit_tests/services/tools/test_tools_transform_service.py index 3bc8090a0cd12d..bbb51d2103d8db 100644 --- a/api/tests/unit_tests/services/tools/test_tools_transform_service.py +++ b/api/tests/unit_tests/services/tools/test_tools_transform_service.py @@ -1,12 +1,12 @@ -from unittest.mock import ANY, MagicMock, Mock, patch +from unittest.mock import MagicMock, Mock, patch from core.tools.__base.tool import Tool from core.tools.entities.api_entities import ToolApiEntity, ToolProviderApiEntity from core.tools.entities.common_entities import I18nObject -from core.tools.entities.tool_entities import ApiProviderAuthType, ToolParameter, ToolProviderType -from services.tools.legacy_tools_transform_service import ToolTransformService +from core.tools.entities.tool_entities import ToolParameter, ToolProviderType +from services.tools.tools_transform_service import ToolTransformService -MODULE = "services.tools.legacy_tools_transform_service" +MODULE = "services.tools.tools_transform_service" def _parameter( @@ -254,7 +254,7 @@ class TestWorkflowProviderToUserProvider: def test_workflow_provider_to_user_provider_with_workflow_app_id(self): """Test that workflow_provider_to_user_provider correctly sets workflow_app_id.""" - from core.tools.workflow_as_tool.provider import WorkflowToolProviderController + from services.tools.workflow.provider import WorkflowToolProviderController # Create mock workflow tool provider controller workflow_app_id = "app_123" @@ -292,7 +292,7 @@ def test_workflow_provider_to_user_provider_with_workflow_app_id(self): def test_workflow_provider_to_user_provider_without_workflow_app_id(self): """Test that workflow_provider_to_user_provider works when workflow_app_id is not provided.""" - from core.tools.workflow_as_tool.provider import WorkflowToolProviderController + from services.tools.workflow.provider import WorkflowToolProviderController # Create mock workflow tool provider controller provider_id = "provider_123" @@ -321,7 +321,7 @@ def test_workflow_provider_to_user_provider_without_workflow_app_id(self): def test_workflow_provider_to_user_provider_workflow_app_id_none(self): """Test that workflow_provider_to_user_provider handles None workflow_app_id explicitly.""" - from core.tools.workflow_as_tool.provider import WorkflowToolProviderController + from services.tools.workflow.provider import WorkflowToolProviderController # Create mock workflow tool provider controller provider_id = "provider_123" @@ -351,7 +351,7 @@ def test_workflow_provider_to_user_provider_workflow_app_id_none(self): def test_workflow_provider_to_user_provider_preserves_other_fields(self): """Test that workflow_provider_to_user_provider preserves all other entity fields.""" - from core.tools.workflow_as_tool.provider import WorkflowToolProviderController + from services.tools.workflow.provider import WorkflowToolProviderController # Create mock workflow tool provider controller with various fields workflow_app_id = "app_456" @@ -506,45 +506,3 @@ def test_list_type_uses_first_element(self): def test_missing_description_defaults_empty(self): schema = {"type": "object", "properties": {"f": {"type": "string"}}, "required": []} assert ToolTransformService.convert_mcp_schema_to_parameter(schema)[0].llm_description == "" - - -class TestApiProviderToController: - def test_api_key_header_auth(self): - db_provider = MagicMock() - db_provider.credentials = {"auth_type": "api_key_header"} - with patch(f"{MODULE}.db.session"), patch(f"{MODULE}.ApiToolProviderController") as ctrl_cls: - ctrl_cls.from_db.return_value = MagicMock() - ToolTransformService.api_provider_to_controller(db_provider) - ctrl_cls.from_db.assert_called_once_with( - db_provider=db_provider, auth_type=ApiProviderAuthType.API_KEY_HEADER, session=ANY - ) - - def test_api_key_query_auth(self): - db_provider = MagicMock() - db_provider.credentials = {"auth_type": "api_key_query"} - with patch(f"{MODULE}.db.session"), patch(f"{MODULE}.ApiToolProviderController") as ctrl_cls: - ctrl_cls.from_db.return_value = MagicMock() - ToolTransformService.api_provider_to_controller(db_provider) - ctrl_cls.from_db.assert_called_once_with( - db_provider=db_provider, auth_type=ApiProviderAuthType.API_KEY_QUERY, session=ANY - ) - - def test_legacy_api_key_maps_to_header(self): - db_provider = MagicMock() - db_provider.credentials = {"auth_type": "api_key"} - with patch(f"{MODULE}.db.session"), patch(f"{MODULE}.ApiToolProviderController") as ctrl_cls: - ctrl_cls.from_db.return_value = MagicMock() - ToolTransformService.api_provider_to_controller(db_provider) - ctrl_cls.from_db.assert_called_once_with( - db_provider=db_provider, auth_type=ApiProviderAuthType.API_KEY_HEADER, session=ANY - ) - - def test_unknown_auth_defaults_to_none(self): - db_provider = MagicMock() - db_provider.credentials = {"auth_type": "something_else"} - with patch(f"{MODULE}.db.session"), patch(f"{MODULE}.ApiToolProviderController") as ctrl_cls: - ctrl_cls.from_db.return_value = MagicMock() - ToolTransformService.api_provider_to_controller(db_provider) - ctrl_cls.from_db.assert_called_once_with( - db_provider=db_provider, auth_type=ApiProviderAuthType.NONE, session=ANY - ) From 899b0528b44866b814070653ccfe229b97e24717 Mon Sep 17 00:00:00 2001 From: Byron Wang Date: Fri, 9 Oct 2026 13:32:09 +0800 Subject: [PATCH 2/3] test: align tool provider fixtures with repository-backed services --- .../console/workspace/test_tool_provider.py | 47 ++++++++++----- .../core/tools/test_tool_manager.py | 58 ++++++++++++------- .../tools/workflow_as_tool/test_provider.py | 14 +++-- 3 files changed, 78 insertions(+), 41 deletions(-) diff --git a/api/tests/test_containers_integration_tests/controllers/console/workspace/test_tool_provider.py b/api/tests/test_containers_integration_tests/controllers/console/workspace/test_tool_provider.py index 35c8eb684f7474..68f081d15147e9 100644 --- a/api/tests/test_containers_integration_tests/controllers/console/workspace/test_tool_provider.py +++ b/api/tests/test_containers_integration_tests/controllers/console/workspace/test_tool_provider.py @@ -1,7 +1,8 @@ """Integration coverage for the console MCP provider HTTP endpoint.""" import json -from unittest.mock import MagicMock, patch +from types import SimpleNamespace +from unittest.mock import ANY, call, patch import pytest from flask import Flask @@ -9,6 +10,7 @@ from sqlalchemy.orm import Session from core.tools.entities.api_entities import ToolProviderApiEntity +from services.tools.mcp_tools_manage_service import ReconnectResult from tests.test_containers_integration_tests.controllers.console.helpers import ( authenticate_console_client, create_console_account_and_tenant, @@ -31,7 +33,7 @@ def _tool_payload() -> dict[str, object]: } -def _provider_entity() -> ToolProviderApiEntity: +def _provider_entity(*, tools: list[dict[str, object]] | None = None) -> ToolProviderApiEntity: return ToolProviderApiEntity.model_validate( { "id": "provider-1", @@ -48,7 +50,7 @@ def _provider_entity() -> ToolProviderApiEntity: "allow_delete": True, "plugin_id": "langgenius/provider", "plugin_unique_identifier": "langgenius/provider:1.0.0", - "tools": [_tool_payload()], + "tools": tools or [], "labels": ["utilities"], "server_url": "", "updated_at": 1710000000, @@ -73,20 +75,19 @@ def test_create_mcp_provider_populates_tools( client: FlaskClient, db_session_with_containers: Session, ) -> None: - account, _tenant = create_console_account_and_tenant(db_session_with_containers) + account, tenant = create_console_account_and_tenant(db_session_with_containers) headers = authenticate_console_client(client, account) - service = MagicMock() - service.create_provider.return_value = MagicMock(id="provider-1") - service.get_provider.return_value = MagicMock(id="provider-1", tenant_id="t1") + created_provider = _provider_entity() + connected_provider = _provider_entity(tools=[_tool_payload()]) + reconnect = ReconnectResult(authed=True, tools=json.dumps([_tool_payload()]), encrypted_credentials="{}") + db_provider = SimpleNamespace(authed=False, tools="[]") - with ( - patch("controllers.console.workspace.tool_providers.MCPToolManageService", return_value=service, autospec=True), - patch( - "services.tools.tools_transform_service.ToolTransformService.mcp_provider_to_user_provider", - return_value=_provider_entity(), - autospec=True, - ), - ): + with patch("controllers.console.workspace.tool_providers.MCPToolManageService", autospec=True) as service_cls: + service = service_cls.return_value + service.create_provider.return_value = "provider-1" + service.get_provider_by_id.return_value = db_provider + service_cls.provider_response.side_effect = [created_provider, connected_provider] + service_cls.reconnect_with_url.return_value = reconnect response = client.post( "/console/api/workspaces/current/tool-provider/mcp", data=json.dumps( @@ -106,6 +107,22 @@ def test_create_mcp_provider_populates_tools( content_type="application/json", ) + create_kwargs = service.create_provider.call_args.kwargs + assert create_kwargs["tenant_id"] == tenant.id + assert create_kwargs["user_id"] == account.id + assert create_kwargs["server_url"] == "http://example.com/mcp" + service_cls.provider_response.assert_has_calls( + [ + call(tenant_id=tenant.id, provider_id="provider-1", tool_providers=ANY), + call(tenant_id=tenant.id, provider_id="provider-1", tool_providers=ANY), + ] + ) + service_cls.reconnect_with_url.assert_called_once_with( + server_url="http://example.com/mcp", headers={}, timeout=5.0, sse_read_timeout=30.0 + ) + service.get_provider_by_id.assert_called_once_with(provider_id="provider-1", tenant_id=tenant.id) + assert db_provider.authed is True + assert db_provider.tools == reconnect.tools assert response.status_code == 200 body = response.get_json() assert body["id"] == "provider-1" diff --git a/api/tests/unit_tests/core/tools/test_tool_manager.py b/api/tests/unit_tests/core/tools/test_tool_manager.py index 49cca599a67e8b..eecedffa7062fa 100644 --- a/api/tests/unit_tests/core/tools/test_tool_manager.py +++ b/api/tests/unit_tests/core/tools/test_tool_manager.py @@ -568,42 +568,56 @@ def test_get_tool_runtime_workflow_path( def test_get_tool_runtime_plugin_path(*, tool_providers, workflow_queries): + tool = SimpleNamespace(runtime=ToolRuntime(tenant_id="tenant-1")) with patch.object( ToolManager, "get_plugin_provider", - return_value=SimpleNamespace(get_tool=lambda _: "plugin-tool"), + return_value=SimpleNamespace(get_tool=lambda _: tool), ): - assert ( - ToolManager.get_tool_runtime( - provider_type=ToolProviderType.PLUGIN, - provider_id="plugin-1", - tool_name="p", - tenant_id="tenant-1", - tool_providers=tool_providers, - workflow_queries=workflow_queries, - ) - == "plugin-tool" + result = ToolManager.get_tool_runtime( + provider_type=ToolProviderType.PLUGIN, + provider_id="plugin-1", + tool_name="p", + tenant_id="tenant-1", + user_id="caller-1", + invoke_from=InvokeFrom.SERVICE_API, + tool_invoke_from=ToolInvokeFrom.WORKFLOW, + tool_providers=tool_providers, + workflow_queries=workflow_queries, ) + assert result is tool + assert tool.runtime.tenant_id == "tenant-1" + assert tool.runtime.user_id == "caller-1" + assert tool.runtime.invoke_from == InvokeFrom.SERVICE_API + assert tool.runtime.tool_invoke_from == ToolInvokeFrom.WORKFLOW + def test_get_tool_runtime_mcp_path(*, tool_providers, workflow_queries): + tool = SimpleNamespace(runtime=ToolRuntime(tenant_id="tenant-1")) with patch.object( ToolManager, "get_mcp_provider_controller", - return_value=SimpleNamespace(get_tool=lambda _: "mcp-tool"), + return_value=SimpleNamespace(get_tool=lambda _: tool), ): - assert ( - ToolManager.get_tool_runtime( - provider_type=ToolProviderType.MCP, - provider_id="mcp-1", - tool_name="m", - tenant_id="tenant-1", - tool_providers=tool_providers, - workflow_queries=workflow_queries, - ) - == "mcp-tool" + result = ToolManager.get_tool_runtime( + provider_type=ToolProviderType.MCP, + provider_id="mcp-1", + tool_name="m", + tenant_id="tenant-1", + user_id="caller-1", + invoke_from=InvokeFrom.SERVICE_API, + tool_invoke_from=ToolInvokeFrom.WORKFLOW, + tool_providers=tool_providers, + workflow_queries=workflow_queries, ) + assert result is tool + assert tool.runtime.tenant_id == "tenant-1" + assert tool.runtime.user_id == "caller-1" + assert tool.runtime.invoke_from == InvokeFrom.SERVICE_API + assert tool.runtime.tool_invoke_from == ToolInvokeFrom.WORKFLOW + def test_get_tool_runtime_app_not_implemented(*, tool_providers, workflow_queries): with pytest.raises(NotImplementedError, match="app provider not implemented"): diff --git a/api/tests/unit_tests/core/tools/workflow_as_tool/test_provider.py b/api/tests/unit_tests/core/tools/workflow_as_tool/test_provider.py index d926a66aff73e6..990317c3eb9a75 100644 --- a/api/tests/unit_tests/core/tools/workflow_as_tool/test_provider.py +++ b/api/tests/unit_tests/core/tools/workflow_as_tool/test_provider.py @@ -5,7 +5,7 @@ from collections.abc import Iterator from types import SimpleNamespace from typing import Any, cast -from unittest.mock import Mock, patch +from unittest.mock import Mock, create_autospec, patch import pytest from sqlalchemy import Engine @@ -58,7 +58,9 @@ def _controller(provider_id: str = "provider-1", *, workflow_queries) -> Workflo credentials_schema=[], ) return WorkflowToolProviderController( - draft_variable_saver=Mock(return_value=Mock(spec=DraftVariableSaverFactory)), + draft_variable_saver=Mock( + return_value=create_autospec(DraftVariableSaverFactory, instance=True, spec_set=True) + ), entity=entity, provider_id=provider_id, queries=workflow_queries, @@ -115,7 +117,9 @@ def _workflow_tool( app = _app(tenant_id=tenant_id) workflow = _workflow(app) return WorkflowTool( - draft_variable_saver=Mock(return_value=Mock(spec=DraftVariableSaverFactory)), + draft_variable_saver=Mock( + return_value=create_autospec(DraftVariableSaverFactory, instance=True, spec_set=True) + ), workflow_as_tool_id="provider-1", entity=ToolEntity( identity=ToolIdentity( @@ -255,7 +259,9 @@ def test_from_db_builds_controller(database_session: Session, *, workflow_querie ): built = WorkflowToolProviderController.from_db( db_provider, - draft_variable_saver=Mock(return_value=Mock(spec=DraftVariableSaverFactory)), + draft_variable_saver=Mock( + return_value=create_autospec(DraftVariableSaverFactory, instance=True, spec_set=True) + ), queries=workflow_queries, ) From 3cecf0c3dd7f98f32fef6d1352af3c263015207b Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 05:31:32 +0000 Subject: [PATCH 3/3] [autofix.ci] apply automated fixes --- .../generated/api/console/apps/errors.gen.ts | 26 ------------------- .../generated/api/console/errors.gen.ts | 20 -------------- .../generated/api/console/rag/errors.gen.ts | 18 ------------- 3 files changed, 64 deletions(-) diff --git a/packages/contracts/generated/api/console/apps/errors.gen.ts b/packages/contracts/generated/api/console/apps/errors.gen.ts index 1ca81cfa7c000b..4cd4d5e228e674 100644 --- a/packages/contracts/generated/api/console/apps/errors.gen.ts +++ b/packages/contracts/generated/api/console/apps/errors.gen.ts @@ -605,8 +605,6 @@ export const get15Errors = { app_not_found: { status: 404, data: zAppNotFoundError }, } -export const get16Errors = { app_not_found: { status: 404, data: zAppNotFoundError } } - export const post13Errors = { no_file_uploaded: { status: 400, data: zNoFileUploadedError }, too_many_files: { status: 400, data: zTooManyFilesError }, @@ -919,17 +917,10 @@ export const get65Errors = { app_not_found: { status: 404, data: zAppNotFoundErr export const get66Errors = { app_not_found: { status: 404, data: zAppNotFoundError } } -export const get67Errors = { app_not_found: { status: 404, data: zAppNotFoundError } } - -export const post44Errors = { app_not_found: { status: 404, data: zAppNotFoundError } } - export const get68Errors = { - app_not_found: { status: 404, data: zAppNotFoundError }, draft_workflow_not_exist: { status: 404, data: zDraftWorkflowNotExistError }, } -export const post45Errors = { app_not_found: { status: 404, data: zAppNotFoundError } } - export const post46Errors = { app_not_found: { status: 404, data: zAppNotFoundError } } export const post47Errors = { app_not_found: { status: 404, data: zAppNotFoundError } } @@ -993,10 +984,6 @@ export const post56Errors = { app_not_found: { status: 404, data: zAppNotFoundEr export const post57Errors = { app_not_found: { status: 404, data: zAppNotFoundError } } -export const delete11Errors = { app_not_found: { status: 404, data: zAppNotFoundError } } - -export const get72Errors = { app_not_found: { status: 404, data: zAppNotFoundError } } - export const post58Errors = { app_not_found: { status: 404, data: zAppNotFoundError }, rate_limit_error: { status: 429, data: zRateLimitError }, @@ -1025,8 +1012,6 @@ export const get76Errors = { workflow_run_not_found: { status: 404, data: zWorkflowRunNotFoundError }, } -export const get77Errors = { app_not_found: { status: 404, data: zAppNotFoundError } } - export const post59Errors = { app_not_found: { status: 404, data: zAppNotFoundError }, rate_limit_error: { status: 429, data: zRateLimitError }, @@ -1037,18 +1022,7 @@ export const post60Errors = { rate_limit_error: { status: 429, data: zRateLimitError }, } -export const put6Errors = { app_not_found: { status: 404, data: zAppNotFoundError } } - -export const delete12Errors = { app_not_found: { status: 404, data: zAppNotFoundError } } - -export const get78Errors = { app_not_found: { status: 404, data: zAppNotFoundError } } - -export const patch2Errors = { app_not_found: { status: 404, data: zAppNotFoundError } } - -export const delete13Errors = { app_not_found: { status: 404, data: zAppNotFoundError } } - export const get79Errors = { - app_not_found: { status: 404, data: zAppNotFoundError }, draft_workflow_not_exist: { status: 404, data: zDraftWorkflowNotExistError }, } diff --git a/packages/contracts/generated/api/console/errors.gen.ts b/packages/contracts/generated/api/console/errors.gen.ts index 34e0844610ffc4..e3098084a0a65e 100644 --- a/packages/contracts/generated/api/console/errors.gen.ts +++ b/packages/contracts/generated/api/console/errors.gen.ts @@ -1,11 +1,8 @@ // This file is auto-generated by scripts/add-orpc-error-maps.ts export { delete10Errors as 'DELETE /apps/{app_id}/workflow/comments/{comment_id}' } from './apps/errors.gen.ts' export { delete10Errors as 'DELETE /knowledge-fs/spaces/{control_space_id}/research-tasks/{task_id}' } from './knowledge-fs/errors.gen.ts' -export { delete11Errors as 'DELETE /apps/{app_id}/workflows/draft/nodes/{node_id}/variables' } from './apps/errors.gen.ts' export { delete11Errors as 'DELETE /knowledge-fs/spaces/{control_space_id}/sources/{source_id}' } from './knowledge-fs/errors.gen.ts' -export { delete12Errors as 'DELETE /apps/{app_id}/workflows/draft/variables/{variable_id}' } from './apps/errors.gen.ts' export { delete12Errors as 'DELETE /knowledge-fs/spaces/{control_space_id}' } from './knowledge-fs/errors.gen.ts' -export { delete13Errors as 'DELETE /apps/{app_id}/workflows/draft/variables' } from './apps/errors.gen.ts' export { delete13Errors as 'DELETE /knowledge-fs/uploads/{upload_id}' } from './knowledge-fs/errors.gen.ts' export { delete14Errors as 'DELETE /apps/{app_id}/workflows/{workflow_id}' } from './apps/errors.gen.ts' export { delete16Errors as 'DELETE /workspaces/current/skills/{skill_id}' } from './workspaces/errors.gen.ts' @@ -14,15 +11,12 @@ export { delete2Errors as 'DELETE /apps/{app_id}/agent/config/skills/{name}' } f export { delete2Errors as 'DELETE /datasets/external-knowledge-api/{external_knowledge_api_id}' } from './datasets/errors.gen.ts' export { delete2Errors as 'DELETE /installed-apps/{installed_app_id}/saved-messages/{message_id}' } from './installed-apps/errors.gen.ts' export { delete2Errors as 'DELETE /knowledge-fs/spaces/{control_space_id}/app-bindings/{caller_kind}/{app_id}' } from './knowledge-fs/errors.gen.ts' -export { delete2Errors as 'DELETE /rag/pipelines/{pipeline_id}/workflows/draft/nodes/{node_id}/variables' } from './rag/errors.gen.ts' export { delete3Errors as 'DELETE /agent/{agent_id}/config/files/{name}' } from './agent/errors.gen.ts' export { delete3Errors as 'DELETE /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id}' } from './datasets/errors.gen.ts' export { delete3Errors as 'DELETE /knowledge-fs/spaces/{control_space_id}/documents/bulk' } from './knowledge-fs/errors.gen.ts' -export { delete3Errors as 'DELETE /rag/pipelines/{pipeline_id}/workflows/draft/variables/{variable_id}' } from './rag/errors.gen.ts' export { delete4Errors as 'DELETE /agent/{agent_id}/config/skills/{name}' } from './agent/errors.gen.ts' export { delete4Errors as 'DELETE /apps/{app_id}/annotations' } from './apps/errors.gen.ts' export { delete4Errors as 'DELETE /knowledge-fs/spaces/{control_space_id}/documents/{document_id}' } from './knowledge-fs/errors.gen.ts' -export { delete4Errors as 'DELETE /rag/pipelines/{pipeline_id}/workflows/draft/variables' } from './rag/errors.gen.ts' export { delete4Errors as 'DELETE /workspaces/current/members/{member_id}' } from './workspaces/errors.gen.ts' export { delete5Errors as 'DELETE /agent/{agent_id}/trace-config' } from './agent/errors.gen.ts' export { delete5Errors as 'DELETE /apps/{app_id}/chat-conversations/{conversation_id}' } from './apps/errors.gen.ts' @@ -63,7 +57,6 @@ export { get13Errors as 'GET /trial-apps/{app_id}/messages/{message_id}/suggeste export { get14Errors as 'GET /agent/{agent_id}/config/skills/{name}/files/content' } from './agent/errors.gen.ts' export { get14Errors as 'GET /apps/{app_id}/agent/config/skills/{name}/inspect' } from './apps/errors.gen.ts' export { get14Errors as 'GET /knowledge-fs/spaces/{control_space_id}/documents/{document_id}/revisions' } from './knowledge-fs/errors.gen.ts' -export { get14Errors as 'GET /rag/pipelines/{pipeline_id}/workflows/draft/nodes/{node_id}/variables' } from './rag/errors.gen.ts' export { get14Errors as 'GET /snippets/{snippet_id}/workflows/draft/variables' } from './snippets/errors.gen.ts' export { get14Errors as 'GET /trial-apps/{app_id}/parameters' } from './trial-apps/errors.gen.ts' export { get15Errors as 'GET /agent/{agent_id}/config/skills/{name}/files/download' } from './agent/errors.gen.ts' @@ -73,19 +66,16 @@ export { get15Errors as 'GET /rag/pipelines/{pipeline_id}/workflows/draft/pre-pr export { get15Errors as 'GET /snippets/{snippet_id}/workflows/draft' } from './snippets/errors.gen.ts' export { get15Errors as 'GET /trial-apps/{app_id}/site' } from './trial-apps/errors.gen.ts' export { get16Errors as 'GET /agent/{agent_id}/config/skills/{name}/files/preview' } from './agent/errors.gen.ts' -export { get16Errors as 'GET /apps/{app_id}/agent/logs' } from './apps/errors.gen.ts' export { get16Errors as 'GET /datasets/{dataset_id}/documents/{document_id}/indexing-estimate' } from './datasets/errors.gen.ts' export { get16Errors as 'GET /knowledge-fs/spaces/{control_space_id}/documents' } from './knowledge-fs/errors.gen.ts' export { get16Errors as 'GET /rag/pipelines/{pipeline_id}/workflows/draft/processing/parameters' } from './rag/errors.gen.ts' export { get16Errors as 'GET /trial-apps/{app_id}/workflows' } from './trial-apps/errors.gen.ts' export { get17Errors as 'GET /agent/{agent_id}/config/skills/{name}/inspect' } from './agent/errors.gen.ts' export { get17Errors as 'GET /knowledge-fs/spaces/{control_space_id}/external-access' } from './knowledge-fs/errors.gen.ts' -export { get17Errors as 'GET /rag/pipelines/{pipeline_id}/workflows/draft/system-variables' } from './rag/errors.gen.ts' export { get17Errors as 'GET /trial-apps/{app_id}' } from './trial-apps/errors.gen.ts' export { get18Errors as 'GET /agent/{agent_id}/config/skills' } from './agent/errors.gen.ts' export { get18Errors as 'GET /datasets/{dataset_id}/documents/{document_id}/notion/sync' } from './datasets/errors.gen.ts' export { get18Errors as 'GET /knowledge-fs/spaces/{control_space_id}/golden-questions' } from './knowledge-fs/errors.gen.ts' -export { get18Errors as 'GET /rag/pipelines/{pipeline_id}/workflows/draft/variables/{variable_id}' } from './rag/errors.gen.ts' export { get19Errors as 'GET /agent/{agent_id}/log-sources' } from './agent/errors.gen.ts' export { get19Errors as 'GET /knowledge-fs/spaces/{control_space_id}/jobs/{job_id}' } from './knowledge-fs/errors.gen.ts' export { get19Errors as 'GET /rag/pipelines/{pipeline_id}/workflows/draft/variables' } from './rag/errors.gen.ts' @@ -228,7 +218,6 @@ export { get63Errors as 'GET /apps/{app_id}/workflow/statistics/daily-terminals' export { get64Errors as 'GET /apps/{app_id}/workflow/statistics/token-costs' } from './apps/errors.gen.ts' export { get65Errors as 'GET /apps/{app_id}/workflows/default-workflow-block-configs/{block_type}' } from './apps/errors.gen.ts' export { get66Errors as 'GET /apps/{app_id}/workflows/default-workflow-block-configs' } from './apps/errors.gen.ts' -export { get67Errors as 'GET /apps/{app_id}/workflows/draft/conversation-variables' } from './apps/errors.gen.ts' export { get68Errors as 'GET /apps/{app_id}/workflows/draft/environment-variables' } from './apps/errors.gen.ts' export { get69Errors as 'GET /apps/{app_id}/workflows/draft/nodes/{node_id}/agent-composer/candidates' } from './apps/errors.gen.ts' export { get6Errors as 'GET /agent/{agent_id}/chat-messages' } from './agent/errors.gen.ts' @@ -239,13 +228,10 @@ export { get6Errors as 'GET /rag/pipelines/{pipeline_id}/exports' } from './rag/ export { get6Errors as 'GET /trial-apps/{app_id}/agent/config/skills/{name}/files/content' } from './trial-apps/errors.gen.ts' export { get70Errors as 'GET /apps/{app_id}/workflows/draft/nodes/{node_id}/agent-composer' } from './apps/errors.gen.ts' export { get71Errors as 'GET /apps/{app_id}/workflows/draft/nodes/{node_id}/last-run' } from './apps/errors.gen.ts' -export { get72Errors as 'GET /apps/{app_id}/workflows/draft/nodes/{node_id}/variables' } from './apps/errors.gen.ts' export { get73Errors as 'GET /apps/{app_id}/workflows/draft/runs/{run_id}/node-outputs/events' } from './apps/errors.gen.ts' export { get74Errors as 'GET /apps/{app_id}/workflows/draft/runs/{run_id}/node-outputs/{node_id}/{output_name}/preview' } from './apps/errors.gen.ts' export { get75Errors as 'GET /apps/{app_id}/workflows/draft/runs/{run_id}/node-outputs/{node_id}' } from './apps/errors.gen.ts' export { get76Errors as 'GET /apps/{app_id}/workflows/draft/runs/{run_id}/node-outputs' } from './apps/errors.gen.ts' -export { get77Errors as 'GET /apps/{app_id}/workflows/draft/system-variables' } from './apps/errors.gen.ts' -export { get78Errors as 'GET /apps/{app_id}/workflows/draft/variables/{variable_id}' } from './apps/errors.gen.ts' export { get79Errors as 'GET /apps/{app_id}/workflows/draft/variables' } from './apps/errors.gen.ts' export { get7Errors as 'GET /account/profile' } from './account/errors.gen.ts' export { get7Errors as 'GET /agent/{agent_id}/composer/candidates' } from './agent/errors.gen.ts' @@ -287,11 +273,9 @@ export { getErrors as 'GET /installed-apps/{installed_app_id}/conversations' } f export { getErrors as 'GET /remote-files/{url}' } from './remote-files/errors.gen.ts' export { getErrors as 'GET /trial-apps/{app_id}/agent-composer' } from './trial-apps/errors.gen.ts' export { getErrors as 'GET /website/crawl/status/{job_id}' } from './website/errors.gen.ts' -export { patch2Errors as 'PATCH /apps/{app_id}/workflows/draft/variables/{variable_id}' } from './apps/errors.gen.ts' export { patch2Errors as 'PATCH /datasets/{dataset_id}/documents/status/{action}/batch' } from './datasets/errors.gen.ts' export { patch2Errors as 'PATCH /installed-apps/{installed_app_id}/conversations/{c_id}/unpin' } from './installed-apps/errors.gen.ts' export { patch2Errors as 'PATCH /knowledge-fs/spaces/{control_space_id}/golden-questions/{question_id}' } from './knowledge-fs/errors.gen.ts' -export { patch2Errors as 'PATCH /rag/pipelines/{pipeline_id}/workflows/draft/variables/{variable_id}' } from './rag/errors.gen.ts' export { patch3Errors as 'PATCH /apps/{app_id}/workflows/{workflow_id}' } from './apps/errors.gen.ts' export { patch3Errors as 'PATCH /datasets/{dataset_id}/documents/{document_id}/processing/pause' } from './datasets/errors.gen.ts' export { patch3Errors as 'PATCH /installed-apps/{installed_app_id}' } from './installed-apps/errors.gen.ts' @@ -452,9 +436,7 @@ export { post42Errors as 'POST /apps/{app_id}/workflow/comments/{comment_id}/res export { post42Errors as 'POST /knowledge-fs/uploads' } from './knowledge-fs/errors.gen.ts' export { post43Errors as 'POST /apps/{app_id}/workflow/comments' } from './apps/errors.gen.ts' export { post43Errors as 'POST /workspaces/current/plugin/upload/github' } from './workspaces/errors.gen.ts' -export { post44Errors as 'POST /apps/{app_id}/workflows/draft/conversation-variables' } from './apps/errors.gen.ts' export { post44Errors as 'POST /workspaces/current/plugin/upload/pkg' } from './workspaces/errors.gen.ts' -export { post45Errors as 'POST /apps/{app_id}/workflows/draft/environment-variables' } from './apps/errors.gen.ts' export { post46Errors as 'POST /apps/{app_id}/workflows/draft/features' } from './apps/errors.gen.ts' export { post47Errors as 'POST /apps/{app_id}/workflows/draft/human-input/nodes/{node_id}/delivery-test' } from './apps/errors.gen.ts' export { post48Errors as 'POST /apps/{app_id}/workflows/draft/human-input/nodes/{node_id}/form/preview' } from './apps/errors.gen.ts' @@ -559,10 +541,8 @@ export { put4Errors as 'PUT /apps/{app_id}/workflow/comments/{comment_id}' } fro export { put4Errors as 'PUT /knowledge-fs/spaces/{control_space_id}/sources/{source_id}/sync-policy' } from './knowledge-fs/errors.gen.ts' export { put5Errors as 'PUT /apps/{app_id}/workflows/draft/nodes/{node_id}/agent-composer' } from './apps/errors.gen.ts' export { put5Errors as 'PUT /knowledge-fs/spaces/{control_space_id}/tags' } from './knowledge-fs/errors.gen.ts' -export { put6Errors as 'PUT /apps/{app_id}/workflows/draft/variables/{variable_id}/reset' } from './apps/errors.gen.ts' export { putErrors as 'PUT /agent/{agent_id}/build-draft' } from './agent/errors.gen.ts' export { putErrors as 'PUT /apps/{app_id}/network-access-group' } from './apps/errors.gen.ts' export { putErrors as 'PUT /knowledge-fs/spaces/{control_space_id}/app-bindings' } from './knowledge-fs/errors.gen.ts' -export { putErrors as 'PUT /rag/pipelines/{pipeline_id}/workflows/draft/variables/{variable_id}/reset' } from './rag/errors.gen.ts' export { putErrors as 'PUT /snippets/{snippet_id}/workflows/draft/nodes/{node_id}/agent-composer' } from './snippets/errors.gen.ts' export { putErrors as 'PUT /workspaces/current/agents/{agent_id}/skills' } from './workspaces/errors.gen.ts' diff --git a/packages/contracts/generated/api/console/rag/errors.gen.ts b/packages/contracts/generated/api/console/rag/errors.gen.ts index f87498e70afbc6..501d339d7f2b47 100644 --- a/packages/contracts/generated/api/console/rag/errors.gen.ts +++ b/packages/contracts/generated/api/console/rag/errors.gen.ts @@ -78,7 +78,6 @@ export const post10Errors = { pipeline_not_found: { status: 404, data: zPipeline export const get12Errors = { draft_workflow_not_exist: { status: 404, data: zDraftWorkflowNotExistError }, - pipeline_not_found: { status: 404, data: zPipelineNotFoundError }, } export const post11Errors = { pipeline_not_found: { status: 404, data: zPipelineNotFoundError } } @@ -89,10 +88,6 @@ export const get13Errors = { pipeline_not_found: { status: 404, data: zPipelineN export const post13Errors = { pipeline_not_found: { status: 404, data: zPipelineNotFoundError } } -export const delete2Errors = { pipeline_not_found: { status: 404, data: zPipelineNotFoundError } } - -export const get14Errors = { pipeline_not_found: { status: 404, data: zPipelineNotFoundError } } - export const get15Errors = { pipeline_not_found: { status: 404, data: zPipelineNotFoundError } } export const get16Errors = { pipeline_not_found: { status: 404, data: zPipelineNotFoundError } } @@ -102,21 +97,8 @@ export const post14Errors = { rate_limit_error: { status: 429, data: zRateLimitError }, } -export const get17Errors = { pipeline_not_found: { status: 404, data: zPipelineNotFoundError } } - -export const putErrors = { pipeline_not_found: { status: 404, data: zPipelineNotFoundError } } - -export const delete3Errors = { pipeline_not_found: { status: 404, data: zPipelineNotFoundError } } - -export const get18Errors = { pipeline_not_found: { status: 404, data: zPipelineNotFoundError } } - -export const patch2Errors = { pipeline_not_found: { status: 404, data: zPipelineNotFoundError } } - -export const delete4Errors = { pipeline_not_found: { status: 404, data: zPipelineNotFoundError } } - export const get19Errors = { draft_workflow_not_exist: { status: 404, data: zDraftWorkflowNotExistError }, - pipeline_not_found: { status: 404, data: zPipelineNotFoundError }, } export const get20Errors = {