From 4bb217db65aafe7b43193db73754d22f3d216ce5 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Thu, 13 Aug 2026 13:54:45 +0900 Subject: [PATCH] test: isolate parameter extractor model fixtures --- .../model_runtime/__mock/plugin_model.py | 254 +---------------- .../parameter_extractor/fixtures}/model.py | 2 + .../fixtures}/plugin_daemon.py | 14 +- .../fixtures/plugin_model.py | 255 ++++++++++++++++++ ...parameter_extractor_execution_behavior.py} | 6 +- 5 files changed, 267 insertions(+), 264 deletions(-) rename api/tests/{integration_tests/workflow/nodes/__mock => unit_tests/core/workflow/nodes/parameter_extractor/fixtures}/model.py (97%) rename api/tests/{integration_tests/model_runtime/__mock => unit_tests/core/workflow/nodes/parameter_extractor/fixtures}/plugin_daemon.py (74%) create mode 100644 api/tests/unit_tests/core/workflow/nodes/parameter_extractor/fixtures/plugin_model.py rename api/tests/{integration_tests/workflow/nodes/test_parameter_extractor.py => unit_tests/core/workflow/nodes/parameter_extractor/test_parameter_extractor_execution_behavior.py} (97%) diff --git a/api/tests/integration_tests/model_runtime/__mock/plugin_model.py b/api/tests/integration_tests/model_runtime/__mock/plugin_model.py index 7be1e2744fe..7e8c24a9f4e 100644 --- a/api/tests/integration_tests/model_runtime/__mock/plugin_model.py +++ b/api/tests/integration_tests/model_runtime/__mock/plugin_model.py @@ -1,253 +1,5 @@ -import datetime -import uuid -from collections.abc import Generator, Sequence -from decimal import Decimal -from json import dumps +"""Compatibility import for unit tests that predate the integration-test migration.""" -from core.plugin.entities.plugin import PluginInstallationSource -from core.plugin.entities.plugin_daemon import PluginModelProviderEntity -from core.plugin.impl.model import PluginModelClient +from tests.unit_tests.core.workflow.nodes.parameter_extractor.fixtures.plugin_model import MockModelClass -# import monkeypatch -from graphon.model_runtime.entities.common_entities import I18nObject -from graphon.model_runtime.entities.llm_entities import ( - LLMMode, - LLMResult, - LLMResultChunk, - LLMResultChunkDelta, - LLMUsage, -) -from graphon.model_runtime.entities.message_entities import AssistantPromptMessage, PromptMessage, PromptMessageTool -from graphon.model_runtime.entities.model_entities import ( - AIModelEntity, - FetchFrom, - ModelFeature, - ModelPropertyKey, - ModelType, -) -from graphon.model_runtime.entities.provider_entities import ConfigurateMethod, ProviderEntity - - -class MockModelClass(PluginModelClient): - def fetch_model_providers(self, tenant_id: str) -> Sequence[PluginModelProviderEntity]: - """ - Fetch model providers for the given tenant. - """ - return [ - PluginModelProviderEntity( - id=uuid.uuid4().hex, - created_at=datetime.datetime.now(), - updated_at=datetime.datetime.now(), - provider="openai", - tenant_id=tenant_id, - plugin_unique_identifier="langgenius/openai/openai", - plugin_id="langgenius/openai", - installation_source=PluginInstallationSource.Marketplace, - declaration=ProviderEntity( - provider="openai", - label=I18nObject( - en_US="OpenAI", - zh_Hans="OpenAI", - ), - description=I18nObject( - en_US="OpenAI", - zh_Hans="OpenAI", - ), - icon_small=I18nObject( - en_US="https://example.com/icon_small.png", - zh_Hans="https://example.com/icon_small.png", - ), - supported_model_types=[ModelType.LLM], - configurate_methods=[ConfigurateMethod.PREDEFINED_MODEL], - models=[ - AIModelEntity( - model="gpt-3.5-turbo", - label=I18nObject( - en_US="gpt-3.5-turbo", - zh_Hans="gpt-3.5-turbo", - ), - model_type=ModelType.LLM, - fetch_from=FetchFrom.PREDEFINED_MODEL, - model_properties={}, - features=[ModelFeature.TOOL_CALL, ModelFeature.MULTI_TOOL_CALL], - ), - AIModelEntity( - model="gpt-3.5-turbo-instruct", - label=I18nObject( - en_US="gpt-3.5-turbo-instruct", - zh_Hans="gpt-3.5-turbo-instruct", - ), - model_type=ModelType.LLM, - fetch_from=FetchFrom.PREDEFINED_MODEL, - model_properties={ - ModelPropertyKey.MODE: LLMMode.COMPLETION, - }, - features=[], - ), - ], - ), - ) - ] - - def get_model_schema( - self, - tenant_id: str, - user_id: str, - plugin_id: str, - provider: str, - model_type: str, - model: str, - credentials: dict, - ) -> AIModelEntity | None: - """ - Get model schema - """ - return AIModelEntity( - model=model, - label=I18nObject( - en_US="OpenAI", - zh_Hans="OpenAI", - ), - model_type=ModelType(model_type), - fetch_from=FetchFrom.PREDEFINED_MODEL, - model_properties={}, - features=[ModelFeature.TOOL_CALL, ModelFeature.MULTI_TOOL_CALL] if model == "gpt-3.5-turbo" else [], - ) - - @staticmethod - def generate_function_call( - tools: list[PromptMessageTool] | None, - ) -> AssistantPromptMessage.ToolCall | None: - if not tools or len(tools) == 0: - return None - function: PromptMessageTool = tools[0] - function_name = function.name - function_parameters = function.parameters - function_parameters_type = function_parameters["type"] - if function_parameters_type != "object": - return None - function_parameters_properties = function_parameters["properties"] - function_parameters_required = function_parameters["required"] - parameters = {} - for parameter_name, parameter in function_parameters_properties.items(): - if parameter_name not in function_parameters_required: - continue - parameter_type = parameter["type"] - if parameter_type == "string": - if "enum" in parameter: - if len(parameter["enum"]) == 0: - continue - parameters[parameter_name] = parameter["enum"][0] - else: - parameters[parameter_name] = "kawaii" - elif parameter_type == "integer": - parameters[parameter_name] = 114514 - elif parameter_type == "number": - parameters[parameter_name] = 1919810.0 - elif parameter_type == "boolean": - parameters[parameter_name] = True - - return AssistantPromptMessage.ToolCall( - id=str(uuid.uuid4()), - type="function", - function=AssistantPromptMessage.ToolCall.ToolCallFunction( - name=function_name, - arguments=dumps(parameters), - ), - ) - - @staticmethod - def mocked_chat_create_sync( - model: str, - prompt_messages: list[PromptMessage], - tools: list[PromptMessageTool] | None = None, - ) -> LLMResult: - tool_call = MockModelClass.generate_function_call(tools=tools) - - return LLMResult( - id=str(uuid.uuid4()), - model=model, - prompt_messages=prompt_messages, - message=AssistantPromptMessage(content="elaina", tool_calls=[tool_call] if tool_call else []), - usage=LLMUsage( - prompt_tokens=2, - completion_tokens=1, - total_tokens=3, - prompt_unit_price=Decimal(0.0001), - completion_unit_price=Decimal(0.0002), - prompt_price_unit=Decimal(1), - prompt_price=Decimal(0.0001), - completion_price_unit=Decimal(1), - completion_price=Decimal(0.0002), - total_price=Decimal(0.0003), - currency="USD", - latency=0.001, - ), - ) - - @staticmethod - def mocked_chat_create_stream( - model: str, - prompt_messages: list[PromptMessage], - tools: list[PromptMessageTool] | None = None, - ) -> Generator[LLMResultChunk, None, None]: - tool_call = MockModelClass.generate_function_call(tools=tools) - - full_text = "Hello, world!\n\n```python\nprint('Hello, world!')\n```" - for i in range(0, len(full_text) + 1): - if i == len(full_text): - yield LLMResultChunk( - model=model, - prompt_messages=prompt_messages, - delta=LLMResultChunkDelta( - index=0, - message=AssistantPromptMessage( - content="", - tool_calls=[tool_call] if tool_call else [], - ), - ), - ) - else: - yield LLMResultChunk( - model=model, - prompt_messages=prompt_messages, - delta=LLMResultChunkDelta( - index=0, - message=AssistantPromptMessage( - content=full_text[i], - tool_calls=[tool_call] if tool_call else [], - ), - usage=LLMUsage( - prompt_tokens=2, - completion_tokens=17, - total_tokens=19, - prompt_unit_price=Decimal(0.0001), - completion_unit_price=Decimal(0.0002), - prompt_price_unit=Decimal(1), - prompt_price=Decimal(0.0001), - completion_price_unit=Decimal(1), - completion_price=Decimal(0.0002), - total_price=Decimal(0.0003), - currency="USD", - latency=0.001, - ), - ), - ) - - def invoke_llm( - self: PluginModelClient, - *, - tenant_id: str, - user_id: str, - plugin_id: str, - provider: str, - model: str, - credentials: dict, - prompt_messages: list[PromptMessage], - model_parameters: dict | None = None, - tools: list[PromptMessageTool] | None = None, - stop: list[str] | None = None, - stream: bool = True, - app_id: str | None = None, - ): - return MockModelClass.mocked_chat_create_stream(model=model, prompt_messages=prompt_messages, tools=tools) +__all__ = ["MockModelClass"] diff --git a/api/tests/integration_tests/workflow/nodes/__mock/model.py b/api/tests/unit_tests/core/workflow/nodes/parameter_extractor/fixtures/model.py similarity index 97% rename from api/tests/integration_tests/workflow/nodes/__mock/model.py rename to api/tests/unit_tests/core/workflow/nodes/parameter_extractor/fixtures/model.py index a77fe5970a4..32e93cd0701 100644 --- a/api/tests/integration_tests/workflow/nodes/__mock/model.py +++ b/api/tests/unit_tests/core/workflow/nodes/parameter_extractor/fixtures/model.py @@ -1,3 +1,5 @@ +"""Model-instance builders for parameter-extractor unit tests.""" + from unittest.mock import MagicMock from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity diff --git a/api/tests/integration_tests/model_runtime/__mock/plugin_daemon.py b/api/tests/unit_tests/core/workflow/nodes/parameter_extractor/fixtures/plugin_daemon.py similarity index 74% rename from api/tests/integration_tests/model_runtime/__mock/plugin_daemon.py rename to api/tests/unit_tests/core/workflow/nodes/parameter_extractor/fixtures/plugin_daemon.py index d4cd5df5537..0632ec1b8a6 100644 --- a/api/tests/integration_tests/model_runtime/__mock/plugin_daemon.py +++ b/api/tests/unit_tests/core/workflow/nodes/parameter_extractor/fixtures/plugin_daemon.py @@ -1,10 +1,9 @@ -import os from collections.abc import Callable import pytest from core.plugin.impl.model import PluginModelClient -from tests.integration_tests.model_runtime.__mock.plugin_model import MockModelClass +from tests.unit_tests.core.workflow.nodes.parameter_extractor.fixtures.plugin_model import MockModelClass def mock_plugin_daemon( @@ -27,15 +26,8 @@ def mock_plugin_daemon( return unpatch -MOCK = os.getenv("MOCK_SWITCH", "false").lower() == "true" - - @pytest.fixture def setup_model_mock(monkeypatch: pytest.MonkeyPatch): - if MOCK: - unpatch = mock_plugin_daemon(monkeypatch) - + unpatch = mock_plugin_daemon(monkeypatch) yield - - if MOCK: - unpatch() + unpatch() diff --git a/api/tests/unit_tests/core/workflow/nodes/parameter_extractor/fixtures/plugin_model.py b/api/tests/unit_tests/core/workflow/nodes/parameter_extractor/fixtures/plugin_model.py new file mode 100644 index 00000000000..1fb376f425f --- /dev/null +++ b/api/tests/unit_tests/core/workflow/nodes/parameter_extractor/fixtures/plugin_model.py @@ -0,0 +1,255 @@ +"""Deterministic plugin-model double for parameter-extractor unit tests.""" + +import datetime +import uuid +from collections.abc import Generator, Sequence +from decimal import Decimal +from json import dumps + +from core.plugin.entities.plugin import PluginInstallationSource +from core.plugin.entities.plugin_daemon import PluginModelProviderEntity +from core.plugin.impl.model import PluginModelClient + +# import monkeypatch +from graphon.model_runtime.entities.common_entities import I18nObject +from graphon.model_runtime.entities.llm_entities import ( + LLMMode, + LLMResult, + LLMResultChunk, + LLMResultChunkDelta, + LLMUsage, +) +from graphon.model_runtime.entities.message_entities import AssistantPromptMessage, PromptMessage, PromptMessageTool +from graphon.model_runtime.entities.model_entities import ( + AIModelEntity, + FetchFrom, + ModelFeature, + ModelPropertyKey, + ModelType, +) +from graphon.model_runtime.entities.provider_entities import ConfigurateMethod, ProviderEntity + + +class MockModelClass(PluginModelClient): + def fetch_model_providers(self, tenant_id: str) -> Sequence[PluginModelProviderEntity]: + """ + Fetch model providers for the given tenant. + """ + return [ + PluginModelProviderEntity( + id=uuid.uuid4().hex, + created_at=datetime.datetime.now(), + updated_at=datetime.datetime.now(), + provider="openai", + tenant_id=tenant_id, + plugin_unique_identifier="langgenius/openai/openai", + plugin_id="langgenius/openai", + installation_source=PluginInstallationSource.Marketplace, + declaration=ProviderEntity( + provider="openai", + label=I18nObject( + en_US="OpenAI", + zh_Hans="OpenAI", + ), + description=I18nObject( + en_US="OpenAI", + zh_Hans="OpenAI", + ), + icon_small=I18nObject( + en_US="https://example.com/icon_small.png", + zh_Hans="https://example.com/icon_small.png", + ), + supported_model_types=[ModelType.LLM], + configurate_methods=[ConfigurateMethod.PREDEFINED_MODEL], + models=[ + AIModelEntity( + model="gpt-3.5-turbo", + label=I18nObject( + en_US="gpt-3.5-turbo", + zh_Hans="gpt-3.5-turbo", + ), + model_type=ModelType.LLM, + fetch_from=FetchFrom.PREDEFINED_MODEL, + model_properties={}, + features=[ModelFeature.TOOL_CALL, ModelFeature.MULTI_TOOL_CALL], + ), + AIModelEntity( + model="gpt-3.5-turbo-instruct", + label=I18nObject( + en_US="gpt-3.5-turbo-instruct", + zh_Hans="gpt-3.5-turbo-instruct", + ), + model_type=ModelType.LLM, + fetch_from=FetchFrom.PREDEFINED_MODEL, + model_properties={ + ModelPropertyKey.MODE: LLMMode.COMPLETION, + }, + features=[], + ), + ], + ), + ) + ] + + def get_model_schema( + self, + tenant_id: str, + user_id: str, + plugin_id: str, + provider: str, + model_type: str, + model: str, + credentials: dict, + ) -> AIModelEntity | None: + """ + Get model schema + """ + return AIModelEntity( + model=model, + label=I18nObject( + en_US="OpenAI", + zh_Hans="OpenAI", + ), + model_type=ModelType(model_type), + fetch_from=FetchFrom.PREDEFINED_MODEL, + model_properties={}, + features=[ModelFeature.TOOL_CALL, ModelFeature.MULTI_TOOL_CALL] if model == "gpt-3.5-turbo" else [], + ) + + @staticmethod + def generate_function_call( + tools: list[PromptMessageTool] | None, + ) -> AssistantPromptMessage.ToolCall | None: + if not tools or len(tools) == 0: + return None + function: PromptMessageTool = tools[0] + function_name = function.name + function_parameters = function.parameters + function_parameters_type = function_parameters["type"] + if function_parameters_type != "object": + return None + function_parameters_properties = function_parameters["properties"] + function_parameters_required = function_parameters["required"] + parameters = {} + for parameter_name, parameter in function_parameters_properties.items(): + if parameter_name not in function_parameters_required: + continue + parameter_type = parameter["type"] + if parameter_type == "string": + if "enum" in parameter: + if len(parameter["enum"]) == 0: + continue + parameters[parameter_name] = parameter["enum"][0] + else: + parameters[parameter_name] = "kawaii" + elif parameter_type == "integer": + parameters[parameter_name] = 114514 + elif parameter_type == "number": + parameters[parameter_name] = 1919810.0 + elif parameter_type == "boolean": + parameters[parameter_name] = True + + return AssistantPromptMessage.ToolCall( + id=str(uuid.uuid4()), + type="function", + function=AssistantPromptMessage.ToolCall.ToolCallFunction( + name=function_name, + arguments=dumps(parameters), + ), + ) + + @staticmethod + def mocked_chat_create_sync( + model: str, + prompt_messages: list[PromptMessage], + tools: list[PromptMessageTool] | None = None, + ) -> LLMResult: + tool_call = MockModelClass.generate_function_call(tools=tools) + + return LLMResult( + id=str(uuid.uuid4()), + model=model, + prompt_messages=prompt_messages, + message=AssistantPromptMessage(content="elaina", tool_calls=[tool_call] if tool_call else []), + usage=LLMUsage( + prompt_tokens=2, + completion_tokens=1, + total_tokens=3, + prompt_unit_price=Decimal(0.0001), + completion_unit_price=Decimal(0.0002), + prompt_price_unit=Decimal(1), + prompt_price=Decimal(0.0001), + completion_price_unit=Decimal(1), + completion_price=Decimal(0.0002), + total_price=Decimal(0.0003), + currency="USD", + latency=0.001, + ), + ) + + @staticmethod + def mocked_chat_create_stream( + model: str, + prompt_messages: list[PromptMessage], + tools: list[PromptMessageTool] | None = None, + ) -> Generator[LLMResultChunk, None, None]: + tool_call = MockModelClass.generate_function_call(tools=tools) + + full_text = "Hello, world!\n\n```python\nprint('Hello, world!')\n```" + for i in range(0, len(full_text) + 1): + if i == len(full_text): + yield LLMResultChunk( + model=model, + prompt_messages=prompt_messages, + delta=LLMResultChunkDelta( + index=0, + message=AssistantPromptMessage( + content="", + tool_calls=[tool_call] if tool_call else [], + ), + ), + ) + else: + yield LLMResultChunk( + model=model, + prompt_messages=prompt_messages, + delta=LLMResultChunkDelta( + index=0, + message=AssistantPromptMessage( + content=full_text[i], + tool_calls=[tool_call] if tool_call else [], + ), + usage=LLMUsage( + prompt_tokens=2, + completion_tokens=17, + total_tokens=19, + prompt_unit_price=Decimal(0.0001), + completion_unit_price=Decimal(0.0002), + prompt_price_unit=Decimal(1), + prompt_price=Decimal(0.0001), + completion_price_unit=Decimal(1), + completion_price=Decimal(0.0002), + total_price=Decimal(0.0003), + currency="USD", + latency=0.001, + ), + ), + ) + + def invoke_llm( + self: PluginModelClient, + *, + tenant_id: str, + user_id: str, + plugin_id: str, + provider: str, + model: str, + credentials: dict, + prompt_messages: list[PromptMessage], + model_parameters: dict | None = None, + tools: list[PromptMessageTool] | None = None, + stop: list[str] | None = None, + stream: bool = True, + app_id: str | None = None, + ): + return MockModelClass.mocked_chat_create_stream(model=model, prompt_messages=prompt_messages, tools=tools) diff --git a/api/tests/integration_tests/workflow/nodes/test_parameter_extractor.py b/api/tests/unit_tests/core/workflow/nodes/parameter_extractor/test_parameter_extractor_execution_behavior.py similarity index 97% rename from api/tests/integration_tests/workflow/nodes/test_parameter_extractor.py rename to api/tests/unit_tests/core/workflow/nodes/parameter_extractor/test_parameter_extractor_execution_behavior.py index 44708285037..ee83192dfec 100644 --- a/api/tests/integration_tests/workflow/nodes/test_parameter_extractor.py +++ b/api/tests/unit_tests/core/workflow/nodes/parameter_extractor/test_parameter_extractor_execution_behavior.py @@ -1,3 +1,5 @@ +"""Unit coverage for parameter extraction with the plugin-model boundary replaced.""" + import os import time import uuid @@ -16,10 +18,10 @@ from graphon.nodes.llm.protocols import CredentialsProvider, ModelFactory from graphon.nodes.parameter_extractor.entities import ParameterExtractorNodeData from graphon.nodes.parameter_extractor.parameter_extractor_node import ParameterExtractorNode from graphon.runtime import GraphRuntimeState, VariablePool -from tests.integration_tests.workflow.nodes.__mock.model import get_mocked_fetch_model_instance +from tests.unit_tests.core.workflow.nodes.parameter_extractor.fixtures.model import get_mocked_fetch_model_instance from tests.workflow_test_utils import build_test_graph_init_params -pytest_plugins = ("tests.integration_tests.model_runtime.__mock.plugin_daemon",) +pytest_plugins = ("tests.unit_tests.core.workflow.nodes.parameter_extractor.fixtures.plugin_daemon",) def get_mocked_fetch_memory(memory_text: str):