From f7064810e48179d77f4eb6821099d9a3e63f08c3 Mon Sep 17 00:00:00 2001 From: Eddy Zhang Date: Sun, 1 Nov 2026 12:29:04 +0800 Subject: [PATCH] test(api): strictly type plugin model and chunk merger tests --- .../plugin/backwards_invocation/test_model.py | 30 +++++++------ .../core/plugin/utils/test_chunk_merger.py | 43 ++++++++++--------- api/tests/unit_tests/pyrefly.toml | 2 - 3 files changed, 40 insertions(+), 35 deletions(-) diff --git a/api/tests/unit_tests/core/plugin/backwards_invocation/test_model.py b/api/tests/unit_tests/core/plugin/backwards_invocation/test_model.py index 0bd803a2f325f0..5e871f8c136fb1 100644 --- a/api/tests/unit_tests/core/plugin/backwards_invocation/test_model.py +++ b/api/tests/unit_tests/core/plugin/backwards_invocation/test_model.py @@ -1,6 +1,7 @@ import json from collections.abc import Callable, Iterator from dataclasses import dataclass, field +from typing import Literal from unittest.mock import patch import httpx @@ -93,7 +94,7 @@ def _tts_schema(audio_type: str) -> AIModelEntity: ) -def test_system_model_helpers_forward_user_id(): +def test_system_model_helpers_forward_user_id() -> None: with ( patch( "core.plugin.backwards_invocation.model.ModelInvocationUtils.get_max_llm_context_tokens", @@ -122,7 +123,7 @@ def test_system_model_helpers_forward_user_id(): ) -def test_invoke_summary_uses_same_user_scope_for_token_helpers(): +def test_invoke_summary_uses_same_user_scope_for_token_helpers() -> None: tenant = Tenant(name="Test Workspace") tenant.id = "tenant-1" payload = RequestInvokeSummary(text="short", instruction="keep it concise") @@ -239,7 +240,7 @@ def get_model_schema(self) -> _ModelSchema: return self.schema -def test_invoke_text_embedding_forwards_input_type_to_bound_model_instance(): +def test_invoke_text_embedding_forwards_input_type_to_bound_model_instance() -> None: tenant = _backwards_invocation_model_tenant() payload = RequestInvokeTextEmbedding( provider="langgenius/openai/openai", @@ -270,7 +271,7 @@ def test_invoke_text_embedding_forwards_input_type_to_bound_model_instance(): ] -def test_invoke_multimodal_embedding_uses_bound_model_instance(): +def test_invoke_multimodal_embedding_uses_bound_model_instance() -> None: tenant = _backwards_invocation_model_tenant() documents = [{"content": "AQID", "content_type": "image", "file_id": "asset.png"}] payload = RequestInvokeMultimodalEmbedding( @@ -305,7 +306,7 @@ def test_invoke_multimodal_embedding_uses_bound_model_instance(): ] -def test_invoke_rerank_accepts_omitted_threshold_and_top_n(): +def test_invoke_rerank_accepts_omitted_threshold_and_top_n() -> None: tenant = _backwards_invocation_model_tenant() payload = RequestInvokeRerank( provider="langgenius/cohere/cohere", @@ -335,7 +336,9 @@ def test_invoke_rerank_accepts_omitted_threshold_and_top_n(): ("model_type", "selected"), [(ModelType.TEXT_EMBEDDING, False), (ModelType.LLM, False), (ModelType.LLM, True)], ) -def test_list_models_returns_only_active_tenant_models_with_installed_identity(model_type, selected): +def test_list_models_returns_only_active_tenant_models_with_installed_identity( + model_type: Literal[ModelType.LLM, ModelType.TEXT_EMBEDDING], selected: bool +) -> None: model_name = "selected-model" payload = RequestListModels(model_type=model_type, model=model_name if selected else None, limit=10) provider = _ModelProvider(provider="langgenius/openai/openai") @@ -395,7 +398,7 @@ def test_list_models_returns_only_active_tenant_models_with_installed_identity(m @pytest.mark.parametrize("parameter", ["max_tokens", "max_completion_tokens", "maxtoken", "custom_limit"]) -def test_model_token_limits_use_schema_maximum_not_default(parameter): +def test_model_token_limits_use_schema_maximum_not_default(parameter: str) -> None: schema = _ModelSchema( model_properties={"context_size": 131072}, parameter_rules=[_ParameterRule(name=parameter, use_template="max_tokens", max=32768.0, default=1024)], @@ -405,6 +408,7 @@ def test_model_token_limits_use_schema_maximum_not_default(parameter): limits = PluginModelBackwardsInvocation._model_token_limits( tenant_id="tenant-1", user_id="user-1", provider="plugin/provider", model="selected-model" ) + assert limits is not None assert limits.context_tokens == 131072 assert limits.max_output_tokens == 32768 assert limits.output_parameter == parameter @@ -418,7 +422,7 @@ def test_model_token_limits_use_schema_maximum_not_default(parameter): @pytest.mark.parametrize("maximum", [None, 0, -1, True, float("inf"), float("nan"), 10**400, 12.5, "32768"]) -def test_model_token_limits_do_not_invent_unknown_output_capacity(maximum): +def test_model_token_limits_do_not_invent_unknown_output_capacity(maximum: object) -> None: schema = _ModelSchema( model_properties={"context_size": 8192}, parameter_rules=[_ParameterRule(name="max_tokens", use_template=None, max=maximum, default=4096)], @@ -431,11 +435,12 @@ def test_model_token_limits_do_not_invent_unknown_output_capacity(maximum): limits = PluginModelBackwardsInvocation._model_token_limits( tenant_id="tenant-1", user_id="user-1", provider="plugin/provider", model="selected-model" ) + assert limits is not None assert limits.context_tokens == 8192 assert limits.max_output_tokens is None -def test_model_token_limits_respect_the_tighter_declared_alias(): +def test_model_token_limits_respect_the_tighter_declared_alias() -> None: schema = _ModelSchema( model_properties={}, parameter_rules=[ @@ -451,11 +456,12 @@ def test_model_token_limits_respect_the_tighter_declared_alias(): limits = PluginModelBackwardsInvocation._model_token_limits( tenant_id="tenant-1", user_id="user-1", provider="plugin/provider", model="selected-model" ) + assert limits is not None assert limits.max_output_tokens == 8192 assert limits.output_parameter == "max_output_tokens" -def test_invoke_tts_emits_the_verified_mime_type_for_backwards_invocation(): +def test_invoke_tts_emits_the_verified_mime_type_for_backwards_invocation() -> None: tenant = Tenant(name="Test Workspace") tenant.id = "tenant-1" model_instance = _tts_model_instance() @@ -485,7 +491,7 @@ def test_invoke_tts_emits_the_verified_mime_type_for_backwards_invocation(): ] -def test_invoke_tts_defers_provider_errors_until_the_response_generator_is_consumed(): +def test_invoke_tts_defers_provider_errors_until_the_response_generator_is_consumed() -> None: tenant = Tenant(name="Test Workspace") tenant.id = "tenant-1" model_instance = _tts_model_instance() @@ -509,7 +515,7 @@ def test_invoke_tts_defers_provider_errors_until_the_response_generator_is_consu next(response) -def test_backwards_event_stream_serializes_a_deferred_mime_error(): +def test_backwards_event_stream_serializes_a_deferred_mime_error() -> None: tenant = Tenant(name="Test Workspace") tenant.id = "tenant-1" model_instance = _tts_model_instance() diff --git a/api/tests/unit_tests/core/plugin/utils/test_chunk_merger.py b/api/tests/unit_tests/core/plugin/utils/test_chunk_merger.py index 314f6bff026236..8ca1a7118e38bd 100644 --- a/api/tests/unit_tests/core/plugin/utils/test_chunk_merger.py +++ b/api/tests/unit_tests/core/plugin/utils/test_chunk_merger.py @@ -10,14 +10,14 @@ class TestChunkMerger: - def test_file_chunk_initialization(self): + def test_file_chunk_initialization(self) -> None: """Test FileChunk initialization.""" chunk = FileChunk(1024) assert chunk.bytes_written == 0 assert chunk.total_length == 1024 assert len(chunk.data) == 1024 - def test_merge_blob_chunks_with_single_complete_chunk(self): + def test_merge_blob_chunks_with_single_complete_chunk(self) -> None: """Test merging a single complete blob chunk.""" def mock_generator() -> Generator[ToolInvokeMessage]: @@ -43,7 +43,7 @@ def mock_generator() -> Generator[ToolInvokeMessage]: # The buffer should contain the complete data assert result[0].message.blob[:10] == b"HelloWorld" - def test_merge_blob_chunks_with_multiple_files(self): + def test_merge_blob_chunks_with_multiple_files(self) -> None: """Test merging chunks from multiple files.""" def mock_generator() -> Generator[ToolInvokeMessage]: @@ -81,7 +81,7 @@ def mock_generator() -> Generator[ToolInvokeMessage]: # Check that both files are properly merged assert all(r.type == ToolInvokeMessage.MessageType.BLOB for r in result) - def test_merge_blob_chunks_passes_through_non_blob_messages(self): + def test_merge_blob_chunks_passes_through_non_blob_messages(self) -> None: """Test that non-blob messages pass through unchanged.""" def mock_generator() -> Generator[ToolInvokeMessage]: @@ -113,7 +113,7 @@ def mock_generator() -> Generator[ToolInvokeMessage]: assert isinstance(result[2].message, ToolInvokeMessage.TextMessage) assert result[2].message.text == "World" - def test_merge_blob_chunks_file_too_large(self): + def test_merge_blob_chunks_file_too_large(self) -> None: """Test that error is raised when file exceeds max size.""" def mock_generator() -> Generator[ToolInvokeMessage]: @@ -131,7 +131,7 @@ def mock_generator() -> Generator[ToolInvokeMessage]: list(merge_blob_chunks(mock_generator(), max_file_size=1000)) assert "File is too large" in str(exc_info.value) - def test_merge_blob_chunks_chunk_too_large(self): + def test_merge_blob_chunks_chunk_too_large(self) -> None: """Test that error is raised when chunk exceeds max chunk size.""" def mock_generator() -> Generator[ToolInvokeMessage]: @@ -147,7 +147,7 @@ def mock_generator() -> Generator[ToolInvokeMessage]: list(merge_blob_chunks(mock_generator(), max_chunk_size=8192)) assert "File chunk is too large" in str(exc_info.value) - def test_merge_blob_chunks_with_agent_invoke_message(self): + def test_merge_blob_chunks_with_agent_invoke_message(self) -> None: """Test that merge_blob_chunks works with AgentInvokeMessage.""" def mock_generator() -> Generator[AgentInvokeMessage]: @@ -171,7 +171,7 @@ def mock_generator() -> Generator[AgentInvokeMessage]: assert isinstance(result[0], AgentInvokeMessage) assert result[0].type == AgentInvokeMessage.MessageType.BLOB - def test_merge_blob_chunks_preserves_meta(self): + def test_merge_blob_chunks_preserves_meta(self) -> None: """Test that meta information is preserved in merged messages.""" def mock_generator() -> Generator[ToolInvokeMessage]: @@ -187,7 +187,7 @@ def mock_generator() -> Generator[ToolInvokeMessage]: assert len(result) == 1 assert result[0].meta == {"key": "value"} - def test_merge_blob_chunks_custom_limits(self): + def test_merge_blob_chunks_custom_limits(self) -> None: """Test merge_blob_chunks with custom size limits.""" def mock_generator() -> Generator[ToolInvokeMessage]: @@ -221,7 +221,7 @@ def mock_generator2() -> Generator[ToolInvokeMessage]: with pytest.raises(ValueError, match="File is too large which reached the limit of 0\\.000286102294921875MB"): list(merge_blob_chunks(mock_generator2(), max_file_size=300)) - def test_merge_blob_chunks_data_integrity(self): + def test_merge_blob_chunks_data_integrity(self) -> None: """Test that merged chunks exactly match the original data.""" # Create original data original_data = b"This is a test message that will be split into chunks for testing purposes." @@ -255,7 +255,7 @@ def mock_generator() -> Generator[ToolInvokeMessage]: # Verify the merged data exactly matches the original assert result[0].message.blob == original_data - def test_merge_blob_chunks_empty_chunk(self): + def test_merge_blob_chunks_empty_chunk(self) -> None: """Test handling of empty chunks.""" def mock_generator() -> Generator[ToolInvokeMessage]: @@ -288,7 +288,7 @@ def mock_generator() -> Generator[ToolInvokeMessage]: # The final blob should contain "Hello" followed by "World" assert result[0].message.blob[:10] == b"HelloWorld" - def test_merge_blob_chunks_single_chunk_file(self): + def test_merge_blob_chunks_single_chunk_file(self) -> None: """Test file that arrives as a single complete chunk.""" def mock_generator() -> Generator[ToolInvokeMessage]: @@ -310,7 +310,7 @@ def mock_generator() -> Generator[ToolInvokeMessage]: assert isinstance(result[0].message, ToolInvokeMessage.BlobMessage) assert result[0].message.blob == b"Single Data" - def test_merge_blob_chunks_concurrent_files(self): + def test_merge_blob_chunks_concurrent_files(self) -> None: """Test that chunks from different files are properly separated.""" def mock_generator() -> Generator[ToolInvokeMessage]: @@ -358,7 +358,7 @@ def mock_generator() -> Generator[ToolInvokeMessage]: expected = {b"First file content", b"Second file data", b"Third file"} assert blobs == expected - def test_merge_blob_chunks_exact_buffer_size(self): + def test_merge_blob_chunks_exact_buffer_size(self) -> None: """Test that data fitting exactly in buffer works correctly.""" def mock_generator() -> Generator[ToolInvokeMessage]: @@ -392,7 +392,7 @@ def mock_generator() -> Generator[ToolInvokeMessage]: assert len(result[0].message.blob) == 100 assert result[0].message.blob == b"X" * 100 - def test_merge_blob_chunks_large_file_simulation(self): + def test_merge_blob_chunks_large_file_simulation(self) -> None: """Test handling of a large file split into many chunks.""" def mock_generator() -> Generator[ToolInvokeMessage]: @@ -433,7 +433,7 @@ def mock_generator() -> Generator[ToolInvokeMessage]: chunk = merged_data[chunk_start:chunk_end] assert all(b == expected_byte for b in chunk), f"Chunk {i} has incorrect data" - def test_merge_blob_chunks_sequential_order_required(self): + def test_merge_blob_chunks_sequential_order_required(self) -> None: """ Test note: The current implementation assumes chunks arrive in sequential order. Out-of-order chunks would need additional logic to handle properly. @@ -465,7 +465,7 @@ def mock_generator() -> Generator[ToolInvokeMessage]: class TestConverter: - def test_convert_parameters_to_plugin_format_with_single_file_and_selector(self): + def test_convert_parameters_to_plugin_format_with_single_file_and_selector(self) -> None: file_param = File( tenant_id="tenant-1", file_type=FileType.IMAGE, @@ -498,7 +498,7 @@ def test_convert_parameters_to_plugin_format_with_single_file_and_selector(self) assert converted["selector"]["provider_id"] == "org/plugin/provider" assert converted["plain"] == 123 - def test_convert_parameters_to_plugin_format_with_lists_and_passthrough_values(self): + def test_convert_parameters_to_plugin_format_with_lists_and_passthrough_values(self) -> None: file_one = File( tenant_id="tenant-1", file_type=FileType.DOCUMENT, @@ -530,10 +530,11 @@ def test_convert_parameters_to_plugin_format_with_lists_and_passthrough_values(s tool_parameters={}, ) + empty_list: list[object] = [] params = { "files": [file_one, file_two], "selectors": [selector_one, selector_two], - "empty_list": [], + "empty_list": empty_list, "mixed_list": [file_one, "raw"], "none_value": None, } @@ -549,7 +550,7 @@ def test_convert_parameters_to_plugin_format_with_lists_and_passthrough_values(s assert converted["mixed_list"] == [file_one, "raw"] assert converted["none_value"] is None - def test_merge_blob_chunks_rejects_declared_size_above_limit(self): + def test_merge_blob_chunks_rejects_declared_size_above_limit(self) -> None: """A declared total_length above the limit is rejected before allocation.""" def mock_generator() -> Generator[ToolInvokeMessage]: @@ -563,7 +564,7 @@ def mock_generator() -> Generator[ToolInvokeMessage]: with pytest.raises(ValueError, match="File is too large"): list(merge_blob_chunks(mock_generator(), max_file_size=1024)) - def test_merge_blob_chunks_rejects_absurd_declared_size(self): + def test_merge_blob_chunks_rejects_absurd_declared_size(self) -> None: """An absurd declared total_length raises ValueError, not MemoryError/OverflowError.""" def mock_generator() -> Generator[ToolInvokeMessage]: diff --git a/api/tests/unit_tests/pyrefly.toml b/api/tests/unit_tests/pyrefly.toml index 41c1d421c12a95..de7bb087fae257 100644 --- a/api/tests/unit_tests/pyrefly.toml +++ b/api/tests/unit_tests/pyrefly.toml @@ -187,11 +187,9 @@ project-excludes = [ "core/plugin/impl/test_oauth.py", "core/plugin/impl/test_trigger.py", "core/plugin/backwards_invocation/test_app.py", - "core/plugin/backwards_invocation/test_model.py", "core/plugin/impl/test_model_runtime.py", "core/plugin/entities/test_plugin_daemon.py", "core/plugin/impl/test_plugin.py", - "core/plugin/utils/test_chunk_merger.py", "core/prompt/test_advanced_prompt_transform.py", "core/prompt/utils/test_extract_thread_messages.py", "core/rag/cleaner/test_clean_processor.py",