mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 21:28:25 +08:00
Merge remote-tracking branch 'origin/main' into feat/support-agent-sandbox
This commit is contained in:
commit
4b62a5e29d
@ -942,7 +942,9 @@ class AccountTrialAppRecord(Base):
|
||||
class ExporleBanner(TypeBase):
|
||||
__tablename__ = "exporle_banners"
|
||||
__table_args__ = (sa.PrimaryKeyConstraint("id", name="exporler_banner_pkey"),)
|
||||
id: Mapped[str] = mapped_column(StringUUID, default=gen_uuidv4_string, init=False)
|
||||
id: Mapped[str] = mapped_column(
|
||||
StringUUID, insert_default=gen_uuidv4_string, default_factory=gen_uuidv4_string, init=False
|
||||
)
|
||||
content: Mapped[dict[str, Any]] = mapped_column(sa.JSON, nullable=False)
|
||||
link: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
sort: Mapped[int] = mapped_column(sa.Integer, nullable=False)
|
||||
@ -1863,7 +1865,9 @@ class AppAnnotationHitHistory(TypeBase):
|
||||
sa.Index("app_annotation_hit_histories_message_idx", "message_id"),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(StringUUID, default=lambda: str(uuid4()), init=False)
|
||||
id: Mapped[str] = mapped_column(
|
||||
StringUUID, insert_default=lambda: str(uuid4()), default_factory=lambda: str(uuid4()), init=False
|
||||
)
|
||||
app_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
annotation_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
source: Mapped[str] = mapped_column(LongText, nullable=False)
|
||||
|
||||
@ -1245,3 +1245,51 @@ class TestAppService:
|
||||
assert paginated_apps is not None
|
||||
assert paginated_apps.total == 1
|
||||
assert all("50%" in app.name for app in paginated_apps.items)
|
||||
|
||||
def test_get_app_code_by_id_not_found(
|
||||
self, db_session_with_containers: Session, mock_external_service_dependencies
|
||||
):
|
||||
"""Test get_app_code_by_id raises ValueError when site is missing."""
|
||||
from uuid import uuid4
|
||||
|
||||
from services.app_service import AppService
|
||||
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
AppService.get_app_code_by_id(str(uuid4()))
|
||||
|
||||
def test_get_app_id_by_code_not_found(
|
||||
self, db_session_with_containers: Session, mock_external_service_dependencies
|
||||
):
|
||||
"""Test get_app_id_by_code raises ValueError when code does not exist."""
|
||||
from services.app_service import AppService
|
||||
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
AppService.get_app_id_by_code("nonexistent-code")
|
||||
|
||||
def test_get_app_meta_returns_empty_when_workflow_missing(
|
||||
self, db_session_with_containers: Session, mock_external_service_dependencies
|
||||
):
|
||||
"""Test get_app_meta returns empty tool_icons when workflow is None."""
|
||||
from types import SimpleNamespace
|
||||
|
||||
from services.app_service import AppService
|
||||
|
||||
app_service = AppService()
|
||||
workflow_app = SimpleNamespace(mode="workflow", workflow=None)
|
||||
|
||||
meta = app_service.get_app_meta(workflow_app)
|
||||
assert meta == {"tool_icons": {}}
|
||||
|
||||
def test_get_app_meta_returns_empty_when_model_config_missing(
|
||||
self, db_session_with_containers: Session, mock_external_service_dependencies
|
||||
):
|
||||
"""Test get_app_meta returns empty tool_icons when app_model_config is None."""
|
||||
from types import SimpleNamespace
|
||||
|
||||
from services.app_service import AppService
|
||||
|
||||
app_service = AppService()
|
||||
chat_app = SimpleNamespace(mode="chat", app_model_config=None)
|
||||
|
||||
meta = app_service.get_app_meta(chat_app)
|
||||
assert meta == {"tool_icons": {}}
|
||||
|
||||
@ -1,12 +1,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
from faker import Faker
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.tools.entities.api_entities import ToolProviderApiEntity
|
||||
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 ApiProviderSchemaType, ToolProviderType
|
||||
from core.tools.entities.tool_entities import (
|
||||
ApiProviderSchemaType,
|
||||
ToolDescription,
|
||||
ToolEntity,
|
||||
ToolIdentity,
|
||||
ToolParameter,
|
||||
ToolProviderEntity,
|
||||
ToolProviderIdentity,
|
||||
ToolProviderType,
|
||||
)
|
||||
from models.tools import ApiToolProvider, BuiltinToolProvider, MCPToolProvider, WorkflowToolProvider
|
||||
from services.plugin.plugin_service import PluginService
|
||||
from services.tools.tools_transform_service import ToolTransformService
|
||||
@ -786,3 +798,192 @@ class TestToolTransformService:
|
||||
assert result is not None
|
||||
assert result == mock_controller
|
||||
mock_from_db.assert_called_once_with(provider)
|
||||
|
||||
|
||||
def _mock_tool(*, base_params, runtime_params):
|
||||
"""Helper to build a Mock tool with real entity objects.
|
||||
|
||||
Tool is abstract and requires runtime behaviour (fork_tool_runtime,
|
||||
get_runtime_parameters), so it stays as a Mock. Everything else uses
|
||||
real Pydantic instances.
|
||||
"""
|
||||
entity = ToolEntity(
|
||||
identity=ToolIdentity(
|
||||
author="test_author",
|
||||
name="test_tool",
|
||||
label=I18nObject(en_US="Test Tool"),
|
||||
provider="test_provider",
|
||||
),
|
||||
parameters=base_params or [],
|
||||
description=ToolDescription(
|
||||
human=I18nObject(en_US="Test description"),
|
||||
llm="Test description for LLM",
|
||||
),
|
||||
output_schema={},
|
||||
)
|
||||
mock_tool = Mock(spec=Tool)
|
||||
mock_tool.entity = entity
|
||||
mock_tool.get_runtime_parameters.return_value = runtime_params
|
||||
mock_tool.fork_tool_runtime.return_value = mock_tool
|
||||
return mock_tool
|
||||
|
||||
|
||||
def _param(name, *, form=ToolParameter.ToolParameterForm.FORM, label=None):
|
||||
return ToolParameter(
|
||||
name=name,
|
||||
label=I18nObject(en_US=label or name),
|
||||
human_description=I18nObject(en_US=name),
|
||||
type=ToolParameter.ToolParameterType.STRING,
|
||||
form=form,
|
||||
)
|
||||
|
||||
|
||||
class TestConvertToolEntityToApiEntity:
|
||||
"""Tests for ToolTransformService.convert_tool_entity_to_api_entity."""
|
||||
|
||||
def test_parameter_override(self):
|
||||
base = [_param("param1", label="Base 1"), _param("param2", label="Base 2")]
|
||||
runtime = [_param("param1", label="Runtime 1")]
|
||||
tool = _mock_tool(base_params=base, runtime_params=runtime)
|
||||
|
||||
result = ToolTransformService.convert_tool_entity_to_api_entity(tool, "t", None)
|
||||
|
||||
assert isinstance(result, ToolApiEntity)
|
||||
assert len(result.parameters) == 2
|
||||
assert next(p for p in result.parameters if p.name == "param1").label.en_US == "Runtime 1"
|
||||
assert next(p for p in result.parameters if p.name == "param2").label.en_US == "Base 2"
|
||||
|
||||
def test_additional_runtime_parameters(self):
|
||||
base = [_param("param1", label="Base 1")]
|
||||
runtime = [_param("param1", label="Runtime 1"), _param("runtime_only", label="Runtime Only")]
|
||||
tool = _mock_tool(base_params=base, runtime_params=runtime)
|
||||
|
||||
result = ToolTransformService.convert_tool_entity_to_api_entity(tool, "t", None)
|
||||
|
||||
assert len(result.parameters) == 2
|
||||
names = [p.name for p in result.parameters]
|
||||
assert "param1" in names
|
||||
assert "runtime_only" in names
|
||||
|
||||
def test_non_form_runtime_parameters_excluded(self):
|
||||
base = [_param("param1")]
|
||||
runtime = [
|
||||
_param("param1", label="Runtime 1"),
|
||||
_param("llm_param", form=ToolParameter.ToolParameterForm.LLM),
|
||||
]
|
||||
tool = _mock_tool(base_params=base, runtime_params=runtime)
|
||||
|
||||
result = ToolTransformService.convert_tool_entity_to_api_entity(tool, "t", None)
|
||||
|
||||
assert len(result.parameters) == 1
|
||||
assert result.parameters[0].name == "param1"
|
||||
|
||||
def test_empty_parameters(self):
|
||||
tool = _mock_tool(base_params=[], runtime_params=[])
|
||||
|
||||
result = ToolTransformService.convert_tool_entity_to_api_entity(tool, "t", None)
|
||||
|
||||
assert isinstance(result, ToolApiEntity)
|
||||
assert len(result.parameters) == 0
|
||||
|
||||
def test_none_parameters(self):
|
||||
tool = _mock_tool(base_params=None, runtime_params=[])
|
||||
|
||||
result = ToolTransformService.convert_tool_entity_to_api_entity(tool, "t", None)
|
||||
|
||||
assert isinstance(result, ToolApiEntity)
|
||||
assert len(result.parameters) == 0
|
||||
|
||||
def test_parameter_order_preserved(self):
|
||||
base = [_param("p1", label="B1"), _param("p2", label="B2"), _param("p3", label="B3")]
|
||||
runtime = [_param("p2", label="R2"), _param("p4", label="R4")]
|
||||
tool = _mock_tool(base_params=base, runtime_params=runtime)
|
||||
|
||||
result = ToolTransformService.convert_tool_entity_to_api_entity(tool, "t", None)
|
||||
|
||||
assert [p.name for p in result.parameters] == ["p1", "p2", "p3", "p4"]
|
||||
assert result.parameters[1].label.en_US == "R2"
|
||||
|
||||
|
||||
class TestWorkflowProviderToUserProvider:
|
||||
"""Tests for ToolTransformService.workflow_provider_to_user_provider."""
|
||||
|
||||
@staticmethod
|
||||
def _make_controller(provider_id="provider_123", **identity_overrides):
|
||||
from core.tools.workflow_as_tool.provider import WorkflowToolProviderController
|
||||
|
||||
defaults = {
|
||||
"author": "test_author",
|
||||
"name": "test_workflow_tool",
|
||||
"description": I18nObject(en_US="Test description"),
|
||||
"icon": '{"type": "emoji", "content": "🔧"}',
|
||||
"icon_dark": None,
|
||||
"label": I18nObject(en_US="Test Workflow Tool"),
|
||||
}
|
||||
defaults.update(identity_overrides)
|
||||
identity = ToolProviderIdentity(**defaults)
|
||||
entity = ToolProviderEntity(identity=identity)
|
||||
return WorkflowToolProviderController(entity=entity, provider_id=provider_id)
|
||||
|
||||
def test_with_workflow_app_id(self):
|
||||
ctrl = self._make_controller()
|
||||
|
||||
result = ToolTransformService.workflow_provider_to_user_provider(
|
||||
provider_controller=ctrl,
|
||||
labels=["l1", "l2"],
|
||||
workflow_app_id="app_123",
|
||||
)
|
||||
|
||||
assert isinstance(result, ToolProviderApiEntity)
|
||||
assert result.id == "provider_123"
|
||||
assert result.type == ToolProviderType.WORKFLOW
|
||||
assert result.workflow_app_id == "app_123"
|
||||
assert result.labels == ["l1", "l2"]
|
||||
assert result.is_team_authorization is True
|
||||
|
||||
def test_without_workflow_app_id(self):
|
||||
ctrl = self._make_controller()
|
||||
|
||||
result = ToolTransformService.workflow_provider_to_user_provider(
|
||||
provider_controller=ctrl,
|
||||
labels=["l1"],
|
||||
)
|
||||
|
||||
assert result.workflow_app_id is None
|
||||
|
||||
def test_workflow_app_id_none_explicit(self):
|
||||
ctrl = self._make_controller()
|
||||
|
||||
result = ToolTransformService.workflow_provider_to_user_provider(
|
||||
provider_controller=ctrl,
|
||||
labels=None,
|
||||
workflow_app_id=None,
|
||||
)
|
||||
|
||||
assert result.workflow_app_id is None
|
||||
assert result.labels == []
|
||||
|
||||
def test_preserves_other_fields(self):
|
||||
ctrl = self._make_controller(
|
||||
"provider_456",
|
||||
author="another_author",
|
||||
name="another_workflow_tool",
|
||||
description=I18nObject(en_US="Another desc", zh_Hans="Another desc"),
|
||||
icon='{"type": "emoji", "content": "⚙️"}',
|
||||
icon_dark='{"type": "emoji", "content": "🔧"}',
|
||||
label=I18nObject(en_US="Another Tool", zh_Hans="Another Tool"),
|
||||
)
|
||||
|
||||
result = ToolTransformService.workflow_provider_to_user_provider(
|
||||
provider_controller=ctrl,
|
||||
labels=["automation"],
|
||||
workflow_app_id="app_456",
|
||||
)
|
||||
|
||||
assert result.id == "provider_456"
|
||||
assert result.author == "another_author"
|
||||
assert result.name == "another_workflow_tool"
|
||||
assert result.type == ToolProviderType.WORKFLOW
|
||||
assert result.workflow_app_id == "app_456"
|
||||
assert result.is_team_authorization is True
|
||||
assert result.allow_delete is True
|
||||
|
||||
@ -1,683 +0,0 @@
|
||||
"""Unit tests for services.app_service."""
|
||||
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
from typing import cast
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from core.errors.error import ProviderTokenNotInitError
|
||||
from models import Account, Tenant
|
||||
from models.model import App, AppMode, IconType
|
||||
from services.app_service import AppService
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def service() -> AppService:
|
||||
"""Provide AppService instance."""
|
||||
return AppService()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def account() -> Account:
|
||||
"""Create account object for create_app tests."""
|
||||
tenant = Tenant(name="Tenant")
|
||||
tenant.id = "tenant-1"
|
||||
result = Account(name="Account User", email="account@example.com")
|
||||
result.id = "acc-1"
|
||||
result._current_tenant = tenant
|
||||
return result
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def default_args() -> dict:
|
||||
"""Create default create_app args."""
|
||||
return {
|
||||
"name": "Test App",
|
||||
"mode": AppMode.CHAT.value,
|
||||
"icon": "🤖",
|
||||
"icon_background": "#FFFFFF",
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app_template() -> dict:
|
||||
"""Create basic app template for create_app tests."""
|
||||
return {
|
||||
AppMode.CHAT: {
|
||||
"app": {},
|
||||
"model_config": {
|
||||
"model": {
|
||||
"provider": "provider-a",
|
||||
"name": "model-a",
|
||||
"mode": "chat",
|
||||
"completion_params": {},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def _make_current_user() -> Account:
|
||||
user = Account(name="Tester", email="tester@example.com")
|
||||
user.id = "user-1"
|
||||
tenant = Tenant(name="Tenant")
|
||||
tenant.id = "tenant-1"
|
||||
user._current_tenant = tenant
|
||||
return user
|
||||
|
||||
|
||||
class TestAppServicePagination:
|
||||
"""Test suite for get_paginate_apps."""
|
||||
|
||||
def test_get_paginate_apps_should_return_none_when_tag_filter_empty(self, service: AppService) -> None:
|
||||
"""Test pagination returns None when tag filter has no targets."""
|
||||
# Arrange
|
||||
args = {"mode": "chat", "page": 1, "limit": 20, "tag_ids": ["tag-1"]}
|
||||
|
||||
with patch("services.app_service.TagService.get_target_ids_by_tag_ids", return_value=[]):
|
||||
# Act
|
||||
result = service.get_paginate_apps("user-1", "tenant-1", args)
|
||||
|
||||
# Assert
|
||||
assert result is None
|
||||
|
||||
def test_get_paginate_apps_should_delegate_to_db_paginate(self, service: AppService) -> None:
|
||||
"""Test pagination delegates to db.paginate when filters are valid."""
|
||||
# Arrange
|
||||
args = {
|
||||
"mode": "workflow",
|
||||
"is_created_by_me": True,
|
||||
"name": "My_App%",
|
||||
"tag_ids": ["tag-1"],
|
||||
"page": 2,
|
||||
"limit": 10,
|
||||
}
|
||||
expected_pagination = MagicMock()
|
||||
|
||||
with (
|
||||
patch("services.app_service.TagService.get_target_ids_by_tag_ids", return_value=["app-1"]),
|
||||
patch("libs.helper.escape_like_pattern", return_value="escaped"),
|
||||
patch("services.app_service.db") as mock_db,
|
||||
):
|
||||
mock_db.paginate.return_value = expected_pagination
|
||||
|
||||
# Act
|
||||
result = service.get_paginate_apps("user-1", "tenant-1", args)
|
||||
|
||||
# Assert
|
||||
assert result is expected_pagination
|
||||
mock_db.paginate.assert_called_once()
|
||||
|
||||
|
||||
class TestAppServiceCreate:
|
||||
"""Test suite for create_app."""
|
||||
|
||||
def test_create_app_should_create_with_matching_default_model(
|
||||
self,
|
||||
service: AppService,
|
||||
account: Account,
|
||||
default_args: dict,
|
||||
app_template: dict,
|
||||
) -> None:
|
||||
"""Test create_app uses matching default model and persists app config."""
|
||||
# Arrange
|
||||
app_instance = SimpleNamespace(id="app-1", tenant_id="tenant-1")
|
||||
app_model_config = SimpleNamespace(id="cfg-1")
|
||||
model_instance = SimpleNamespace(
|
||||
model_name="model-a",
|
||||
provider="provider-a",
|
||||
model_type_instance=MagicMock(),
|
||||
credentials={"k": "v"},
|
||||
)
|
||||
|
||||
with (
|
||||
patch("services.app_service.default_app_templates", app_template),
|
||||
patch("services.app_service.App", return_value=app_instance),
|
||||
patch("services.app_service.AppModelConfig", return_value=app_model_config),
|
||||
patch("services.app_service.ModelManager") as mock_model_manager,
|
||||
patch("services.app_service.db") as mock_db,
|
||||
patch("services.app_service.app_was_created") as mock_event,
|
||||
patch("services.app_service.FeatureService.get_system_features") as mock_features,
|
||||
patch("services.app_service.BillingService") as mock_billing,
|
||||
patch("services.app_service.dify_config") as mock_config,
|
||||
):
|
||||
manager = mock_model_manager.return_value
|
||||
manager.get_default_model_instance.return_value = model_instance
|
||||
mock_features.return_value = SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False))
|
||||
mock_config.BILLING_ENABLED = True
|
||||
|
||||
# Act
|
||||
result = service.create_app("tenant-1", default_args, account)
|
||||
|
||||
# Assert
|
||||
assert result is app_instance
|
||||
assert app_instance.app_model_config_id == "cfg-1"
|
||||
mock_db.session.add.assert_any_call(app_instance)
|
||||
mock_db.session.add.assert_any_call(app_model_config)
|
||||
assert mock_db.session.flush.call_count == 2
|
||||
mock_db.session.commit.assert_called_once()
|
||||
mock_event.send.assert_called_once_with(app_instance, account=account)
|
||||
mock_billing.clean_billing_info_cache.assert_called_once_with("tenant-1")
|
||||
|
||||
def test_create_app_should_raise_when_model_schema_missing(
|
||||
self,
|
||||
service: AppService,
|
||||
account: Account,
|
||||
default_args: dict,
|
||||
app_template: dict,
|
||||
) -> None:
|
||||
"""Test create_app raises ValueError when non-matching model has no schema."""
|
||||
# Arrange
|
||||
app_instance = SimpleNamespace(id="app-1")
|
||||
model_instance = SimpleNamespace(
|
||||
model_name="model-b",
|
||||
provider="provider-b",
|
||||
model_type_instance=MagicMock(),
|
||||
credentials={"k": "v"},
|
||||
)
|
||||
model_instance.model_type_instance.get_model_schema.return_value = None
|
||||
|
||||
with (
|
||||
patch("services.app_service.default_app_templates", app_template),
|
||||
patch("services.app_service.App", return_value=app_instance),
|
||||
patch("services.app_service.ModelManager") as mock_model_manager,
|
||||
patch("services.app_service.db") as mock_db,
|
||||
):
|
||||
manager = mock_model_manager.return_value
|
||||
manager.get_default_model_instance.return_value = model_instance
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(ValueError, match="model schema not found"):
|
||||
service.create_app("tenant-1", default_args, account)
|
||||
mock_db.session.commit.assert_not_called()
|
||||
|
||||
def test_create_app_should_fallback_to_default_provider_when_model_missing(
|
||||
self,
|
||||
service: AppService,
|
||||
account: Account,
|
||||
default_args: dict,
|
||||
app_template: dict,
|
||||
) -> None:
|
||||
"""Test create_app falls back to provider/model name when no default model instance is available."""
|
||||
# Arrange
|
||||
app_instance = SimpleNamespace(id="app-1", tenant_id="tenant-1")
|
||||
app_model_config = SimpleNamespace(id="cfg-1")
|
||||
|
||||
with (
|
||||
patch("services.app_service.default_app_templates", app_template),
|
||||
patch("services.app_service.App", return_value=app_instance),
|
||||
patch("services.app_service.AppModelConfig", return_value=app_model_config),
|
||||
patch("services.app_service.ModelManager") as mock_model_manager,
|
||||
patch("services.app_service.db") as mock_db,
|
||||
patch("services.app_service.app_was_created") as mock_event,
|
||||
patch("services.app_service.FeatureService.get_system_features") as mock_features,
|
||||
patch("services.app_service.EnterpriseService") as mock_enterprise,
|
||||
patch("services.app_service.dify_config") as mock_config,
|
||||
):
|
||||
manager = mock_model_manager.return_value
|
||||
manager.get_default_model_instance.side_effect = ProviderTokenNotInitError("not ready")
|
||||
manager.get_default_provider_model_name.return_value = ("fallback-provider", "fallback-model")
|
||||
mock_features.return_value = SimpleNamespace(webapp_auth=SimpleNamespace(enabled=True))
|
||||
mock_config.BILLING_ENABLED = False
|
||||
|
||||
# Act
|
||||
result = service.create_app("tenant-1", default_args, account)
|
||||
|
||||
# Assert
|
||||
assert result is app_instance
|
||||
mock_event.send.assert_called_once_with(app_instance, account=account)
|
||||
mock_db.session.commit.assert_called_once()
|
||||
mock_enterprise.WebAppAuth.update_app_access_mode.assert_called_once_with("app-1", "private")
|
||||
|
||||
def test_create_app_should_log_and_fallback_on_unexpected_model_error(
|
||||
self,
|
||||
service: AppService,
|
||||
account: Account,
|
||||
default_args: dict,
|
||||
app_template: dict,
|
||||
) -> None:
|
||||
"""Test unexpected model manager errors are logged and fallback provider is used."""
|
||||
# Arrange
|
||||
app_instance = SimpleNamespace(id="app-1", tenant_id="tenant-1")
|
||||
app_model_config = SimpleNamespace(id="cfg-1")
|
||||
|
||||
with (
|
||||
patch("services.app_service.default_app_templates", app_template),
|
||||
patch("services.app_service.App", return_value=app_instance),
|
||||
patch("services.app_service.AppModelConfig", return_value=app_model_config),
|
||||
patch("services.app_service.ModelManager") as mock_model_manager,
|
||||
patch("services.app_service.db"),
|
||||
patch("services.app_service.app_was_created"),
|
||||
patch(
|
||||
"services.app_service.FeatureService.get_system_features",
|
||||
return_value=SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False)),
|
||||
),
|
||||
patch("services.app_service.dify_config", new=SimpleNamespace(BILLING_ENABLED=False)),
|
||||
patch("services.app_service.logger") as mock_logger,
|
||||
):
|
||||
manager = mock_model_manager.return_value
|
||||
manager.get_default_model_instance.side_effect = RuntimeError("boom")
|
||||
manager.get_default_provider_model_name.return_value = ("fallback-provider", "fallback-model")
|
||||
|
||||
# Act
|
||||
result = service.create_app("tenant-1", default_args, account)
|
||||
|
||||
# Assert
|
||||
assert result is app_instance
|
||||
mock_logger.exception.assert_called_once()
|
||||
|
||||
|
||||
class TestAppServiceGetAndUpdate:
|
||||
"""Test suite for app retrieval and update methods."""
|
||||
|
||||
def test_get_app_should_return_original_when_not_agent_app(self, service: AppService) -> None:
|
||||
"""Test get_app returns original app for non-agent modes."""
|
||||
# Arrange
|
||||
app = MagicMock()
|
||||
app.mode = AppMode.CHAT
|
||||
app.is_agent = False
|
||||
|
||||
with patch("services.app_service.current_user", _make_current_user()):
|
||||
# Act
|
||||
result = service.get_app(app)
|
||||
|
||||
# Assert
|
||||
assert result is app
|
||||
|
||||
def test_get_app_should_return_original_when_model_config_missing(self, service: AppService) -> None:
|
||||
"""Test get_app returns app when agent mode has no model config."""
|
||||
# Arrange
|
||||
app = MagicMock()
|
||||
app.id = "app-1"
|
||||
app.mode = AppMode.AGENT_CHAT
|
||||
app.is_agent = False
|
||||
app.app_model_config = None
|
||||
|
||||
with patch("services.app_service.current_user", _make_current_user()):
|
||||
# Act
|
||||
result = service.get_app(app)
|
||||
|
||||
# Assert
|
||||
assert result is app
|
||||
|
||||
def test_get_app_should_mask_tool_parameters_for_agent_tools(self, service: AppService) -> None:
|
||||
"""Test get_app decrypts and masks secret tool parameters."""
|
||||
# Arrange
|
||||
tool = {
|
||||
"provider_type": "builtin",
|
||||
"provider_id": "provider-1",
|
||||
"tool_name": "tool-a",
|
||||
"tool_parameters": {"secret": "encrypted"},
|
||||
"extra": True,
|
||||
}
|
||||
model_config = MagicMock()
|
||||
model_config.agent_mode_dict = {"tools": [tool, {"skip": True}]}
|
||||
|
||||
app = MagicMock()
|
||||
app.id = "app-1"
|
||||
app.mode = AppMode.AGENT_CHAT
|
||||
app.is_agent = False
|
||||
app.app_model_config = model_config
|
||||
|
||||
manager = MagicMock()
|
||||
manager.decrypt_tool_parameters.return_value = {"secret": "decrypted"}
|
||||
manager.mask_tool_parameters.return_value = {"secret": "***"}
|
||||
|
||||
with (
|
||||
patch("services.app_service.current_user", _make_current_user()),
|
||||
patch("services.app_service.ToolManager.get_agent_tool_runtime", return_value=MagicMock()),
|
||||
patch("services.app_service.ToolParameterConfigurationManager", return_value=manager),
|
||||
):
|
||||
# Act
|
||||
result = service.get_app(app)
|
||||
|
||||
# Assert
|
||||
assert result.app_model_config is model_config
|
||||
assert tool["tool_parameters"] == {"secret": "***"}
|
||||
assert json.loads(model_config.agent_mode)["tools"][0]["tool_parameters"] == {"secret": "***"}
|
||||
|
||||
def test_get_app_should_continue_when_tool_parameter_masking_fails(self, service: AppService) -> None:
|
||||
"""Test get_app logs and continues when masking fails."""
|
||||
# Arrange
|
||||
tool = {
|
||||
"provider_type": "builtin",
|
||||
"provider_id": "provider-1",
|
||||
"tool_name": "tool-a",
|
||||
"tool_parameters": {"secret": "encrypted"},
|
||||
"extra": True,
|
||||
}
|
||||
model_config = MagicMock()
|
||||
model_config.agent_mode_dict = {"tools": [tool]}
|
||||
|
||||
app = MagicMock()
|
||||
app.id = "app-1"
|
||||
app.mode = AppMode.AGENT_CHAT
|
||||
app.is_agent = False
|
||||
app.app_model_config = model_config
|
||||
|
||||
with (
|
||||
patch("services.app_service.current_user", _make_current_user()),
|
||||
patch("services.app_service.ToolManager.get_agent_tool_runtime", side_effect=RuntimeError("mask-failed")),
|
||||
patch("services.app_service.logger") as mock_logger,
|
||||
):
|
||||
# Act
|
||||
result = service.get_app(app)
|
||||
|
||||
# Assert
|
||||
assert result.app_model_config is model_config
|
||||
mock_logger.exception.assert_called_once()
|
||||
|
||||
def test_update_methods_should_mutate_app_and_commit(self, service: AppService) -> None:
|
||||
"""Test update methods set fields and commit changes."""
|
||||
# Arrange
|
||||
app = cast(
|
||||
App,
|
||||
SimpleNamespace(
|
||||
name="old",
|
||||
description="old",
|
||||
icon_type="emoji",
|
||||
icon="a",
|
||||
icon_background="#111",
|
||||
enable_site=True,
|
||||
enable_api=True,
|
||||
),
|
||||
)
|
||||
args = {
|
||||
"name": "new",
|
||||
"description": "new-desc",
|
||||
"icon_type": "image",
|
||||
"icon": "new-icon",
|
||||
"icon_background": "#222",
|
||||
"use_icon_as_answer_icon": True,
|
||||
"max_active_requests": 5,
|
||||
}
|
||||
user = SimpleNamespace(id="user-1")
|
||||
|
||||
with (
|
||||
patch("services.app_service.current_user", user),
|
||||
patch("services.app_service.db") as mock_db,
|
||||
patch("services.app_service.naive_utc_now", return_value="now"),
|
||||
):
|
||||
# Act
|
||||
updated = service.update_app(app, args)
|
||||
renamed = service.update_app_name(app, "rename")
|
||||
iconed = service.update_app_icon(app, "icon-2", "#333")
|
||||
site_same = service.update_app_site_status(app, app.enable_site)
|
||||
api_same = service.update_app_api_status(app, app.enable_api)
|
||||
site_changed = service.update_app_site_status(app, False)
|
||||
api_changed = service.update_app_api_status(app, False)
|
||||
|
||||
# Assert
|
||||
assert updated is app
|
||||
assert updated.icon_type == IconType.IMAGE
|
||||
assert renamed is app
|
||||
assert iconed is app
|
||||
assert site_same is app
|
||||
assert api_same is app
|
||||
assert site_changed is app
|
||||
assert api_changed is app
|
||||
assert mock_db.session.commit.call_count >= 5
|
||||
|
||||
def test_update_app_should_preserve_icon_type_when_not_provided(self, service: AppService) -> None:
|
||||
"""Test update_app keeps the existing icon_type when the payload omits it."""
|
||||
# Arrange
|
||||
app = cast(
|
||||
App,
|
||||
SimpleNamespace(
|
||||
name="old",
|
||||
description="old",
|
||||
icon_type=IconType.EMOJI,
|
||||
icon="a",
|
||||
icon_background="#111",
|
||||
use_icon_as_answer_icon=False,
|
||||
max_active_requests=1,
|
||||
),
|
||||
)
|
||||
args = {
|
||||
"name": "new",
|
||||
"description": "new-desc",
|
||||
"icon_type": None,
|
||||
"icon": "new-icon",
|
||||
"icon_background": "#222",
|
||||
"use_icon_as_answer_icon": True,
|
||||
"max_active_requests": 5,
|
||||
}
|
||||
user = SimpleNamespace(id="user-1")
|
||||
|
||||
with (
|
||||
patch("services.app_service.current_user", user),
|
||||
patch("services.app_service.db") as mock_db,
|
||||
patch("services.app_service.naive_utc_now", return_value="now"),
|
||||
):
|
||||
# Act
|
||||
updated = service.update_app(app, args)
|
||||
|
||||
# Assert
|
||||
assert updated is app
|
||||
assert updated.icon_type == IconType.EMOJI
|
||||
mock_db.session.commit.assert_called_once()
|
||||
|
||||
def test_update_app_should_reject_empty_icon_type(self, service: AppService) -> None:
|
||||
"""Test update_app rejects an explicit empty icon_type."""
|
||||
app = cast(
|
||||
App,
|
||||
SimpleNamespace(
|
||||
name="old",
|
||||
description="old",
|
||||
icon_type=IconType.EMOJI,
|
||||
icon="a",
|
||||
icon_background="#111",
|
||||
use_icon_as_answer_icon=False,
|
||||
max_active_requests=1,
|
||||
),
|
||||
)
|
||||
args = {
|
||||
"name": "new",
|
||||
"description": "new-desc",
|
||||
"icon_type": "",
|
||||
"icon": "new-icon",
|
||||
"icon_background": "#222",
|
||||
"use_icon_as_answer_icon": True,
|
||||
"max_active_requests": 5,
|
||||
}
|
||||
user = SimpleNamespace(id="user-1")
|
||||
|
||||
with (
|
||||
patch("services.app_service.current_user", user),
|
||||
patch("services.app_service.db") as mock_db,
|
||||
):
|
||||
with pytest.raises(ValueError):
|
||||
service.update_app(app, args)
|
||||
|
||||
mock_db.session.commit.assert_not_called()
|
||||
|
||||
|
||||
class TestAppServiceDeleteAndMeta:
|
||||
"""Test suite for delete and metadata methods."""
|
||||
|
||||
def test_delete_app_should_cleanup_and_enqueue_task(self, service: AppService) -> None:
|
||||
"""Test delete_app removes app, runs cleanup, and triggers async deletion task."""
|
||||
# Arrange
|
||||
app = cast(App, SimpleNamespace(id="app-1", tenant_id="tenant-1"))
|
||||
|
||||
with (
|
||||
patch("services.app_service.db") as mock_db,
|
||||
patch(
|
||||
"services.app_service.FeatureService.get_system_features",
|
||||
return_value=SimpleNamespace(webapp_auth=SimpleNamespace(enabled=True)),
|
||||
),
|
||||
patch("services.app_service.EnterpriseService") as mock_enterprise,
|
||||
patch(
|
||||
"services.app_service.dify_config",
|
||||
new=SimpleNamespace(BILLING_ENABLED=True, CONSOLE_API_URL="https://console.example"),
|
||||
),
|
||||
patch("services.app_service.BillingService") as mock_billing,
|
||||
patch("services.app_service.remove_app_and_related_data_task") as mock_task,
|
||||
):
|
||||
# Act
|
||||
service.delete_app(app)
|
||||
|
||||
# Assert
|
||||
mock_db.session.delete.assert_called_once_with(app)
|
||||
mock_db.session.commit.assert_called_once()
|
||||
mock_enterprise.WebAppAuth.cleanup_webapp.assert_called_once_with("app-1")
|
||||
mock_billing.clean_billing_info_cache.assert_called_once_with("tenant-1")
|
||||
mock_task.delay.assert_called_once_with(tenant_id="tenant-1", app_id="app-1")
|
||||
|
||||
def test_get_app_meta_should_handle_workflow_and_tool_provider_icons(self, service: AppService) -> None:
|
||||
"""Test get_app_meta extracts builtin and API tool icons from workflow graph."""
|
||||
# Arrange
|
||||
workflow = SimpleNamespace(
|
||||
graph_dict={
|
||||
"nodes": [
|
||||
{
|
||||
"data": {
|
||||
"type": "tool",
|
||||
"provider_type": "builtin",
|
||||
"provider_id": "builtin-provider",
|
||||
"tool_name": "tool_builtin",
|
||||
}
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"type": "tool",
|
||||
"provider_type": "api",
|
||||
"provider_id": "api-provider-id",
|
||||
"tool_name": "tool_api",
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
app = cast(
|
||||
App,
|
||||
SimpleNamespace(
|
||||
mode=AppMode.WORKFLOW.value,
|
||||
workflow=workflow,
|
||||
app_model_config=None,
|
||||
tenant_id="tenant-1",
|
||||
icon_type="emoji",
|
||||
icon_background="#fff",
|
||||
),
|
||||
)
|
||||
|
||||
provider = SimpleNamespace(icon=json.dumps({"background": "#000", "content": "A"}))
|
||||
|
||||
with (
|
||||
patch("services.app_service.dify_config", new=SimpleNamespace(CONSOLE_API_URL="https://console.example")),
|
||||
patch("services.app_service.db") as mock_db,
|
||||
):
|
||||
query = MagicMock()
|
||||
query.where.return_value = query
|
||||
query.first.return_value = provider
|
||||
mock_db.session.query.return_value = query
|
||||
|
||||
# Act
|
||||
meta = service.get_app_meta(app)
|
||||
|
||||
# Assert
|
||||
assert meta["tool_icons"]["tool_builtin"].endswith("/builtin-provider/icon")
|
||||
assert meta["tool_icons"]["tool_api"] == {"background": "#000", "content": "A"}
|
||||
|
||||
def test_get_app_meta_should_use_default_api_icon_on_lookup_error(self, service: AppService) -> None:
|
||||
"""Test get_app_meta falls back to default icon when API provider lookup fails."""
|
||||
# Arrange
|
||||
app_model_config = SimpleNamespace(
|
||||
agent_mode_dict={
|
||||
"tools": [{"provider_type": "api", "provider_id": "x", "tool_name": "t", "tool_parameters": {}}]
|
||||
}
|
||||
)
|
||||
app = cast(App, SimpleNamespace(mode=AppMode.CHAT.value, app_model_config=app_model_config, workflow=None))
|
||||
|
||||
with (
|
||||
patch("services.app_service.dify_config", new=SimpleNamespace(CONSOLE_API_URL="https://console.example")),
|
||||
patch("services.app_service.db") as mock_db,
|
||||
):
|
||||
query = MagicMock()
|
||||
query.where.return_value = query
|
||||
query.first.return_value = None
|
||||
mock_db.session.query.return_value = query
|
||||
|
||||
# Act
|
||||
meta = service.get_app_meta(app)
|
||||
|
||||
# Assert
|
||||
assert meta["tool_icons"]["t"] == {"background": "#252525", "content": "\ud83d\ude01"}
|
||||
|
||||
def test_get_app_meta_should_return_empty_when_required_data_missing(self, service: AppService) -> None:
|
||||
"""Test get_app_meta returns empty metadata when workflow/model config is absent."""
|
||||
# Arrange
|
||||
workflow_app = cast(App, SimpleNamespace(mode=AppMode.WORKFLOW.value, workflow=None))
|
||||
chat_app = cast(App, SimpleNamespace(mode=AppMode.CHAT.value, app_model_config=None))
|
||||
|
||||
# Act
|
||||
workflow_meta = service.get_app_meta(workflow_app)
|
||||
chat_meta = service.get_app_meta(chat_app)
|
||||
|
||||
# Assert
|
||||
assert workflow_meta == {"tool_icons": {}}
|
||||
assert chat_meta == {"tool_icons": {}}
|
||||
|
||||
|
||||
class TestAppServiceCodeLookup:
|
||||
"""Test suite for app code lookup methods."""
|
||||
|
||||
def test_get_app_code_by_id_should_raise_when_site_missing(self) -> None:
|
||||
"""Test get_app_code_by_id raises when site is missing."""
|
||||
# Arrange
|
||||
with patch("services.app_service.db") as mock_db:
|
||||
query = MagicMock()
|
||||
query.where.return_value = query
|
||||
query.first.return_value = None
|
||||
mock_db.session.query.return_value = query
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
AppService.get_app_code_by_id("app-1")
|
||||
|
||||
def test_get_app_code_by_id_should_return_code(self) -> None:
|
||||
"""Test get_app_code_by_id returns site code."""
|
||||
# Arrange
|
||||
site = SimpleNamespace(code="code-1")
|
||||
with patch("services.app_service.db") as mock_db:
|
||||
query = MagicMock()
|
||||
query.where.return_value = query
|
||||
query.first.return_value = site
|
||||
mock_db.session.query.return_value = query
|
||||
|
||||
# Act
|
||||
result = AppService.get_app_code_by_id("app-1")
|
||||
|
||||
# Assert
|
||||
assert result == "code-1"
|
||||
|
||||
def test_get_app_id_by_code_should_raise_when_site_missing(self) -> None:
|
||||
"""Test get_app_id_by_code raises when code does not exist."""
|
||||
# Arrange
|
||||
with patch("services.app_service.db") as mock_db:
|
||||
query = MagicMock()
|
||||
query.where.return_value = query
|
||||
query.first.return_value = None
|
||||
mock_db.session.query.return_value = query
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
AppService.get_app_id_by_code("missing")
|
||||
|
||||
def test_get_app_id_by_code_should_return_app_id(self) -> None:
|
||||
"""Test get_app_id_by_code returns linked app id."""
|
||||
# Arrange
|
||||
site = SimpleNamespace(app_id="app-1")
|
||||
with patch("services.app_service.db") as mock_db:
|
||||
query = MagicMock()
|
||||
query.where.return_value = query
|
||||
query.first.return_value = site
|
||||
mock_db.session.query.return_value = query
|
||||
|
||||
# Act
|
||||
result = AppService.get_app_id_by_code("code-1")
|
||||
|
||||
# Assert
|
||||
assert result == "app-1"
|
||||
@ -1,452 +0,0 @@
|
||||
from unittest.mock import Mock
|
||||
|
||||
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 ToolParameter, ToolProviderType
|
||||
from services.tools.tools_transform_service import ToolTransformService
|
||||
|
||||
|
||||
class TestToolTransformService:
|
||||
"""Test cases for ToolTransformService.convert_tool_entity_to_api_entity method"""
|
||||
|
||||
def test_convert_tool_with_parameter_override(self):
|
||||
"""Test that runtime parameters correctly override base parameters"""
|
||||
# Create mock base parameters
|
||||
base_param1 = Mock(spec=ToolParameter)
|
||||
base_param1.name = "param1"
|
||||
base_param1.form = ToolParameter.ToolParameterForm.FORM
|
||||
base_param1.type = "string"
|
||||
base_param1.label = "Base Param 1"
|
||||
|
||||
base_param2 = Mock(spec=ToolParameter)
|
||||
base_param2.name = "param2"
|
||||
base_param2.form = ToolParameter.ToolParameterForm.FORM
|
||||
base_param2.type = "string"
|
||||
base_param2.label = "Base Param 2"
|
||||
|
||||
# Create mock runtime parameters that override base parameters
|
||||
runtime_param1 = Mock(spec=ToolParameter)
|
||||
runtime_param1.name = "param1"
|
||||
runtime_param1.form = ToolParameter.ToolParameterForm.FORM
|
||||
runtime_param1.type = "string"
|
||||
runtime_param1.label = "Runtime Param 1" # Different label to verify override
|
||||
|
||||
# Create mock tool
|
||||
mock_tool = Mock(spec=Tool)
|
||||
mock_tool.entity = Mock()
|
||||
mock_tool.entity.parameters = [base_param1, base_param2]
|
||||
mock_tool.entity.identity = Mock()
|
||||
mock_tool.entity.identity.author = "test_author"
|
||||
mock_tool.entity.identity.name = "test_tool"
|
||||
mock_tool.entity.identity.label = I18nObject(en_US="Test Tool")
|
||||
mock_tool.entity.description = Mock()
|
||||
mock_tool.entity.description.human = I18nObject(en_US="Test description")
|
||||
mock_tool.entity.output_schema = {}
|
||||
mock_tool.get_runtime_parameters.return_value = [runtime_param1]
|
||||
|
||||
# Mock fork_tool_runtime to return the same tool
|
||||
mock_tool.fork_tool_runtime.return_value = mock_tool
|
||||
|
||||
# Call the method
|
||||
result = ToolTransformService.convert_tool_entity_to_api_entity(mock_tool, "test_tenant", None)
|
||||
|
||||
# Verify the result
|
||||
assert isinstance(result, ToolApiEntity)
|
||||
assert result.author == "test_author"
|
||||
assert result.name == "test_tool"
|
||||
assert result.parameters is not None
|
||||
assert len(result.parameters) == 2
|
||||
|
||||
# Find the overridden parameter
|
||||
overridden_param = next((p for p in result.parameters if p.name == "param1"), None)
|
||||
assert overridden_param is not None
|
||||
assert overridden_param.label == "Runtime Param 1" # Should be runtime version
|
||||
|
||||
# Find the non-overridden parameter
|
||||
original_param = next((p for p in result.parameters if p.name == "param2"), None)
|
||||
assert original_param is not None
|
||||
assert original_param.label == "Base Param 2" # Should be base version
|
||||
|
||||
def test_convert_tool_with_additional_runtime_parameters(self):
|
||||
"""Test that additional runtime parameters are added to the final list"""
|
||||
# Create mock base parameters
|
||||
base_param1 = Mock(spec=ToolParameter)
|
||||
base_param1.name = "param1"
|
||||
base_param1.form = ToolParameter.ToolParameterForm.FORM
|
||||
base_param1.type = "string"
|
||||
base_param1.label = "Base Param 1"
|
||||
|
||||
# Create mock runtime parameters - one that overrides and one that's new
|
||||
runtime_param1 = Mock(spec=ToolParameter)
|
||||
runtime_param1.name = "param1"
|
||||
runtime_param1.form = ToolParameter.ToolParameterForm.FORM
|
||||
runtime_param1.type = "string"
|
||||
runtime_param1.label = "Runtime Param 1"
|
||||
|
||||
runtime_param2 = Mock(spec=ToolParameter)
|
||||
runtime_param2.name = "runtime_only"
|
||||
runtime_param2.form = ToolParameter.ToolParameterForm.FORM
|
||||
runtime_param2.type = "string"
|
||||
runtime_param2.label = "Runtime Only Param"
|
||||
|
||||
# Create mock tool
|
||||
mock_tool = Mock(spec=Tool)
|
||||
mock_tool.entity = Mock()
|
||||
mock_tool.entity.parameters = [base_param1]
|
||||
mock_tool.entity.identity = Mock()
|
||||
mock_tool.entity.identity.author = "test_author"
|
||||
mock_tool.entity.identity.name = "test_tool"
|
||||
mock_tool.entity.identity.label = I18nObject(en_US="Test Tool")
|
||||
mock_tool.entity.description = Mock()
|
||||
mock_tool.entity.description.human = I18nObject(en_US="Test description")
|
||||
mock_tool.entity.output_schema = {}
|
||||
mock_tool.get_runtime_parameters.return_value = [runtime_param1, runtime_param2]
|
||||
|
||||
# Mock fork_tool_runtime to return the same tool
|
||||
mock_tool.fork_tool_runtime.return_value = mock_tool
|
||||
|
||||
# Call the method
|
||||
result = ToolTransformService.convert_tool_entity_to_api_entity(mock_tool, "test_tenant", None)
|
||||
|
||||
# Verify the result
|
||||
assert isinstance(result, ToolApiEntity)
|
||||
assert result.parameters is not None
|
||||
assert len(result.parameters) == 2
|
||||
|
||||
# Check that both parameters are present
|
||||
param_names = [p.name for p in result.parameters]
|
||||
assert "param1" in param_names
|
||||
assert "runtime_only" in param_names
|
||||
|
||||
# Verify the overridden parameter has runtime version
|
||||
overridden_param = next((p for p in result.parameters if p.name == "param1"), None)
|
||||
assert overridden_param is not None
|
||||
assert overridden_param.label == "Runtime Param 1"
|
||||
|
||||
# Verify the new runtime parameter is included
|
||||
new_param = next((p for p in result.parameters if p.name == "runtime_only"), None)
|
||||
assert new_param is not None
|
||||
assert new_param.label == "Runtime Only Param"
|
||||
|
||||
def test_convert_tool_with_non_form_runtime_parameters(self):
|
||||
"""Test that non-FORM runtime parameters are not added as new parameters"""
|
||||
# Create mock base parameters
|
||||
base_param1 = Mock(spec=ToolParameter)
|
||||
base_param1.name = "param1"
|
||||
base_param1.form = ToolParameter.ToolParameterForm.FORM
|
||||
base_param1.type = "string"
|
||||
base_param1.label = "Base Param 1"
|
||||
|
||||
# Create mock runtime parameters with different forms
|
||||
runtime_param1 = Mock(spec=ToolParameter)
|
||||
runtime_param1.name = "param1"
|
||||
runtime_param1.form = ToolParameter.ToolParameterForm.FORM
|
||||
runtime_param1.type = "string"
|
||||
runtime_param1.label = "Runtime Param 1"
|
||||
|
||||
runtime_param2 = Mock(spec=ToolParameter)
|
||||
runtime_param2.name = "llm_param"
|
||||
runtime_param2.form = ToolParameter.ToolParameterForm.LLM
|
||||
runtime_param2.type = "string"
|
||||
runtime_param2.label = "LLM Param"
|
||||
|
||||
# Create mock tool
|
||||
mock_tool = Mock(spec=Tool)
|
||||
mock_tool.entity = Mock()
|
||||
mock_tool.entity.parameters = [base_param1]
|
||||
mock_tool.entity.identity = Mock()
|
||||
mock_tool.entity.identity.author = "test_author"
|
||||
mock_tool.entity.identity.name = "test_tool"
|
||||
mock_tool.entity.identity.label = I18nObject(en_US="Test Tool")
|
||||
mock_tool.entity.description = Mock()
|
||||
mock_tool.entity.description.human = I18nObject(en_US="Test description")
|
||||
mock_tool.entity.output_schema = {}
|
||||
mock_tool.get_runtime_parameters.return_value = [runtime_param1, runtime_param2]
|
||||
|
||||
# Mock fork_tool_runtime to return the same tool
|
||||
mock_tool.fork_tool_runtime.return_value = mock_tool
|
||||
|
||||
# Call the method
|
||||
result = ToolTransformService.convert_tool_entity_to_api_entity(mock_tool, "test_tenant", None)
|
||||
|
||||
# Verify the result
|
||||
assert isinstance(result, ToolApiEntity)
|
||||
assert result.parameters is not None
|
||||
assert len(result.parameters) == 1 # Only the FORM parameter should be present
|
||||
|
||||
# Check that only the FORM parameter is present
|
||||
param_names = [p.name for p in result.parameters]
|
||||
assert "param1" in param_names
|
||||
assert "llm_param" not in param_names
|
||||
|
||||
def test_convert_tool_with_empty_parameters(self):
|
||||
"""Test conversion with empty base and runtime parameters"""
|
||||
# Create mock tool with no parameters
|
||||
mock_tool = Mock(spec=Tool)
|
||||
mock_tool.entity = Mock()
|
||||
mock_tool.entity.parameters = []
|
||||
mock_tool.entity.identity = Mock()
|
||||
mock_tool.entity.identity.author = "test_author"
|
||||
mock_tool.entity.identity.name = "test_tool"
|
||||
mock_tool.entity.identity.label = I18nObject(en_US="Test Tool")
|
||||
mock_tool.entity.description = Mock()
|
||||
mock_tool.entity.description.human = I18nObject(en_US="Test description")
|
||||
mock_tool.entity.output_schema = {}
|
||||
mock_tool.get_runtime_parameters.return_value = []
|
||||
|
||||
# Mock fork_tool_runtime to return the same tool
|
||||
mock_tool.fork_tool_runtime.return_value = mock_tool
|
||||
|
||||
# Call the method
|
||||
result = ToolTransformService.convert_tool_entity_to_api_entity(mock_tool, "test_tenant", None)
|
||||
|
||||
# Verify the result
|
||||
assert isinstance(result, ToolApiEntity)
|
||||
assert result.parameters is not None
|
||||
assert len(result.parameters) == 0
|
||||
|
||||
def test_convert_tool_with_none_parameters(self):
|
||||
"""Test conversion when base parameters is None"""
|
||||
# Create mock tool with None parameters
|
||||
mock_tool = Mock(spec=Tool)
|
||||
mock_tool.entity = Mock()
|
||||
mock_tool.entity.parameters = None
|
||||
mock_tool.entity.identity = Mock()
|
||||
mock_tool.entity.identity.author = "test_author"
|
||||
mock_tool.entity.identity.name = "test_tool"
|
||||
mock_tool.entity.identity.label = I18nObject(en_US="Test Tool")
|
||||
mock_tool.entity.description = Mock()
|
||||
mock_tool.entity.description.human = I18nObject(en_US="Test description")
|
||||
mock_tool.entity.output_schema = {}
|
||||
mock_tool.get_runtime_parameters.return_value = []
|
||||
|
||||
# Mock fork_tool_runtime to return the same tool
|
||||
mock_tool.fork_tool_runtime.return_value = mock_tool
|
||||
|
||||
# Call the method
|
||||
result = ToolTransformService.convert_tool_entity_to_api_entity(mock_tool, "test_tenant", None)
|
||||
|
||||
# Verify the result
|
||||
assert isinstance(result, ToolApiEntity)
|
||||
assert result.parameters is not None
|
||||
assert len(result.parameters) == 0
|
||||
|
||||
def test_convert_tool_parameter_order_preserved(self):
|
||||
"""Test that parameter order is preserved correctly"""
|
||||
# Create mock base parameters in specific order
|
||||
base_param1 = Mock(spec=ToolParameter)
|
||||
base_param1.name = "param1"
|
||||
base_param1.form = ToolParameter.ToolParameterForm.FORM
|
||||
base_param1.type = "string"
|
||||
base_param1.label = "Base Param 1"
|
||||
|
||||
base_param2 = Mock(spec=ToolParameter)
|
||||
base_param2.name = "param2"
|
||||
base_param2.form = ToolParameter.ToolParameterForm.FORM
|
||||
base_param2.type = "string"
|
||||
base_param2.label = "Base Param 2"
|
||||
|
||||
base_param3 = Mock(spec=ToolParameter)
|
||||
base_param3.name = "param3"
|
||||
base_param3.form = ToolParameter.ToolParameterForm.FORM
|
||||
base_param3.type = "string"
|
||||
base_param3.label = "Base Param 3"
|
||||
|
||||
# Create runtime parameter that overrides middle parameter
|
||||
runtime_param2 = Mock(spec=ToolParameter)
|
||||
runtime_param2.name = "param2"
|
||||
runtime_param2.form = ToolParameter.ToolParameterForm.FORM
|
||||
runtime_param2.type = "string"
|
||||
runtime_param2.label = "Runtime Param 2"
|
||||
|
||||
# Create new runtime parameter
|
||||
runtime_param4 = Mock(spec=ToolParameter)
|
||||
runtime_param4.name = "param4"
|
||||
runtime_param4.form = ToolParameter.ToolParameterForm.FORM
|
||||
runtime_param4.type = "string"
|
||||
runtime_param4.label = "Runtime Param 4"
|
||||
|
||||
# Create mock tool
|
||||
mock_tool = Mock(spec=Tool)
|
||||
mock_tool.entity = Mock()
|
||||
mock_tool.entity.parameters = [base_param1, base_param2, base_param3]
|
||||
mock_tool.entity.identity = Mock()
|
||||
mock_tool.entity.identity.author = "test_author"
|
||||
mock_tool.entity.identity.name = "test_tool"
|
||||
mock_tool.entity.identity.label = I18nObject(en_US="Test Tool")
|
||||
mock_tool.entity.description = Mock()
|
||||
mock_tool.entity.description.human = I18nObject(en_US="Test description")
|
||||
mock_tool.entity.output_schema = {}
|
||||
mock_tool.get_runtime_parameters.return_value = [runtime_param2, runtime_param4]
|
||||
|
||||
# Mock fork_tool_runtime to return the same tool
|
||||
mock_tool.fork_tool_runtime.return_value = mock_tool
|
||||
|
||||
# Call the method
|
||||
result = ToolTransformService.convert_tool_entity_to_api_entity(mock_tool, "test_tenant", None)
|
||||
|
||||
# Verify the result
|
||||
assert isinstance(result, ToolApiEntity)
|
||||
assert result.parameters is not None
|
||||
assert len(result.parameters) == 4
|
||||
|
||||
# Check that order is maintained: base parameters first, then new runtime parameters
|
||||
param_names = [p.name for p in result.parameters]
|
||||
assert param_names == ["param1", "param2", "param3", "param4"]
|
||||
|
||||
# Verify that param2 was overridden with runtime version
|
||||
param2 = result.parameters[1]
|
||||
assert param2.name == "param2"
|
||||
assert param2.label == "Runtime Param 2"
|
||||
|
||||
|
||||
class TestWorkflowProviderToUserProvider:
|
||||
"""Test cases for ToolTransformService.workflow_provider_to_user_provider method"""
|
||||
|
||||
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
|
||||
|
||||
# Create mock workflow tool provider controller
|
||||
workflow_app_id = "app_123"
|
||||
provider_id = "provider_123"
|
||||
mock_controller = Mock(spec=WorkflowToolProviderController)
|
||||
mock_controller.provider_id = provider_id
|
||||
mock_controller.entity = Mock()
|
||||
mock_controller.entity.identity = Mock()
|
||||
mock_controller.entity.identity.author = "test_author"
|
||||
mock_controller.entity.identity.name = "test_workflow_tool"
|
||||
mock_controller.entity.identity.description = I18nObject(en_US="Test description")
|
||||
mock_controller.entity.identity.icon = {"type": "emoji", "content": "🔧"}
|
||||
mock_controller.entity.identity.icon_dark = None
|
||||
mock_controller.entity.identity.label = I18nObject(en_US="Test Workflow Tool")
|
||||
|
||||
# Call the method
|
||||
result = ToolTransformService.workflow_provider_to_user_provider(
|
||||
provider_controller=mock_controller,
|
||||
labels=["label1", "label2"],
|
||||
workflow_app_id=workflow_app_id,
|
||||
)
|
||||
|
||||
# Verify the result
|
||||
assert isinstance(result, ToolProviderApiEntity)
|
||||
assert result.id == provider_id
|
||||
assert result.author == "test_author"
|
||||
assert result.name == "test_workflow_tool"
|
||||
assert result.type == ToolProviderType.WORKFLOW
|
||||
assert result.workflow_app_id == workflow_app_id
|
||||
assert result.labels == ["label1", "label2"]
|
||||
assert result.is_team_authorization is True
|
||||
assert result.plugin_id is None
|
||||
assert result.plugin_unique_identifier is None
|
||||
assert result.tools == []
|
||||
|
||||
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
|
||||
|
||||
# Create mock workflow tool provider controller
|
||||
provider_id = "provider_123"
|
||||
mock_controller = Mock(spec=WorkflowToolProviderController)
|
||||
mock_controller.provider_id = provider_id
|
||||
mock_controller.entity = Mock()
|
||||
mock_controller.entity.identity = Mock()
|
||||
mock_controller.entity.identity.author = "test_author"
|
||||
mock_controller.entity.identity.name = "test_workflow_tool"
|
||||
mock_controller.entity.identity.description = I18nObject(en_US="Test description")
|
||||
mock_controller.entity.identity.icon = {"type": "emoji", "content": "🔧"}
|
||||
mock_controller.entity.identity.icon_dark = None
|
||||
mock_controller.entity.identity.label = I18nObject(en_US="Test Workflow Tool")
|
||||
|
||||
# Call the method without workflow_app_id
|
||||
result = ToolTransformService.workflow_provider_to_user_provider(
|
||||
provider_controller=mock_controller,
|
||||
labels=["label1"],
|
||||
)
|
||||
|
||||
# Verify the result
|
||||
assert isinstance(result, ToolProviderApiEntity)
|
||||
assert result.id == provider_id
|
||||
assert result.workflow_app_id is None
|
||||
assert result.labels == ["label1"]
|
||||
|
||||
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
|
||||
|
||||
# Create mock workflow tool provider controller
|
||||
provider_id = "provider_123"
|
||||
mock_controller = Mock(spec=WorkflowToolProviderController)
|
||||
mock_controller.provider_id = provider_id
|
||||
mock_controller.entity = Mock()
|
||||
mock_controller.entity.identity = Mock()
|
||||
mock_controller.entity.identity.author = "test_author"
|
||||
mock_controller.entity.identity.name = "test_workflow_tool"
|
||||
mock_controller.entity.identity.description = I18nObject(en_US="Test description")
|
||||
mock_controller.entity.identity.icon = {"type": "emoji", "content": "🔧"}
|
||||
mock_controller.entity.identity.icon_dark = None
|
||||
mock_controller.entity.identity.label = I18nObject(en_US="Test Workflow Tool")
|
||||
|
||||
# Call the method with explicit None values
|
||||
result = ToolTransformService.workflow_provider_to_user_provider(
|
||||
provider_controller=mock_controller,
|
||||
labels=None,
|
||||
workflow_app_id=None,
|
||||
)
|
||||
|
||||
# Verify the result
|
||||
assert isinstance(result, ToolProviderApiEntity)
|
||||
assert result.id == provider_id
|
||||
assert result.workflow_app_id is None
|
||||
assert result.labels == []
|
||||
|
||||
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
|
||||
|
||||
# Create mock workflow tool provider controller with various fields
|
||||
workflow_app_id = "app_456"
|
||||
provider_id = "provider_456"
|
||||
mock_controller = Mock(spec=WorkflowToolProviderController)
|
||||
mock_controller.provider_id = provider_id
|
||||
mock_controller.entity = Mock()
|
||||
mock_controller.entity.identity = Mock()
|
||||
mock_controller.entity.identity.author = "another_author"
|
||||
mock_controller.entity.identity.name = "another_workflow_tool"
|
||||
mock_controller.entity.identity.description = I18nObject(
|
||||
en_US="Another description", zh_Hans="Another description"
|
||||
)
|
||||
mock_controller.entity.identity.icon = {"type": "emoji", "content": "⚙️"}
|
||||
mock_controller.entity.identity.icon_dark = {"type": "emoji", "content": "🔧"}
|
||||
mock_controller.entity.identity.label = I18nObject(
|
||||
en_US="Another Workflow Tool", zh_Hans="Another Workflow Tool"
|
||||
)
|
||||
|
||||
# Call the method
|
||||
result = ToolTransformService.workflow_provider_to_user_provider(
|
||||
provider_controller=mock_controller,
|
||||
labels=["automation", "workflow"],
|
||||
workflow_app_id=workflow_app_id,
|
||||
)
|
||||
|
||||
# Verify all fields are preserved correctly
|
||||
assert isinstance(result, ToolProviderApiEntity)
|
||||
assert result.id == provider_id
|
||||
assert result.author == "another_author"
|
||||
assert result.name == "another_workflow_tool"
|
||||
assert result.description.en_US == "Another description"
|
||||
assert result.description.zh_Hans == "Another description"
|
||||
assert result.icon == {"type": "emoji", "content": "⚙️"}
|
||||
assert result.icon_dark == {"type": "emoji", "content": "🔧"}
|
||||
assert result.label.en_US == "Another Workflow Tool"
|
||||
assert result.label.zh_Hans == "Another Workflow Tool"
|
||||
assert result.type == ToolProviderType.WORKFLOW
|
||||
assert result.workflow_app_id == workflow_app_id
|
||||
assert result.labels == ["automation", "workflow"]
|
||||
assert result.masked_credentials == {}
|
||||
assert result.is_team_authorization is True
|
||||
assert result.allow_delete is True
|
||||
assert result.plugin_id is None
|
||||
assert result.plugin_unique_identifier is None
|
||||
assert result.tools == []
|
||||
Loading…
Reference in New Issue
Block a user