"""Tests for fastmcp.utilities.components module.""" import pytest from pydantic import ValidationError from fastmcp.utilities.components import ( FastMCPComponent, FastMCPMeta, MirroredComponent, _convert_set_default_none, ) class TestConvertSetDefaultNone: """Tests for the _convert_set_default_none helper function.""" def test_none_returns_empty_set(self): """Test that None returns an empty set.""" result = _convert_set_default_none(None) assert result == set() def test_set_returns_same_set(self): """Test that a set returns the same set.""" test_set = {"tag1", "tag2"} result = _convert_set_default_none(test_set) assert result == test_set def test_list_converts_to_set(self): """Test that a list converts to a set.""" test_list = ["tag1", "tag2", "tag1"] # Duplicate to test deduplication result = _convert_set_default_none(test_list) assert result == {"tag1", "tag2"} def test_tuple_converts_to_set(self): """Test that a tuple converts to a set.""" test_tuple = ("tag1", "tag2") result = _convert_set_default_none(test_tuple) assert result == {"tag1", "tag2"} class TestFastMCPComponent: """Tests for the FastMCPComponent class.""" @pytest.fixture def basic_component(self): """Create a basic component for testing.""" return FastMCPComponent( name="test_component", title="Test Component", description="A test component", tags={"test", "component"}, ) def test_initialization_with_minimal_params(self): """Test component initialization with minimal parameters.""" component = FastMCPComponent(name="minimal") assert component.name == "minimal" assert component.title is None assert component.description is None assert component.tags == set() assert component.meta is None assert component.enabled is True def test_initialization_with_all_params(self): """Test component initialization with all parameters.""" meta = {"custom": "value"} component = FastMCPComponent( name="full", title="Full Component", description="A fully configured component", tags={"tag1", "tag2"}, meta=meta, enabled=False, ) assert component.name == "full" assert component.title == "Full Component" assert component.description == "A fully configured component" assert component.tags == {"tag1", "tag2"} assert component.meta == meta assert component.enabled is False def test_key_property_without_custom_key(self, basic_component): """Test that key property returns name when no custom key is set.""" assert basic_component.key == "test_component" def test_get_meta_without_fastmcp_meta(self, basic_component): """Test get_meta without including fastmcp meta.""" basic_component.meta = {"custom": "data"} result = basic_component.get_meta(include_fastmcp_meta=False) assert result == {"custom": "data"} assert "_fastmcp" not in result def test_get_meta_with_fastmcp_meta(self, basic_component): """Test get_meta including fastmcp meta.""" basic_component.meta = {"custom": "data"} basic_component.tags = {"tag2", "tag1"} # Unordered to test sorting result = basic_component.get_meta(include_fastmcp_meta=True) assert result["custom"] == "data" assert "_fastmcp" in result assert result["_fastmcp"]["tags"] == ["tag1", "tag2"] # Should be sorted def test_get_meta_preserves_existing_fastmcp_meta(self): """Test that get_meta preserves existing _fastmcp meta.""" component = FastMCPComponent( name="test", meta={"_fastmcp": {"existing": "value"}}, tags={"new_tag"}, ) result = component.get_meta(include_fastmcp_meta=True) assert result is not None assert result["_fastmcp"]["existing"] == "value" assert result["_fastmcp"]["tags"] == ["new_tag"] def test_get_meta_returns_none_when_empty(self): """Test that get_meta returns None when no meta and fastmcp_meta is False.""" component = FastMCPComponent(name="test") result = component.get_meta(include_fastmcp_meta=False) assert result is None def test_equality_same_components(self): """Test that identical components are equal.""" comp1 = FastMCPComponent(name="test", description="desc") comp2 = FastMCPComponent(name="test", description="desc") assert comp1 == comp2 def test_equality_different_components(self): """Test that different components are not equal.""" comp1 = FastMCPComponent(name="test1") comp2 = FastMCPComponent(name="test2") assert comp1 != comp2 def test_equality_different_types(self, basic_component): """Test that component is not equal to other types.""" assert basic_component != "not a component" assert basic_component != 123 assert basic_component is not None def test_repr(self, basic_component): """Test string representation of component.""" repr_str = repr(basic_component) assert "FastMCPComponent" in repr_str assert "name='test_component'" in repr_str assert "title='Test Component'" in repr_str assert "description='A test component'" in repr_str def test_enable_method(self): """Test enable method.""" component = FastMCPComponent(name="test", enabled=False) assert component.enabled is False component.enable() assert component.enabled is True def test_disable_method(self): """Test disable method.""" component = FastMCPComponent(name="test", enabled=True) assert component.enabled is True component.disable() assert component.enabled is False def test_copy_method(self, basic_component): """Test copy method creates an independent copy.""" copy = basic_component.copy() assert copy == basic_component assert copy is not basic_component # Modify copy and ensure original is unchanged copy.name = "modified" assert basic_component.name == "test_component" def test_tags_deduplication(self): """Test that tags are deduplicated when passed as a sequence.""" component = FastMCPComponent( name="test", tags=["tag1", "tag2", "tag1", "tag2"], # type: ignore[arg-type] ) assert component.tags == {"tag1", "tag2"} def test_validation_error_for_invalid_data(self): """Test that validation errors are raised for invalid data.""" with pytest.raises(ValidationError): FastMCPComponent() # type: ignore[call-arg] def test_extra_fields_forbidden(self): """Test that extra fields are not allowed.""" with pytest.raises(ValidationError) as exc_info: FastMCPComponent(name="test", unknown_field="value") assert "Extra inputs are not permitted" in str(exc_info.value) class TestMirroredComponent: """Tests for the MirroredComponent class.""" @pytest.fixture def mirrored_component(self): """Create a mirrored component for testing.""" return MirroredComponent( name="mirrored", description="A mirrored component", _mirrored=True, ) @pytest.fixture def non_mirrored_component(self): """Create a non-mirrored component for testing.""" return MirroredComponent( name="local", description="A local component", _mirrored=False, ) def test_initialization_mirrored(self, mirrored_component): """Test initialization of a mirrored component.""" assert mirrored_component.name == "mirrored" assert mirrored_component._mirrored is True def test_initialization_non_mirrored(self, non_mirrored_component): """Test initialization of a non-mirrored component.""" assert non_mirrored_component.name == "local" assert non_mirrored_component._mirrored is False def test_enable_raises_error_when_mirrored(self, mirrored_component): """Test that enable raises an error for mirrored components.""" with pytest.raises(RuntimeError) as exc_info: mirrored_component.enable() assert "Cannot enable mirrored component" in str(exc_info.value) assert "mirrored" in str(exc_info.value) assert ".copy()" in str(exc_info.value) def test_disable_raises_error_when_mirrored(self, mirrored_component): """Test that disable raises an error for mirrored components.""" with pytest.raises(RuntimeError) as exc_info: mirrored_component.disable() assert "Cannot disable mirrored component" in str(exc_info.value) assert "mirrored" in str(exc_info.value) assert ".copy()" in str(exc_info.value) def test_enable_works_when_not_mirrored(self, non_mirrored_component): """Test that enable works for non-mirrored components.""" non_mirrored_component.enabled = False non_mirrored_component.enable() assert non_mirrored_component.enabled is True def test_disable_works_when_not_mirrored(self, non_mirrored_component): """Test that disable works for non-mirrored components.""" non_mirrored_component.enabled = True non_mirrored_component.disable() assert non_mirrored_component.enabled is False def test_copy_removes_mirrored_flag(self, mirrored_component): """Test that copy creates a non-mirrored version.""" copy = mirrored_component.copy() assert copy._mirrored is False assert copy.name == mirrored_component.name assert copy is not mirrored_component # Should be able to enable/disable the copy copy.enable() copy.disable() assert copy.enabled is False def test_copy_preserves_non_mirrored_state(self, non_mirrored_component): """Test that copy preserves non-mirrored state.""" copy = non_mirrored_component.copy() assert copy._mirrored is False assert copy == non_mirrored_component assert copy is not non_mirrored_component def test_inheritance_from_fastmcp_component(self): """Test that MirroredComponent inherits from FastMCPComponent.""" component = MirroredComponent(name="test") assert isinstance(component, FastMCPComponent) assert isinstance(component, MirroredComponent) def test_all_fastmcp_component_features_work(self, mirrored_component): """Test that all FastMCPComponent features work except enable/disable.""" # Test key property assert mirrored_component.key == "mirrored" # Test get_meta mirrored_component.tags = {"tag1"} meta = mirrored_component.get_meta(include_fastmcp_meta=True) assert meta["_fastmcp"]["tags"] == ["tag1"] # Test repr repr_str = repr(mirrored_component) assert "MirroredComponent" in repr_str class TestFastMCPMeta: """Tests for the FastMCPMeta TypedDict.""" def test_fastmcp_meta_structure(self): """Test that FastMCPMeta has the expected structure.""" meta: FastMCPMeta = {"tags": ["tag1", "tag2"]} assert meta["tags"] == ["tag1", "tag2"] def test_fastmcp_meta_optional_fields(self): """Test that FastMCPMeta fields are optional.""" meta: FastMCPMeta = {} assert "tags" not in meta # Should be optional class TestEdgeCasesAndIntegration: """Tests for edge cases and integration scenarios.""" def test_empty_tags_conversion(self): """Test that empty tags are handled correctly.""" component = FastMCPComponent(name="test", tags=set()) assert component.tags == set() def test_tags_with_none_values(self): """Test tags behavior with various input types.""" # Test with None (through validator) component = FastMCPComponent(name="test") assert component.tags == set() def test_meta_mutation_affects_original(self): """Test that get_meta returns a reference to the original meta.""" component = FastMCPComponent(name="test", meta={"key": "value"}) meta = component.get_meta(include_fastmcp_meta=False) assert meta is not None meta["key"] = "modified" assert component.meta is not None assert component.meta["key"] == "modified" # Original is modified # This is the actual behavior - get_meta returns a reference def test_component_with_complex_meta(self): """Test component with nested meta structures.""" complex_meta = { "nested": {"level1": {"level2": "value"}}, "list": [1, 2, 3], "bool": True, } component = FastMCPComponent(name="test", meta=complex_meta) assert component.meta == complex_meta def test_model_copy_preserves_all_attributes(self): """Test that model_copy preserves all component attributes.""" component = FastMCPComponent( name="test", title="Title", description="Description", tags={"tag1", "tag2"}, meta={"key": "value"}, enabled=False, ) new_component = component.model_copy() assert new_component.name == component.name assert new_component.title == component.title assert new_component.description == component.description assert new_component.tags == component.tags assert new_component.meta == component.meta assert new_component.enabled == component.enabled assert new_component.key == component.key def test_mirrored_component_copy_chain(self): """Test creating copies of copies for mirrored components.""" original = MirroredComponent(name="original", _mirrored=True) copy1 = original.copy() copy2 = copy1.copy() assert original._mirrored is True assert copy1._mirrored is False assert copy2._mirrored is False # All copies should be independent copy1.name = "copy1" copy2.name = "copy2" assert original.name == "original" assert copy1.name == "copy1" assert copy2.name == "copy2" def test_model_copy_with_update(self): """Test that model_copy works with update dict.""" component = FastMCPComponent( name="test", title="Original Title", description="Original Description", tags={"tag1"}, enabled=True, ) # Test with update (including name which affects .key) updated_component = component.model_copy( update={ "name": "new_name", "title": "New Title", "description": "New Description", }, ) assert updated_component.name == "new_name" # Updated assert updated_component.title == "New Title" # Updated assert updated_component.description == "New Description" # Updated assert updated_component.tags == {"tag1"} # Not in update, unchanged assert updated_component.enabled is True # Not in update, unchanged assert updated_component.key == "new_name" # .key is computed from name # Original should be unchanged assert component.name == "test" assert component.title == "Original Title" assert component.description == "Original Description" assert component.key == "test" # Uses name as key def test_model_copy_deep_parameter(self): """Test that model_copy respects the deep parameter.""" nested_dict = {"nested": {"value": 1}} component = FastMCPComponent(name="test", meta=nested_dict) # Shallow copy (default) shallow_copy = component.model_copy() assert shallow_copy.meta is not None assert component.meta is not None shallow_copy.meta["nested"]["value"] = 2 assert component.meta["nested"]["value"] == 2 # Original affected # Deep copy component.meta["nested"]["value"] = 1 # Reset deep_copy = component.model_copy(deep=True) assert deep_copy.meta is not None deep_copy.meta["nested"]["value"] = 3 assert component.meta["nested"]["value"] == 1 # Original unaffected