chiliu commited on
Commit
d829d76
·
unverified ·
1 Parent(s): 54ec17c

Add comprehensive tests for utilities.components module (#1395)

Browse files
Files changed (1) hide show
  1. tests/utilities/test_components.py +391 -0
tests/utilities/test_components.py ADDED
@@ -0,0 +1,391 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for fastmcp.utilities.components module."""
2
+
3
+ import pytest
4
+ from pydantic import ValidationError
5
+
6
+ from fastmcp.utilities.components import (
7
+ FastMCPComponent,
8
+ FastMCPMeta,
9
+ MirroredComponent,
10
+ _convert_set_default_none,
11
+ )
12
+
13
+
14
+ class TestConvertSetDefaultNone:
15
+ """Tests for the _convert_set_default_none helper function."""
16
+
17
+ def test_none_returns_empty_set(self):
18
+ """Test that None returns an empty set."""
19
+ result = _convert_set_default_none(None)
20
+ assert result == set()
21
+
22
+ def test_set_returns_same_set(self):
23
+ """Test that a set returns the same set."""
24
+ test_set = {"tag1", "tag2"}
25
+ result = _convert_set_default_none(test_set)
26
+ assert result == test_set
27
+
28
+ def test_list_converts_to_set(self):
29
+ """Test that a list converts to a set."""
30
+ test_list = ["tag1", "tag2", "tag1"] # Duplicate to test deduplication
31
+ result = _convert_set_default_none(test_list)
32
+ assert result == {"tag1", "tag2"}
33
+
34
+ def test_tuple_converts_to_set(self):
35
+ """Test that a tuple converts to a set."""
36
+ test_tuple = ("tag1", "tag2")
37
+ result = _convert_set_default_none(test_tuple)
38
+ assert result == {"tag1", "tag2"}
39
+
40
+
41
+ class TestFastMCPComponent:
42
+ """Tests for the FastMCPComponent class."""
43
+
44
+ @pytest.fixture
45
+ def basic_component(self):
46
+ """Create a basic component for testing."""
47
+ return FastMCPComponent(
48
+ name="test_component",
49
+ title="Test Component",
50
+ description="A test component",
51
+ tags=["test", "component"],
52
+ )
53
+
54
+ def test_initialization_with_minimal_params(self):
55
+ """Test component initialization with minimal parameters."""
56
+ component = FastMCPComponent(name="minimal")
57
+ assert component.name == "minimal"
58
+ assert component.title is None
59
+ assert component.description is None
60
+ assert component.tags == set()
61
+ assert component.meta is None
62
+ assert component.enabled is True
63
+
64
+ def test_initialization_with_all_params(self):
65
+ """Test component initialization with all parameters."""
66
+ meta = {"custom": "value"}
67
+ component = FastMCPComponent(
68
+ name="full",
69
+ title="Full Component",
70
+ description="A fully configured component",
71
+ tags=["tag1", "tag2"],
72
+ meta=meta,
73
+ enabled=False,
74
+ )
75
+ assert component.name == "full"
76
+ assert component.title == "Full Component"
77
+ assert component.description == "A fully configured component"
78
+ assert component.tags == {"tag1", "tag2"}
79
+ assert component.meta == meta
80
+ assert component.enabled is False
81
+
82
+ def test_key_property_without_custom_key(self, basic_component):
83
+ """Test that key property returns name when no custom key is set."""
84
+ assert basic_component.key == "test_component"
85
+
86
+ def test_key_property_with_custom_key(self):
87
+ """Test that key property returns custom key when set."""
88
+ component = FastMCPComponent(name="test", key="custom_key")
89
+ assert component.key == "custom_key"
90
+ assert component.name == "test"
91
+
92
+ def test_get_meta_without_fastmcp_meta(self, basic_component):
93
+ """Test get_meta without including fastmcp meta."""
94
+ basic_component.meta = {"custom": "data"}
95
+ result = basic_component.get_meta(include_fastmcp_meta=False)
96
+ assert result == {"custom": "data"}
97
+ assert "_fastmcp" not in result
98
+
99
+ def test_get_meta_with_fastmcp_meta(self, basic_component):
100
+ """Test get_meta including fastmcp meta."""
101
+ basic_component.meta = {"custom": "data"}
102
+ basic_component.tags = {"tag2", "tag1"} # Unordered to test sorting
103
+ result = basic_component.get_meta(include_fastmcp_meta=True)
104
+ assert result["custom"] == "data"
105
+ assert "_fastmcp" in result
106
+ assert result["_fastmcp"]["tags"] == ["tag1", "tag2"] # Should be sorted
107
+
108
+ def test_get_meta_preserves_existing_fastmcp_meta(self):
109
+ """Test that get_meta preserves existing _fastmcp meta."""
110
+ component = FastMCPComponent(
111
+ name="test",
112
+ meta={"_fastmcp": {"existing": "value"}},
113
+ tags=["new_tag"],
114
+ )
115
+ result = component.get_meta(include_fastmcp_meta=True)
116
+ assert result is not None
117
+ assert result["_fastmcp"]["existing"] == "value"
118
+ assert result["_fastmcp"]["tags"] == ["new_tag"]
119
+
120
+ def test_get_meta_returns_none_when_empty(self):
121
+ """Test that get_meta returns None when no meta and fastmcp_meta is False."""
122
+ component = FastMCPComponent(name="test")
123
+ result = component.get_meta(include_fastmcp_meta=False)
124
+ assert result is None
125
+
126
+ def test_with_key_creates_copy_with_new_key(self, basic_component):
127
+ """Test that with_key creates a copy with a new key."""
128
+ new_component = basic_component.with_key("new_key")
129
+ assert new_component.key == "new_key"
130
+ assert new_component.name == basic_component.name
131
+ assert new_component is not basic_component # Should be a copy
132
+ assert basic_component.key == "test_component" # Original unchanged
133
+
134
+ def test_equality_same_components(self):
135
+ """Test that identical components are equal."""
136
+ comp1 = FastMCPComponent(name="test", description="desc")
137
+ comp2 = FastMCPComponent(name="test", description="desc")
138
+ assert comp1 == comp2
139
+
140
+ def test_equality_different_components(self):
141
+ """Test that different components are not equal."""
142
+ comp1 = FastMCPComponent(name="test1")
143
+ comp2 = FastMCPComponent(name="test2")
144
+ assert comp1 != comp2
145
+
146
+ def test_equality_different_types(self, basic_component):
147
+ """Test that component is not equal to other types."""
148
+ assert basic_component != "not a component"
149
+ assert basic_component != 123
150
+ assert basic_component is not None
151
+
152
+ def test_repr(self, basic_component):
153
+ """Test string representation of component."""
154
+ repr_str = repr(basic_component)
155
+ assert "FastMCPComponent" in repr_str
156
+ assert "name='test_component'" in repr_str
157
+ assert "title='Test Component'" in repr_str
158
+ assert "description='A test component'" in repr_str
159
+
160
+ def test_enable_method(self):
161
+ """Test enable method."""
162
+ component = FastMCPComponent(name="test", enabled=False)
163
+ assert component.enabled is False
164
+ component.enable()
165
+ assert component.enabled is True
166
+
167
+ def test_disable_method(self):
168
+ """Test disable method."""
169
+ component = FastMCPComponent(name="test", enabled=True)
170
+ assert component.enabled is True
171
+ component.disable()
172
+ assert component.enabled is False
173
+
174
+ def test_copy_method(self, basic_component):
175
+ """Test copy method creates an independent copy."""
176
+ copy = basic_component.copy()
177
+ assert copy == basic_component
178
+ assert copy is not basic_component
179
+
180
+ # Modify copy and ensure original is unchanged
181
+ copy.name = "modified"
182
+ assert basic_component.name == "test_component"
183
+
184
+ def test_tags_deduplication(self):
185
+ """Test that tags are deduplicated."""
186
+ component = FastMCPComponent(
187
+ name="test",
188
+ tags=["tag1", "tag2", "tag1", "tag2"],
189
+ )
190
+ assert component.tags == {"tag1", "tag2"}
191
+
192
+ def test_validation_error_for_invalid_data(self):
193
+ """Test that validation errors are raised for invalid data."""
194
+ with pytest.raises(ValidationError):
195
+ FastMCPComponent() # Missing required name field
196
+
197
+ def test_extra_fields_forbidden(self):
198
+ """Test that extra fields are not allowed."""
199
+ with pytest.raises(ValidationError) as exc_info:
200
+ FastMCPComponent(name="test", unknown_field="value")
201
+ assert "Extra inputs are not permitted" in str(exc_info.value)
202
+
203
+
204
+ class TestMirroredComponent:
205
+ """Tests for the MirroredComponent class."""
206
+
207
+ @pytest.fixture
208
+ def mirrored_component(self):
209
+ """Create a mirrored component for testing."""
210
+ return MirroredComponent(
211
+ name="mirrored",
212
+ description="A mirrored component",
213
+ _mirrored=True,
214
+ )
215
+
216
+ @pytest.fixture
217
+ def non_mirrored_component(self):
218
+ """Create a non-mirrored component for testing."""
219
+ return MirroredComponent(
220
+ name="local",
221
+ description="A local component",
222
+ _mirrored=False,
223
+ )
224
+
225
+ def test_initialization_mirrored(self, mirrored_component):
226
+ """Test initialization of a mirrored component."""
227
+ assert mirrored_component.name == "mirrored"
228
+ assert mirrored_component._mirrored is True
229
+
230
+ def test_initialization_non_mirrored(self, non_mirrored_component):
231
+ """Test initialization of a non-mirrored component."""
232
+ assert non_mirrored_component.name == "local"
233
+ assert non_mirrored_component._mirrored is False
234
+
235
+ def test_enable_raises_error_when_mirrored(self, mirrored_component):
236
+ """Test that enable raises an error for mirrored components."""
237
+ with pytest.raises(RuntimeError) as exc_info:
238
+ mirrored_component.enable()
239
+ assert "Cannot enable mirrored component" in str(exc_info.value)
240
+ assert "mirrored" in str(exc_info.value)
241
+ assert ".copy()" in str(exc_info.value)
242
+
243
+ def test_disable_raises_error_when_mirrored(self, mirrored_component):
244
+ """Test that disable raises an error for mirrored components."""
245
+ with pytest.raises(RuntimeError) as exc_info:
246
+ mirrored_component.disable()
247
+ assert "Cannot disable mirrored component" in str(exc_info.value)
248
+ assert "mirrored" in str(exc_info.value)
249
+ assert ".copy()" in str(exc_info.value)
250
+
251
+ def test_enable_works_when_not_mirrored(self, non_mirrored_component):
252
+ """Test that enable works for non-mirrored components."""
253
+ non_mirrored_component.enabled = False
254
+ non_mirrored_component.enable()
255
+ assert non_mirrored_component.enabled is True
256
+
257
+ def test_disable_works_when_not_mirrored(self, non_mirrored_component):
258
+ """Test that disable works for non-mirrored components."""
259
+ non_mirrored_component.enabled = True
260
+ non_mirrored_component.disable()
261
+ assert non_mirrored_component.enabled is False
262
+
263
+ def test_copy_removes_mirrored_flag(self, mirrored_component):
264
+ """Test that copy creates a non-mirrored version."""
265
+ copy = mirrored_component.copy()
266
+ assert copy._mirrored is False
267
+ assert copy.name == mirrored_component.name
268
+ assert copy is not mirrored_component
269
+
270
+ # Should be able to enable/disable the copy
271
+ copy.enable()
272
+ copy.disable()
273
+ assert copy.enabled is False
274
+
275
+ def test_copy_preserves_non_mirrored_state(self, non_mirrored_component):
276
+ """Test that copy preserves non-mirrored state."""
277
+ copy = non_mirrored_component.copy()
278
+ assert copy._mirrored is False
279
+ assert copy == non_mirrored_component
280
+ assert copy is not non_mirrored_component
281
+
282
+ def test_inheritance_from_fastmcp_component(self):
283
+ """Test that MirroredComponent inherits from FastMCPComponent."""
284
+ component = MirroredComponent(name="test")
285
+ assert isinstance(component, FastMCPComponent)
286
+ assert isinstance(component, MirroredComponent)
287
+
288
+ def test_all_fastmcp_component_features_work(self, mirrored_component):
289
+ """Test that all FastMCPComponent features work except enable/disable."""
290
+ # Test key property
291
+ assert mirrored_component.key == "mirrored"
292
+
293
+ # Test with_key
294
+ with_key = mirrored_component.with_key("new_key")
295
+ assert with_key.key == "new_key"
296
+
297
+ # Test get_meta
298
+ mirrored_component.tags = {"tag1"}
299
+ meta = mirrored_component.get_meta(include_fastmcp_meta=True)
300
+ assert meta["_fastmcp"]["tags"] == ["tag1"]
301
+
302
+ # Test repr
303
+ repr_str = repr(mirrored_component)
304
+ assert "MirroredComponent" in repr_str
305
+
306
+
307
+ class TestFastMCPMeta:
308
+ """Tests for the FastMCPMeta TypedDict."""
309
+
310
+ def test_fastmcp_meta_structure(self):
311
+ """Test that FastMCPMeta has the expected structure."""
312
+ meta: FastMCPMeta = {"tags": ["tag1", "tag2"]}
313
+ assert meta["tags"] == ["tag1", "tag2"]
314
+
315
+ def test_fastmcp_meta_optional_fields(self):
316
+ """Test that FastMCPMeta fields are optional."""
317
+ meta: FastMCPMeta = {}
318
+ assert "tags" not in meta # Should be optional
319
+
320
+
321
+ class TestEdgeCasesAndIntegration:
322
+ """Tests for edge cases and integration scenarios."""
323
+
324
+ def test_empty_tags_conversion(self):
325
+ """Test that empty tags are handled correctly."""
326
+ component = FastMCPComponent(name="test", tags=[])
327
+ assert component.tags == set()
328
+
329
+ def test_tags_with_none_values(self):
330
+ """Test tags behavior with various input types."""
331
+ # Test with None (through validator)
332
+ component = FastMCPComponent(name="test")
333
+ assert component.tags == set()
334
+
335
+ def test_meta_mutation_affects_original(self):
336
+ """Test that get_meta returns a reference to the original meta."""
337
+ component = FastMCPComponent(name="test", meta={"key": "value"})
338
+ meta = component.get_meta(include_fastmcp_meta=False)
339
+ assert meta is not None
340
+ meta["key"] = "modified"
341
+ assert component.meta is not None
342
+ assert component.meta["key"] == "modified" # Original is modified
343
+
344
+ # This is the actual behavior - get_meta returns a reference
345
+
346
+ def test_component_with_complex_meta(self):
347
+ """Test component with nested meta structures."""
348
+ complex_meta = {
349
+ "nested": {"level1": {"level2": "value"}},
350
+ "list": [1, 2, 3],
351
+ "bool": True,
352
+ }
353
+ component = FastMCPComponent(name="test", meta=complex_meta)
354
+ assert component.meta == complex_meta
355
+
356
+ def test_with_key_preserves_all_attributes(self):
357
+ """Test that with_key preserves all component attributes."""
358
+ component = FastMCPComponent(
359
+ name="test",
360
+ title="Title",
361
+ description="Description",
362
+ tags=["tag1", "tag2"],
363
+ meta={"key": "value"},
364
+ enabled=False,
365
+ )
366
+ new_component = component.with_key("new_key")
367
+
368
+ assert new_component.name == component.name
369
+ assert new_component.title == component.title
370
+ assert new_component.description == component.description
371
+ assert new_component.tags == component.tags
372
+ assert new_component.meta == component.meta
373
+ assert new_component.enabled == component.enabled
374
+ assert new_component.key == "new_key"
375
+
376
+ def test_mirrored_component_copy_chain(self):
377
+ """Test creating copies of copies for mirrored components."""
378
+ original = MirroredComponent(name="original", _mirrored=True)
379
+ copy1 = original.copy()
380
+ copy2 = copy1.copy()
381
+
382
+ assert original._mirrored is True
383
+ assert copy1._mirrored is False
384
+ assert copy2._mirrored is False
385
+
386
+ # All copies should be independent
387
+ copy1.name = "copy1"
388
+ copy2.name = "copy2"
389
+ assert original.name == "original"
390
+ assert copy1.name == "copy1"
391
+ assert copy2.name == "copy2"