refactor(api): enable strict typing for core/libs/services test files (#44815)

Co-authored-by: Felix Shi <204130665+shi-felix@users.noreply.github.com>
Co-authored-by: Asuka Minato <i@asukaminato.eu.org>
This commit is contained in:
authored and GitHub committed 2026-10-26 17:18:11 +00:00
1 parent bc989a63ea
commit adb27d0d01
10 files changed
+74 -57

No files matched your search

@@ -1,19 +1,24 @@
from types import SimpleNamespace
from unittest.mock import Mock, patch
from core.app.features.hosting_moderation.hosting_moderation import HostingModerationFeature
from core.app.features.hosting_moderation.hosting_moderation import (
HostingModerationFeature,
)
from graphon.model_runtime.entities.message_entities import PromptMessage, UserPromptMessage
class TestHostingModerationFeature:
def test_check_aggregates_text_and_calls_moderation(self):
def test_check_aggregates_text_and_calls_moderation(self) -> None:
application_generate_entity = Mock()
application_generate_entity.model_conf = {"model": "mock"}
application_generate_entity.app_config = SimpleNamespace(tenant_id="tenant-1")
prompt_messages = [
SimpleNamespace(content="hello"),
SimpleNamespace(content=123),
SimpleNamespace(content="world"),
# The middle message bypasses validation on purpose: `content` is not
# always a string at runtime, and the feature must skip those entries.
prompt_messages: list[PromptMessage] = [
UserPromptMessage(role="user", content="hello"),
UserPromptMessage.model_construct(role="user", content=123),
UserPromptMessage(role="user", content="world"),
]
with patch("core.app.features.hosting_moderation.hosting_moderation.moderation.check_moderation") as mock_check:
@@ -4,7 +4,7 @@ from core.extension.extensible import ExtensionModule
from core.external_data_tool.factory import ExternalDataToolFactory
def test_external_data_tool_factory_init():
def test_external_data_tool_factory_init() -> None:
with patch("core.external_data_tool.factory.code_based_extension") as mock_code_based_extension:
mock_extension_class = MagicMock()
mock_code_based_extension.extension_class.return_value = mock_extension_class
@@ -23,7 +23,7 @@ def test_external_data_tool_factory_init():
)
def test_external_data_tool_factory_validate_config():
def test_external_data_tool_factory_validate_config() -> None:
with patch("core.external_data_tool.factory.code_based_extension") as mock_code_based_extension:
mock_extension_class = MagicMock()
mock_code_based_extension.extension_class.return_value = mock_extension_class
@@ -38,7 +38,7 @@ def test_external_data_tool_factory_validate_config():
mock_extension_class.validate_config.assert_called_once_with(tenant_id, config)
def test_external_data_tool_factory_query():
def test_external_data_tool_factory_query() -> None:
with patch("core.external_data_tool.factory.code_based_extension") as mock_code_based_extension:
mock_extension_class = MagicMock()
mock_extension_instance = MagicMock()
@@ -6,7 +6,7 @@ from core.plugin.impl.asset import PluginAssetManager
class TestPluginAssetManager:
def test_fetch_asset_success(self, mocker: MockerFixture):
def test_fetch_asset_success(self, mocker: MockerFixture) -> None:
manager = PluginAssetManager()
response = httpx.Response(status_code=200, content=b"asset-bytes")
request_mock = mocker.patch.object(manager, "_request", return_value=response)
@@ -16,14 +16,18 @@ class TestPluginAssetManager:
assert result == b"asset-bytes"
request_mock.assert_called_once_with(method="GET", path="plugin/tenant-1/asset/asset-1")
def test_fetch_asset_not_found_raises(self, mocker: MockerFixture):
def test_fetch_asset_not_found_raises(self, mocker: MockerFixture) -> None:
manager = PluginAssetManager()
mocker.patch.object(manager, "_request", return_value=httpx.Response(status_code=404, content=b""))
mocker.patch.object(
manager,
"_request",
return_value=httpx.Response(status_code=404, content=b""),
)
with pytest.raises(ValueError, match="can not found asset asset-1"):
manager.fetch_asset("tenant-1", "asset-1")
def test_extract_asset_success(self, mocker: MockerFixture):
def test_extract_asset_success(self, mocker: MockerFixture) -> None:
manager = PluginAssetManager()
response = httpx.Response(status_code=200, content=b"file-content")
request_mock = mocker.patch.object(manager, "_request", return_value=response)
@@ -34,12 +38,19 @@ class TestPluginAssetManager:
request_mock.assert_called_once_with(
method="GET",
path="plugin/tenant-1/extract-asset/",
params={"plugin_unique_identifier": "org/plugin:1", "file_path": "README.md"},
params={
"plugin_unique_identifier": "org/plugin:1",
"file_path": "README.md",
},
)
def test_extract_asset_not_found_raises(self, mocker: MockerFixture):
def test_extract_asset_not_found_raises(self, mocker: MockerFixture) -> None:
manager = PluginAssetManager()
mocker.patch.object(manager, "_request", return_value=httpx.Response(status_code=404, content=b""))
mocker.patch.object(
manager,
"_request",
return_value=httpx.Response(status_code=404, content=b""),
)
with pytest.raises(ValueError, match="can not found asset org/plugin:1, 404"):
manager.extract_asset("tenant-1", "org/plugin:1", "README.md")
@@ -7,13 +7,13 @@ from core.plugin.impl.exc import PluginDaemonError, PluginInvokeError
class TestPluginImplExceptions:
def test_plugin_daemon_error_str_contains_request_id(self, mocker: MockerFixture):
def test_plugin_daemon_error_str_contains_request_id(self, mocker: MockerFixture) -> None:
mocker.patch("core.plugin.impl.exc.get_request_id", return_value="req-123")
error = PluginDaemonError("bad")
assert str(error) == "req_id: req-123 PluginDaemonError: bad"
def test_plugin_invoke_error_with_json_payload(self):
def test_plugin_invoke_error_with_json_payload(self) -> None:
err = PluginInvokeError(json.dumps({"error_type": "RateLimit", "message": "too many"}))
assert err.get_error_type() == "RateLimit"
@@ -23,7 +23,7 @@ class TestPluginImplExceptions:
assert "RateLimit" in friendly
assert "too many" in friendly
def test_plugin_invoke_error_invalid_json_and_fallback(self, mocker: MockerFixture):
def test_plugin_invoke_error_invalid_json_and_fallback(self, mocker: MockerFixture) -> None:
err = PluginInvokeError("plain text")
assert err._get_error_object() == {}
@@ -34,7 +34,7 @@ class TestPluginImplExceptions:
err2 = PluginInvokeError("plain text")
assert err2.get_error_message() == "plain text"
def test_plugin_invoke_error_get_error_object_handles_adapter_exception(self, mocker: MockerFixture):
def test_plugin_invoke_error_get_error_object_handles_adapter_exception(self, mocker: MockerFixture) -> None:
adapter = mocker.patch.object(exc_module, "TypeAdapter")
adapter.return_value.validate_json.side_effect = RuntimeError("invalid")
@@ -21,41 +21,41 @@ def _make_schema(name: str, field_type: str = "secret-input") -> ProviderConfig:
class TestMaskedCredentials:
def test_short_secret_fully_masked(self):
def test_short_secret_fully_masked(self) -> None:
schema = [_make_schema("key", "secret-input")]
result = masked_credentials(schema, {"key": "ab"})
assert result["key"] == "**"
def test_long_secret_partially_masked(self):
def test_long_secret_partially_masked(self) -> None:
schema = [_make_schema("key", "secret-input")]
result = masked_credentials(schema, {"key": "abcdef"})
assert result["key"].startswith("ab")
assert result["key"].endswith("ef")
assert "**" in result["key"]
def test_non_secret_field_unchanged(self):
def test_non_secret_field_unchanged(self) -> None:
schema = [_make_schema("host", "text-input")]
result = masked_credentials(schema, {"host": "example.com"})
assert result["host"] == "example.com"
def test_unknown_key_passes_through(self):
def test_unknown_key_passes_through(self) -> None:
result = masked_credentials([], {"unknown": "value"})
assert result["unknown"] == "value"
class TestCacheKeyGeneration:
def test_credentials_cache_key_contains_ids(self):
def test_credentials_cache_key_contains_ids(self) -> None:
cache = TriggerProviderCredentialsCache(tenant_id="t1", provider_id="p1", credential_id="c1")
assert "t1" in cache.cache_key
assert "p1" in cache.cache_key
assert "c1" in cache.cache_key
def test_oauth_client_cache_key_contains_ids(self):
def test_oauth_client_cache_key_contains_ids(self) -> None:
cache = TriggerProviderOAuthClientParamsCache(tenant_id="t1", provider_id="p1")
assert "t1" in cache.cache_key
assert "p1" in cache.cache_key
def test_properties_cache_key_contains_ids(self):
def test_properties_cache_key_contains_ids(self) -> None:
cache = TriggerProviderPropertiesCache(tenant_id="t1", provider_id="p1", subscription_id="s1")
assert "t1" in cache.cache_key
assert "p1" in cache.cache_key
@@ -24,25 +24,25 @@ _GRAPH = {
}
def test_upstream_node_ids_collects_all_ancestors_excluding_ghosts():
def test_upstream_node_ids_collects_all_ancestors_excluding_ghosts() -> None:
topology = WorkflowGraphTopology.from_graph(_GRAPH)
assert topology.upstream_node_ids("agent") == {"start", "llm-1", "llm-2"}
def test_upstream_node_ids_differ_per_target_node():
def test_upstream_node_ids_differ_per_target_node() -> None:
topology = WorkflowGraphTopology.from_graph(_GRAPH)
assert topology.upstream_node_ids("llm-1") == {"start"}
assert topology.upstream_node_ids("end") == {"start", "llm-1", "llm-2", "agent"}
assert topology.upstream_node_ids("start") == set()
def test_is_upstream_kept_for_publish_validation():
def test_is_upstream_kept_for_publish_validation() -> None:
topology = WorkflowGraphTopology.from_graph(_GRAPH)
assert topology.is_upstream(source_node_id="start", target_node_id="end")
assert not topology.is_upstream(source_node_id="end", target_node_id="start")
def test_cycle_safe():
def test_cycle_safe() -> None:
graph = {
"nodes": [{"id": "a"}, {"id": "b"}],
"edges": [{"source": "a", "target": "b"}, {"source": "b", "target": "a"}],
+11 -11
View File
@@ -8,27 +8,27 @@ from libs.custom_inputs import time_duration
class TestTimeDuration:
"""Test time_duration input validator."""
def test_valid_days(self):
def test_valid_days(self) -> None:
"""Test valid days format."""
result = time_duration("7d")
assert result == "7d"
def test_valid_hours(self):
def test_valid_hours(self) -> None:
"""Test valid hours format."""
result = time_duration("4h")
assert result == "4h"
def test_valid_minutes(self):
def test_valid_minutes(self) -> None:
"""Test valid minutes format."""
result = time_duration("30m")
assert result == "30m"
def test_valid_seconds(self):
def test_valid_seconds(self) -> None:
"""Test valid seconds format."""
result = time_duration("30s")
assert result == "30s"
def test_uppercase_conversion(self):
def test_uppercase_conversion(self) -> None:
"""Test uppercase units are converted to lowercase."""
result = time_duration("7D")
assert result == "7d"
@@ -36,12 +36,12 @@ class TestTimeDuration:
result = time_duration("4H")
assert result == "4h"
def test_invalid_format_no_unit(self):
def test_invalid_format_no_unit(self) -> None:
"""Test invalid format without unit."""
with pytest.raises(ValueError, match="Invalid time duration format"):
time_duration("7")
def test_invalid_format_wrong_unit(self):
def test_invalid_format_wrong_unit(self) -> None:
"""Test invalid format with wrong unit."""
with pytest.raises(ValueError, match="Invalid time duration format"):
time_duration("7days")
@@ -49,7 +49,7 @@ class TestTimeDuration:
with pytest.raises(ValueError, match="Invalid time duration format"):
time_duration("7x")
def test_invalid_format_no_number(self):
def test_invalid_format_no_number(self) -> None:
"""Test invalid format without number."""
with pytest.raises(ValueError, match="Invalid time duration format"):
time_duration("d")
@@ -57,12 +57,12 @@ class TestTimeDuration:
with pytest.raises(ValueError, match="Invalid time duration format"):
time_duration("abc")
def test_empty_string(self):
def test_empty_string(self) -> None:
"""Test empty string."""
with pytest.raises(ValueError, match="Time duration cannot be empty"):
time_duration("")
def test_none(self):
def test_none(self) -> None:
"""Test None value."""
with pytest.raises(ValueError, match="Time duration cannot be empty"):
time_duration(None)
time_duration(None) # type: ignore[arg-type]
-9
View File
@@ -186,7 +186,6 @@ project-excludes = [
"core/app/entities/test_app_invoke_entities.py",
"core/app/features/rate_limiting/test_rate_limit.py",
"core/app/features/annotation_reply/test_annotation_reply.py",
"core/app/features/hosting_moderation/test_hosting_moderation.py",
"core/app/layers/test_conversation_variable_persist_layer.py",
"core/app/layers/test_pause_state_persist_layer.py",
"core/app/layers/test_timeslice_layer.py",
@@ -208,7 +207,6 @@ project-excludes = [
"core/entities/test_mcp_provider.py",
"core/entities/test_provider_configuration.py",
"core/extension/test_api_based_extension_requestor.py",
"core/external_data_tool/test_factory.py",
"core/helper/code_executor/test_code_executor.py",
"core/helper/test_ssrf_proxy.py",
"core/llm_generator/output_parser/test_structured_output.py",
@@ -231,11 +229,9 @@ project-excludes = [
"core/ops/entities/test_trace_entity.py",
"core/ops/test_utils.py",
"core/plugin/impl/test_agent.py",
"core/plugin/impl/test_asset.py",
"core/plugin/impl/test_base.py",
"core/plugin/impl/test_datasource.py",
"core/plugin/impl/test_endpoint.py",
"core/plugin/impl/test_exc.py",
"core/plugin/impl/test_model.py",
"core/plugin/impl/test_oauth.py",
"core/plugin/impl/test_trigger.py",
@@ -300,7 +296,6 @@ project-excludes = [
"core/tools/utils/test_web_reader_tool.py",
"core/tools/workflow_as_tool/test_provider.py",
"core/tools/workflow_as_tool/test_tool.py",
"core/trigger/utils/test_encryption.py",
"context/test_execution_context.py",
"context/test_flask_app_context.py",
"../e2e_tests/contracts/core/workflow/entities/test_private_workflow_pause.py",
@@ -348,7 +343,6 @@ project-excludes = [
"core/workflow/nodes/trigger_webhook/test_entities.py",
"core/workflow/nodes/trigger_webhook/test_node.py",
"core/workflow/nodes/human_input/test_pause_reason.py",
"core/workflow/test_graph_topology.py",
"core/workflow/nodes/human_input/test_callback.py",
"core/workflow/test_human_input_forms.py",
"../e2e_tests/contracts/core/workflow/test_node_mapping_bootstrap.py",
@@ -384,7 +378,6 @@ project-excludes = [
"libs/broadcast_channel/redis/test_streams_channel.py",
"libs/test_archive_storage.py",
"libs/test_schedule_utils.py",
"libs/test_custom_inputs.py",
"libs/test_datetime_utils.py",
"libs/test_email_i18n.py",
"libs/test_external_api.py",
@@ -420,7 +413,6 @@ project-excludes = [
"../helpers/unit/services/hit_service.py",
"services/plugin/test_plugin_parameter_service.py",
"services/errors/test_plugin.py",
"services/rag_pipeline/pipeline_template/test_pipeline_template_base.py",
"services/rag_pipeline/test_pipeline_generate_service.py",
"services/rag_pipeline/test_rag_pipeline_dsl_service.py",
"services/rag_pipeline/test_rag_pipeline.py",
@@ -469,7 +461,6 @@ project-excludes = [
"services/test_workflow_node_execution_trace_service.py",
"services/test_workflow_run_service.py",
"services/test_workflow_service.py",
"services/tools/test_api_tools_manage_service.py",
"services/tools/test_builtin_tools_manage_service.py",
"services/tools/test_tools_transform_service.py",
"services/tools/test_tools_manage_service.py",
@@ -1,15 +1,21 @@
import pytest
from sqlalchemy.orm import Session
from services.rag_pipeline.pipeline_template.pipeline_template_base import PipelineTemplateRetrievalBase
from services.rag_pipeline.pipeline_template.pipeline_template_base import (
PipelineTemplateRetrievalBase,
)
class DummyRetrieval(PipelineTemplateRetrievalBase):
def get_pipeline_templates(self, language: str, *, session) -> dict:
del session
def get_pipeline_templates(
self, language: str, current_tenant_id: str | None = None, *, session: Session
) -> dict[str, object]:
del current_tenant_id, session
return {"language": language}
def get_pipeline_template_detail(self, template_id: str, current_tenant_id: str, *, session) -> dict | None:
def get_pipeline_template_detail(
self, template_id: str, current_tenant_id: str, *, session: Session
) -> dict[str, object] | None:
del current_tenant_id, session
return {"id": template_id}
@@ -18,7 +24,9 @@ class DummyRetrieval(PipelineTemplateRetrievalBase):
@pytest.mark.parametrize("sqlite_session", [()], indirect=True)
def test_pipeline_template_retrieval_base_concrete_implementation(sqlite_session: Session) -> None:
def test_pipeline_template_retrieval_base_concrete_implementation(
sqlite_session: Session,
) -> None:
retrieval = DummyRetrieval()
assert retrieval.get_pipeline_templates("en-US", session=sqlite_session) == {"language": "en-US"}
@@ -5,7 +5,9 @@ import pytest
from services.tools.api_tools_manage_service import ApiToolManageService
def test_get_api_tool_provider_remote_schema_uses_ssrf_proxy_get(monkeypatch: pytest.MonkeyPatch) -> None:
def test_get_api_tool_provider_remote_schema_uses_ssrf_proxy_get(
monkeypatch: pytest.MonkeyPatch,
) -> None:
schema = """
{
"openapi": "3.0.0",