mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
test: use real tool runtimes responses and plugin entities (#43142)
This commit is contained in:
1 parent
38f42f357e
commit
948fd166dd
5 files changed
+102
-114
No files matched your search
@@ -29,13 +29,24 @@ from core.tools.entities.tool_entities import ToolIdentity, ToolProviderIdentity
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_identity(mocker: MockerFixture):
|
||||
return mocker.MagicMock(spec=AgentStrategyIdentity)
|
||||
def strategy_identity() -> AgentStrategyIdentity:
|
||||
return AgentStrategyIdentity(
|
||||
author="test",
|
||||
name="test-strategy",
|
||||
label=I18nObject(en_US="Test strategy"),
|
||||
provider="test-provider",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_provider_identity(mocker: MockerFixture):
|
||||
return mocker.MagicMock(spec=AgentStrategyProviderIdentity)
|
||||
def provider_identity() -> AgentStrategyProviderIdentity:
|
||||
return AgentStrategyProviderIdentity(
|
||||
author="test",
|
||||
name="test-provider",
|
||||
description=I18nObject(en_US="Test provider description"),
|
||||
icon="icon.svg",
|
||||
label=I18nObject(en_US="Test provider"),
|
||||
)
|
||||
|
||||
|
||||
# =========================================================
|
||||
@@ -176,22 +187,22 @@ class TestAgentStrategyParameter:
|
||||
|
||||
|
||||
class TestAgentStrategyProviderEntity:
|
||||
def test_creation_with_plugin_id(self, mock_provider_identity) -> None:
|
||||
def test_creation_with_plugin_id(self, provider_identity) -> None:
|
||||
entity = AgentStrategyProviderEntity(
|
||||
identity=mock_provider_identity,
|
||||
identity=provider_identity,
|
||||
plugin_id="plugin-123",
|
||||
)
|
||||
assert entity.plugin_id == "plugin-123"
|
||||
|
||||
def test_creation_with_empty_plugin_id(self, mock_provider_identity) -> None:
|
||||
def test_creation_with_empty_plugin_id(self, provider_identity) -> None:
|
||||
entity = AgentStrategyProviderEntity(
|
||||
identity=mock_provider_identity,
|
||||
identity=provider_identity,
|
||||
plugin_id="",
|
||||
)
|
||||
assert entity.plugin_id == ""
|
||||
|
||||
def test_creation_without_plugin_id(self, mock_provider_identity) -> None:
|
||||
entity = AgentStrategyProviderEntity(identity=mock_provider_identity)
|
||||
def test_creation_without_plugin_id(self, provider_identity) -> None:
|
||||
entity = AgentStrategyProviderEntity(identity=provider_identity)
|
||||
assert entity.plugin_id is None
|
||||
|
||||
def test_invalid_identity_raises(self) -> None:
|
||||
@@ -205,22 +216,22 @@ class TestAgentStrategyProviderEntity:
|
||||
|
||||
|
||||
class TestAgentStrategyEntity:
|
||||
def test_parameters_default_empty(self, mock_identity) -> None:
|
||||
def test_parameters_default_empty(self, strategy_identity) -> None:
|
||||
entity = AgentStrategyEntity(
|
||||
identity=mock_identity,
|
||||
identity=strategy_identity,
|
||||
description=I18nObject(en_US="test"),
|
||||
)
|
||||
assert entity.parameters == []
|
||||
|
||||
def test_parameters_none_converted_to_empty(self, mock_identity) -> None:
|
||||
def test_parameters_none_converted_to_empty(self, strategy_identity) -> None:
|
||||
entity = AgentStrategyEntity(
|
||||
identity=mock_identity,
|
||||
identity=strategy_identity,
|
||||
description=I18nObject(en_US="test"),
|
||||
parameters=None,
|
||||
)
|
||||
assert entity.parameters == []
|
||||
|
||||
def test_parameters_preserved(self, mock_identity) -> None:
|
||||
def test_parameters_preserved(self, strategy_identity) -> None:
|
||||
param = AgentStrategyParameter.model_construct(
|
||||
type=AgentStrategyParameter.AgentStrategyParameterType.STRING,
|
||||
name="test",
|
||||
@@ -228,16 +239,16 @@ class TestAgentStrategyEntity:
|
||||
)
|
||||
|
||||
entity = AgentStrategyEntity(
|
||||
identity=mock_identity,
|
||||
identity=strategy_identity,
|
||||
description=I18nObject(en_US="test"),
|
||||
parameters=[param],
|
||||
)
|
||||
assert entity.parameters == [param]
|
||||
|
||||
def test_invalid_parameters_type_raises(self, mock_identity) -> None:
|
||||
def test_invalid_parameters_type_raises(self, strategy_identity) -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
AgentStrategyEntity(
|
||||
identity=mock_identity,
|
||||
identity=strategy_identity,
|
||||
description=I18nObject(en_US="test"),
|
||||
parameters="invalid",
|
||||
)
|
||||
@@ -250,25 +261,25 @@ class TestAgentStrategyEntity:
|
||||
[AgentFeature.HISTORY_MESSAGES],
|
||||
],
|
||||
)
|
||||
def test_features_valid(self, mock_identity, features) -> None:
|
||||
def test_features_valid(self, strategy_identity, features) -> None:
|
||||
entity = AgentStrategyEntity(
|
||||
identity=mock_identity,
|
||||
identity=strategy_identity,
|
||||
description=I18nObject(en_US="test"),
|
||||
features=features,
|
||||
)
|
||||
assert entity.features == features
|
||||
|
||||
def test_invalid_features_type_raises(self, mock_identity) -> None:
|
||||
def test_invalid_features_type_raises(self, strategy_identity) -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
AgentStrategyEntity(
|
||||
identity=mock_identity,
|
||||
identity=strategy_identity,
|
||||
description=I18nObject(en_US="test"),
|
||||
features="invalid",
|
||||
)
|
||||
|
||||
def test_output_schema_and_meta_version(self, mock_identity) -> None:
|
||||
def test_output_schema_and_meta_version(self, strategy_identity) -> None:
|
||||
entity = AgentStrategyEntity(
|
||||
identity=mock_identity,
|
||||
identity=strategy_identity,
|
||||
description=I18nObject(en_US="test"),
|
||||
output_schema={"type": "object"},
|
||||
meta_version="v1",
|
||||
@@ -276,9 +287,9 @@ class TestAgentStrategyEntity:
|
||||
assert entity.output_schema == {"type": "object"}
|
||||
assert entity.meta_version == "v1"
|
||||
|
||||
def test_missing_required_fields_raise(self, mock_identity) -> None:
|
||||
def test_missing_required_fields_raise(self, strategy_identity) -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
AgentStrategyEntity(identity=mock_identity)
|
||||
AgentStrategyEntity(identity=strategy_identity)
|
||||
|
||||
|
||||
# =========================================================
|
||||
@@ -287,27 +298,27 @@ class TestAgentStrategyEntity:
|
||||
|
||||
|
||||
class TestAgentProviderEntityWithPlugin:
|
||||
def test_default_strategies_empty(self, mock_provider_identity) -> None:
|
||||
entity = AgentProviderEntityWithPlugin(identity=mock_provider_identity)
|
||||
def test_default_strategies_empty(self, provider_identity) -> None:
|
||||
entity = AgentProviderEntityWithPlugin(identity=provider_identity)
|
||||
assert entity.strategies == []
|
||||
|
||||
def test_strategies_assignment(self, mock_provider_identity, mock_identity) -> None:
|
||||
def test_strategies_assignment(self, provider_identity, strategy_identity) -> None:
|
||||
strategy = AgentStrategyEntity.model_construct(
|
||||
identity=mock_identity,
|
||||
identity=strategy_identity,
|
||||
description=I18nObject(en_US="test"),
|
||||
parameters=[],
|
||||
)
|
||||
|
||||
entity = AgentProviderEntityWithPlugin(
|
||||
identity=mock_provider_identity,
|
||||
identity=provider_identity,
|
||||
strategies=[strategy],
|
||||
)
|
||||
assert entity.strategies == [strategy]
|
||||
|
||||
def test_invalid_strategies_type_raises(self, mock_provider_identity) -> None:
|
||||
def test_invalid_strategies_type_raises(self, provider_identity) -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
AgentProviderEntityWithPlugin(
|
||||
identity=mock_provider_identity,
|
||||
identity=provider_identity,
|
||||
strategies="invalid",
|
||||
)
|
||||
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
"""Unit tests for MCP entities module."""
|
||||
|
||||
from unittest.mock import Mock
|
||||
from queue import Queue
|
||||
|
||||
from core.mcp.entities import (
|
||||
SUPPORTED_PROTOCOL_VERSIONS,
|
||||
RequestContext,
|
||||
)
|
||||
from core.mcp.session.base_session import BaseSession
|
||||
from core.mcp.types import LATEST_PROTOCOL_VERSION, RequestParams
|
||||
from core.mcp.types import LATEST_PROTOCOL_VERSION, RequestParams, ServerNotification, ServerRequest
|
||||
|
||||
|
||||
def _session() -> BaseSession:
|
||||
return BaseSession(
|
||||
read_stream=Queue(),
|
||||
write_stream=Queue(),
|
||||
receive_request_type=ServerRequest,
|
||||
receive_notification_type=ServerNotification,
|
||||
)
|
||||
|
||||
|
||||
class TestProtocolVersions:
|
||||
@@ -31,46 +40,46 @@ class TestRequestContext:
|
||||
|
||||
def test_request_context_creation(self):
|
||||
"""Test creating a RequestContext instance."""
|
||||
mock_session = Mock(spec=BaseSession)
|
||||
mock_lifespan = {"key": "value"}
|
||||
mock_meta = RequestParams.Meta(progressToken="test-token")
|
||||
session = _session()
|
||||
lifespan = {"key": "value"}
|
||||
meta = RequestParams.Meta(progressToken="test-token")
|
||||
|
||||
context = RequestContext(
|
||||
request_id="test-request-123",
|
||||
meta=mock_meta,
|
||||
session=mock_session,
|
||||
lifespan_context=mock_lifespan,
|
||||
meta=meta,
|
||||
session=session,
|
||||
lifespan_context=lifespan,
|
||||
)
|
||||
|
||||
assert context.request_id == "test-request-123"
|
||||
assert context.meta == mock_meta
|
||||
assert context.session == mock_session
|
||||
assert context.lifespan_context == mock_lifespan
|
||||
assert context.meta == meta
|
||||
assert context.session == session
|
||||
assert context.lifespan_context == lifespan
|
||||
|
||||
def test_request_context_with_none_meta(self):
|
||||
"""Test creating RequestContext with None meta."""
|
||||
mock_session = Mock(spec=BaseSession)
|
||||
session = _session()
|
||||
|
||||
context = RequestContext(
|
||||
request_id=42, # Can be int or string
|
||||
meta=None,
|
||||
session=mock_session,
|
||||
session=session,
|
||||
lifespan_context=None,
|
||||
)
|
||||
|
||||
assert context.request_id == 42
|
||||
assert context.meta is None
|
||||
assert context.session == mock_session
|
||||
assert context.session == session
|
||||
assert context.lifespan_context is None
|
||||
|
||||
def test_request_context_attributes(self):
|
||||
"""Test RequestContext attributes are accessible."""
|
||||
mock_session = Mock(spec=BaseSession)
|
||||
session = _session()
|
||||
|
||||
context = RequestContext(
|
||||
request_id="test-123",
|
||||
meta=None,
|
||||
session=mock_session,
|
||||
session=session,
|
||||
lifespan_context=None,
|
||||
)
|
||||
|
||||
@@ -83,19 +92,19 @@ class TestRequestContext:
|
||||
# Verify values
|
||||
assert context.request_id == "test-123"
|
||||
assert context.meta is None
|
||||
assert context.session == mock_session
|
||||
assert context.session == session
|
||||
assert context.lifespan_context is None
|
||||
|
||||
def test_request_context_generic_typing(self):
|
||||
"""Test RequestContext with different generic types."""
|
||||
# Create a mock session with specific type
|
||||
mock_session = Mock(spec=BaseSession)
|
||||
# Create a real session without starting its receive loop.
|
||||
session = _session()
|
||||
|
||||
# Create context with string lifespan context
|
||||
context_str = RequestContext[BaseSession, str](
|
||||
request_id="test-1",
|
||||
meta=None,
|
||||
session=mock_session,
|
||||
session=session,
|
||||
lifespan_context="string-context",
|
||||
)
|
||||
assert isinstance(context_str.lifespan_context, str)
|
||||
@@ -104,7 +113,7 @@ class TestRequestContext:
|
||||
context_dict = RequestContext[BaseSession, dict](
|
||||
request_id="test-2",
|
||||
meta=None,
|
||||
session=mock_session,
|
||||
session=session,
|
||||
lifespan_context={"key": "value"},
|
||||
)
|
||||
assert isinstance(context_dict.lifespan_context, dict)
|
||||
@@ -118,7 +127,7 @@ class TestRequestContext:
|
||||
context_custom = RequestContext[BaseSession, CustomLifespan](
|
||||
request_id="test-3",
|
||||
meta=None,
|
||||
session=mock_session,
|
||||
session=session,
|
||||
lifespan_context=custom_lifespan,
|
||||
)
|
||||
assert isinstance(context_custom.lifespan_context, CustomLifespan)
|
||||
@@ -126,13 +135,13 @@ class TestRequestContext:
|
||||
|
||||
def test_request_context_with_progress_meta(self):
|
||||
"""Test RequestContext with progress metadata."""
|
||||
mock_session = Mock(spec=BaseSession)
|
||||
session = _session()
|
||||
progress_meta = RequestParams.Meta(progressToken="progress-123")
|
||||
|
||||
context = RequestContext(
|
||||
request_id="req-456",
|
||||
meta=progress_meta,
|
||||
session=mock_session,
|
||||
session=session,
|
||||
lifespan_context=None,
|
||||
)
|
||||
|
||||
@@ -141,27 +150,27 @@ class TestRequestContext:
|
||||
|
||||
def test_request_context_equality(self):
|
||||
"""Test RequestContext equality comparison."""
|
||||
mock_session1 = Mock(spec=BaseSession)
|
||||
mock_session2 = Mock(spec=BaseSession)
|
||||
session1 = _session()
|
||||
session2 = _session()
|
||||
|
||||
context1 = RequestContext(
|
||||
request_id="test-123",
|
||||
meta=None,
|
||||
session=mock_session1,
|
||||
session=session1,
|
||||
lifespan_context="context",
|
||||
)
|
||||
|
||||
context2 = RequestContext(
|
||||
request_id="test-123",
|
||||
meta=None,
|
||||
session=mock_session1,
|
||||
session=session1,
|
||||
lifespan_context="context",
|
||||
)
|
||||
|
||||
context3 = RequestContext(
|
||||
request_id="test-456",
|
||||
meta=None,
|
||||
session=mock_session1,
|
||||
session=session1,
|
||||
lifespan_context="context",
|
||||
)
|
||||
|
||||
@@ -175,24 +184,23 @@ class TestRequestContext:
|
||||
context4 = RequestContext(
|
||||
request_id="test-123",
|
||||
meta=None,
|
||||
session=mock_session2,
|
||||
session=session2,
|
||||
lifespan_context="context",
|
||||
)
|
||||
assert context1 != context4
|
||||
|
||||
def test_request_context_repr(self):
|
||||
"""Test RequestContext string representation."""
|
||||
mock_session = Mock(spec=BaseSession)
|
||||
mock_session.__repr__ = Mock(return_value="<MockSession>")
|
||||
session = _session()
|
||||
|
||||
context = RequestContext(
|
||||
request_id="test-123",
|
||||
meta=None,
|
||||
session=mock_session,
|
||||
session=session,
|
||||
lifespan_context={"data": "test"},
|
||||
)
|
||||
|
||||
repr_str = repr(context)
|
||||
assert "RequestContext" in repr_str
|
||||
assert "test-123" in repr_str
|
||||
assert "MockSession" in repr_str
|
||||
assert repr(session) in repr_str
|
||||
@@ -44,11 +44,8 @@ class MockBrandingService:
|
||||
self.application_title = application_title
|
||||
|
||||
def get_branding_config(self) -> BrandingModel:
|
||||
"""Return mock branding configuration"""
|
||||
branding_model = MagicMock(spec=BrandingModel)
|
||||
branding_model.enabled = self.enabled
|
||||
branding_model.application_title = self.application_title
|
||||
return branding_model
|
||||
"""Return branding configuration with the requested test settings."""
|
||||
return BrandingModel(enabled=self.enabled, application_title=self.application_title)
|
||||
|
||||
|
||||
class MockEmailSender:
|
||||
|
||||
@@ -54,15 +54,13 @@ class TestApiToolInvoke:
|
||||
author="test_author",
|
||||
)
|
||||
|
||||
# Create a mock runtime
|
||||
self.mock_runtime = Mock(spec=ToolRuntime)
|
||||
self.mock_runtime.credentials = {"auth_type": "none"}
|
||||
self.runtime = ToolRuntime(tenant_id="test_tenant", credentials={"auth_type": "none"})
|
||||
|
||||
# Create the ApiTool instance
|
||||
self.api_tool = ApiTool(
|
||||
entity=self.mock_tool_entity,
|
||||
api_bundle=self.mock_api_bundle,
|
||||
runtime=self.mock_runtime,
|
||||
runtime=self.runtime,
|
||||
provider_id="test_provider",
|
||||
)
|
||||
|
||||
@@ -71,19 +69,13 @@ class TestApiToolInvoke:
|
||||
self, mock_get: Mock, database_session: Session
|
||||
) -> None:
|
||||
"""Test that when upstream returns JSON, the output Text message contains JSON-serialized string."""
|
||||
# Setup mock response with JSON content
|
||||
# Use the response's real JSON parsing and content decoding.
|
||||
json_response_data = {
|
||||
"key": "value",
|
||||
"number": 123,
|
||||
"nested": {"inner": "data"},
|
||||
}
|
||||
mock_response = Mock(spec=httpx.Response)
|
||||
mock_response.status_code = 200
|
||||
mock_response.content = json.dumps(json_response_data).encode("utf-8")
|
||||
mock_response.json.return_value = json_response_data
|
||||
mock_response.text = json.dumps(json_response_data)
|
||||
mock_response.headers = {"content-type": "application/json"}
|
||||
mock_get.return_value = mock_response
|
||||
mock_get.return_value = httpx.Response(200, json=json_response_data)
|
||||
|
||||
# Invoke the tool
|
||||
result_generator = self.api_tool._invoke(session=database_session, user_id="test_user", tool_parameters={})
|
||||
@@ -150,15 +142,11 @@ class TestApiToolInvoke:
|
||||
self, mock_get: Mock, test_case, database_session: Session
|
||||
) -> None:
|
||||
"""Test that when upstream returns a non-dict JSON, the output Text message contains JSON-serialized string."""
|
||||
# Setup mock response with non-dict JSON content
|
||||
# Explicit content also covers JSON null, which is distinct from no body.
|
||||
_, json_value = test_case
|
||||
mock_response = Mock(spec=httpx.Response)
|
||||
mock_response.status_code = 200
|
||||
mock_response.content = json.dumps(json_value).encode("utf-8")
|
||||
mock_response.json.return_value = json_value
|
||||
mock_response.text = json.dumps(json_value)
|
||||
mock_response.headers = {"content-type": "application/json"}
|
||||
mock_get.return_value = mock_response
|
||||
mock_get.return_value = httpx.Response(
|
||||
200, content=json.dumps(json_value), headers={"content-type": "application/json"}
|
||||
)
|
||||
|
||||
# Invoke the tool
|
||||
result_generator = self.api_tool._invoke(session=database_session, user_id="test_user", tool_parameters={})
|
||||
@@ -189,15 +177,9 @@ class TestApiToolInvoke:
|
||||
self, mock_get: Mock, database_session: Session
|
||||
) -> None:
|
||||
"""Test that when upstream returns plain text, the output Text message contains the original text."""
|
||||
# Setup mock response with plain text content
|
||||
# Plain text exercises the response's real JSON decoding failure.
|
||||
text_response_data = "This is a plain text response"
|
||||
mock_response = Mock(spec=httpx.Response)
|
||||
mock_response.status_code = 200
|
||||
mock_response.content = text_response_data.encode("utf-8")
|
||||
mock_response.json.side_effect = json.JSONDecodeError("Expecting value", "doc", 0)
|
||||
mock_response.text = text_response_data
|
||||
mock_response.headers = {"content-type": "text/plain"}
|
||||
mock_get.return_value = mock_response
|
||||
mock_get.return_value = httpx.Response(200, text=text_response_data)
|
||||
|
||||
# Invoke the tool
|
||||
result_generator = self.api_tool._invoke(session=database_session, user_id="test_user", tool_parameters={})
|
||||
@@ -218,12 +200,7 @@ class TestApiToolInvoke:
|
||||
@patch("core.tools.custom_tool.tool.ssrf_proxy.get")
|
||||
def test_invoke_with_empty_response(self, mock_get: Mock, database_session: Session) -> None:
|
||||
"""Test that empty responses are handled correctly."""
|
||||
# Setup mock response with empty content
|
||||
mock_response = Mock(spec=httpx.Response)
|
||||
mock_response.status_code = 200
|
||||
mock_response.content = b""
|
||||
mock_response.headers = {"content-type": "application/json"}
|
||||
mock_get.return_value = mock_response
|
||||
mock_get.return_value = httpx.Response(200, content=b"", headers={"content-type": "application/json"})
|
||||
|
||||
# Invoke the tool
|
||||
result_generator = self.api_tool._invoke(session=database_session, user_id="test_user", tool_parameters={})
|
||||
@@ -244,11 +221,7 @@ class TestApiToolInvoke:
|
||||
@patch("core.tools.custom_tool.tool.ssrf_proxy.get")
|
||||
def test_invoke_with_error_response(self, mock_get: Mock, database_session: Session) -> None:
|
||||
"""Test that error responses are handled correctly."""
|
||||
# Setup mock response with error status code
|
||||
mock_response = Mock(spec=httpx.Response)
|
||||
mock_response.status_code = 404
|
||||
mock_response.text = "Not Found"
|
||||
mock_get.return_value = mock_response
|
||||
mock_get.return_value = httpx.Response(404, text="Not Found")
|
||||
|
||||
result_generator = self.api_tool._invoke(session=database_session, user_id="test_user", tool_parameters={})
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import base64
|
||||
from collections.abc import Iterator
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
from unittest.mock import Mock, patch
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.engine import Engine
|
||||
@@ -39,8 +39,7 @@ def _make_mcp_tool(output_schema: dict[str, Any] | None = None) -> MCPTool:
|
||||
provider="test_provider",
|
||||
)
|
||||
entity = ToolEntity(identity=identity, output_schema=output_schema or {})
|
||||
runtime = Mock(spec=ToolRuntime)
|
||||
runtime.credentials = {}
|
||||
runtime = ToolRuntime(tenant_id="test_tenant")
|
||||
return MCPTool(
|
||||
entity=entity,
|
||||
runtime=runtime,
|
||||
|
||||
Reference in new issue
Block a user