kiln-ai 0.20.1__py3-none-any.whl → 0.22.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of kiln-ai might be problematic. Click here for more details.
- kiln_ai/adapters/__init__.py +6 -0
- kiln_ai/adapters/adapter_registry.py +43 -226
- kiln_ai/adapters/chunkers/__init__.py +13 -0
- kiln_ai/adapters/chunkers/base_chunker.py +42 -0
- kiln_ai/adapters/chunkers/chunker_registry.py +16 -0
- kiln_ai/adapters/chunkers/fixed_window_chunker.py +39 -0
- kiln_ai/adapters/chunkers/helpers.py +23 -0
- kiln_ai/adapters/chunkers/test_base_chunker.py +63 -0
- kiln_ai/adapters/chunkers/test_chunker_registry.py +28 -0
- kiln_ai/adapters/chunkers/test_fixed_window_chunker.py +346 -0
- kiln_ai/adapters/chunkers/test_helpers.py +75 -0
- kiln_ai/adapters/data_gen/test_data_gen_task.py +9 -3
- kiln_ai/adapters/embedding/__init__.py +0 -0
- kiln_ai/adapters/embedding/base_embedding_adapter.py +44 -0
- kiln_ai/adapters/embedding/embedding_registry.py +32 -0
- kiln_ai/adapters/embedding/litellm_embedding_adapter.py +199 -0
- kiln_ai/adapters/embedding/test_base_embedding_adapter.py +283 -0
- kiln_ai/adapters/embedding/test_embedding_registry.py +166 -0
- kiln_ai/adapters/embedding/test_litellm_embedding_adapter.py +1149 -0
- kiln_ai/adapters/eval/eval_runner.py +6 -2
- kiln_ai/adapters/eval/test_base_eval.py +1 -3
- kiln_ai/adapters/eval/test_g_eval.py +1 -1
- kiln_ai/adapters/extractors/__init__.py +18 -0
- kiln_ai/adapters/extractors/base_extractor.py +72 -0
- kiln_ai/adapters/extractors/encoding.py +20 -0
- kiln_ai/adapters/extractors/extractor_registry.py +44 -0
- kiln_ai/adapters/extractors/extractor_runner.py +112 -0
- kiln_ai/adapters/extractors/litellm_extractor.py +406 -0
- kiln_ai/adapters/extractors/test_base_extractor.py +244 -0
- kiln_ai/adapters/extractors/test_encoding.py +54 -0
- kiln_ai/adapters/extractors/test_extractor_registry.py +181 -0
- kiln_ai/adapters/extractors/test_extractor_runner.py +181 -0
- kiln_ai/adapters/extractors/test_litellm_extractor.py +1290 -0
- kiln_ai/adapters/fine_tune/test_dataset_formatter.py +2 -2
- kiln_ai/adapters/fine_tune/test_fireworks_tinetune.py +2 -6
- kiln_ai/adapters/fine_tune/test_together_finetune.py +2 -6
- kiln_ai/adapters/ml_embedding_model_list.py +494 -0
- kiln_ai/adapters/ml_model_list.py +876 -18
- kiln_ai/adapters/model_adapters/litellm_adapter.py +40 -75
- kiln_ai/adapters/model_adapters/test_litellm_adapter.py +79 -1
- kiln_ai/adapters/model_adapters/test_litellm_adapter_tools.py +119 -5
- kiln_ai/adapters/model_adapters/test_saving_adapter_results.py +9 -3
- kiln_ai/adapters/model_adapters/test_structured_output.py +9 -10
- kiln_ai/adapters/ollama_tools.py +69 -12
- kiln_ai/adapters/provider_tools.py +190 -46
- kiln_ai/adapters/rag/deduplication.py +49 -0
- kiln_ai/adapters/rag/progress.py +252 -0
- kiln_ai/adapters/rag/rag_runners.py +844 -0
- kiln_ai/adapters/rag/test_deduplication.py +195 -0
- kiln_ai/adapters/rag/test_progress.py +785 -0
- kiln_ai/adapters/rag/test_rag_runners.py +2376 -0
- kiln_ai/adapters/remote_config.py +80 -8
- kiln_ai/adapters/test_adapter_registry.py +579 -86
- kiln_ai/adapters/test_ml_embedding_model_list.py +239 -0
- kiln_ai/adapters/test_ml_model_list.py +202 -0
- kiln_ai/adapters/test_ollama_tools.py +340 -1
- kiln_ai/adapters/test_prompt_builders.py +1 -1
- kiln_ai/adapters/test_provider_tools.py +199 -8
- kiln_ai/adapters/test_remote_config.py +551 -56
- kiln_ai/adapters/vector_store/__init__.py +1 -0
- kiln_ai/adapters/vector_store/base_vector_store_adapter.py +83 -0
- kiln_ai/adapters/vector_store/lancedb_adapter.py +389 -0
- kiln_ai/adapters/vector_store/test_base_vector_store.py +160 -0
- kiln_ai/adapters/vector_store/test_lancedb_adapter.py +1841 -0
- kiln_ai/adapters/vector_store/test_vector_store_registry.py +199 -0
- kiln_ai/adapters/vector_store/vector_store_registry.py +33 -0
- kiln_ai/datamodel/__init__.py +16 -13
- kiln_ai/datamodel/basemodel.py +201 -4
- kiln_ai/datamodel/chunk.py +158 -0
- kiln_ai/datamodel/datamodel_enums.py +27 -0
- kiln_ai/datamodel/embedding.py +64 -0
- kiln_ai/datamodel/external_tool_server.py +206 -54
- kiln_ai/datamodel/extraction.py +317 -0
- kiln_ai/datamodel/project.py +33 -1
- kiln_ai/datamodel/rag.py +79 -0
- kiln_ai/datamodel/task.py +5 -0
- kiln_ai/datamodel/task_output.py +41 -11
- kiln_ai/datamodel/test_attachment.py +649 -0
- kiln_ai/datamodel/test_basemodel.py +270 -14
- kiln_ai/datamodel/test_chunk_models.py +317 -0
- kiln_ai/datamodel/test_dataset_split.py +1 -1
- kiln_ai/datamodel/test_datasource.py +50 -0
- kiln_ai/datamodel/test_embedding_models.py +448 -0
- kiln_ai/datamodel/test_eval_model.py +6 -6
- kiln_ai/datamodel/test_external_tool_server.py +534 -152
- kiln_ai/datamodel/test_extraction_chunk.py +206 -0
- kiln_ai/datamodel/test_extraction_model.py +501 -0
- kiln_ai/datamodel/test_rag.py +641 -0
- kiln_ai/datamodel/test_task.py +35 -1
- kiln_ai/datamodel/test_tool_id.py +187 -1
- kiln_ai/datamodel/test_vector_store.py +320 -0
- kiln_ai/datamodel/tool_id.py +58 -0
- kiln_ai/datamodel/vector_store.py +141 -0
- kiln_ai/tools/base_tool.py +12 -3
- kiln_ai/tools/built_in_tools/math_tools.py +12 -4
- kiln_ai/tools/kiln_task_tool.py +158 -0
- kiln_ai/tools/mcp_server_tool.py +2 -2
- kiln_ai/tools/mcp_session_manager.py +51 -22
- kiln_ai/tools/rag_tools.py +164 -0
- kiln_ai/tools/test_kiln_task_tool.py +527 -0
- kiln_ai/tools/test_mcp_server_tool.py +4 -15
- kiln_ai/tools/test_mcp_session_manager.py +187 -227
- kiln_ai/tools/test_rag_tools.py +929 -0
- kiln_ai/tools/test_tool_registry.py +290 -7
- kiln_ai/tools/tool_registry.py +69 -16
- kiln_ai/utils/__init__.py +3 -0
- kiln_ai/utils/async_job_runner.py +62 -17
- kiln_ai/utils/config.py +2 -2
- kiln_ai/utils/env.py +15 -0
- kiln_ai/utils/filesystem.py +14 -0
- kiln_ai/utils/filesystem_cache.py +60 -0
- kiln_ai/utils/litellm.py +94 -0
- kiln_ai/utils/lock.py +100 -0
- kiln_ai/utils/mime_type.py +38 -0
- kiln_ai/utils/open_ai_types.py +19 -2
- kiln_ai/utils/pdf_utils.py +59 -0
- kiln_ai/utils/test_async_job_runner.py +151 -35
- kiln_ai/utils/test_env.py +142 -0
- kiln_ai/utils/test_filesystem_cache.py +316 -0
- kiln_ai/utils/test_litellm.py +206 -0
- kiln_ai/utils/test_lock.py +185 -0
- kiln_ai/utils/test_mime_type.py +66 -0
- kiln_ai/utils/test_open_ai_types.py +88 -12
- kiln_ai/utils/test_pdf_utils.py +86 -0
- kiln_ai/utils/test_uuid.py +111 -0
- kiln_ai/utils/test_validation.py +524 -0
- kiln_ai/utils/uuid.py +9 -0
- kiln_ai/utils/validation.py +90 -0
- {kiln_ai-0.20.1.dist-info → kiln_ai-0.22.0.dist-info}/METADATA +9 -1
- kiln_ai-0.22.0.dist-info/RECORD +213 -0
- kiln_ai-0.20.1.dist-info/RECORD +0 -138
- {kiln_ai-0.20.1.dist-info → kiln_ai-0.22.0.dist-info}/WHEEL +0 -0
- {kiln_ai-0.20.1.dist-info → kiln_ai-0.22.0.dist-info}/licenses/LICENSE.txt +0 -0
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
from typing import
|
|
1
|
+
from typing import Dict
|
|
2
2
|
from unittest.mock import Mock, patch
|
|
3
3
|
|
|
4
4
|
import pytest
|
|
5
5
|
|
|
6
|
-
from kiln_ai.datamodel.external_tool_server import
|
|
6
|
+
from kiln_ai.datamodel.external_tool_server import (
|
|
7
|
+
ExternalToolServer,
|
|
8
|
+
LocalServerProperties,
|
|
9
|
+
RemoteServerProperties,
|
|
10
|
+
ToolServerType,
|
|
11
|
+
)
|
|
7
12
|
from kiln_ai.utils.config import MCP_SECRETS_KEY, Config
|
|
8
13
|
from kiln_ai.utils.exhaustive_error import raise_exhaustive_enum_error
|
|
9
14
|
|
|
@@ -16,11 +21,12 @@ class TestExternalToolServer:
|
|
|
16
21
|
config_instance = Mock()
|
|
17
22
|
config_instance.get_value.return_value = {}
|
|
18
23
|
config_instance.update_settings = Mock()
|
|
24
|
+
config_instance.user_id = "test-user"
|
|
19
25
|
mock_shared.return_value = config_instance
|
|
20
26
|
yield config_instance
|
|
21
27
|
|
|
22
28
|
@pytest.fixture
|
|
23
|
-
def remote_mcp_base_props(self) ->
|
|
29
|
+
def remote_mcp_base_props(self) -> RemoteServerProperties:
|
|
24
30
|
"""Base properties for remote MCP server."""
|
|
25
31
|
return {
|
|
26
32
|
"server_url": "https://api.example.com/mcp",
|
|
@@ -28,7 +34,29 @@ class TestExternalToolServer:
|
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
@pytest.fixture
|
|
31
|
-
def
|
|
37
|
+
def sample_remote_mcp_secrets(self) -> Dict[str, str]:
|
|
38
|
+
return {
|
|
39
|
+
"Authorization": "Bearer token123",
|
|
40
|
+
"X-API-Key": "api-key-456",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@pytest.fixture
|
|
44
|
+
def remote_mcp_props_with_secrets(
|
|
45
|
+
self, remote_mcp_base_props, sample_remote_mcp_secrets
|
|
46
|
+
) -> RemoteServerProperties:
|
|
47
|
+
"""Properties for remote MCP server with secrets."""
|
|
48
|
+
base_headers = remote_mcp_base_props.get("headers", {})
|
|
49
|
+
return {
|
|
50
|
+
"server_url": remote_mcp_base_props["server_url"],
|
|
51
|
+
"headers": {
|
|
52
|
+
**base_headers,
|
|
53
|
+
**sample_remote_mcp_secrets,
|
|
54
|
+
},
|
|
55
|
+
"secret_header_keys": list(sample_remote_mcp_secrets.keys()),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@pytest.fixture
|
|
59
|
+
def local_mcp_base_props(self) -> LocalServerProperties:
|
|
32
60
|
"""Base properties for local MCP server."""
|
|
33
61
|
return {
|
|
34
62
|
"command": "python",
|
|
@@ -36,6 +64,17 @@ class TestExternalToolServer:
|
|
|
36
64
|
"env_vars": {},
|
|
37
65
|
}
|
|
38
66
|
|
|
67
|
+
@pytest.fixture
|
|
68
|
+
def kiln_task_base_props(self) -> dict:
|
|
69
|
+
"""Base properties for kiln task server."""
|
|
70
|
+
return {
|
|
71
|
+
"task_id": "task-123",
|
|
72
|
+
"run_config_id": "run-config-456",
|
|
73
|
+
"name": "test_task_tool",
|
|
74
|
+
"description": "A test task tool for unit testing",
|
|
75
|
+
"is_archived": False,
|
|
76
|
+
}
|
|
77
|
+
|
|
39
78
|
@pytest.mark.parametrize(
|
|
40
79
|
"server_type, properties",
|
|
41
80
|
[
|
|
@@ -54,42 +93,274 @@ class TestExternalToolServer:
|
|
|
54
93
|
"env_vars": {"API_KEY": "secret123"},
|
|
55
94
|
},
|
|
56
95
|
),
|
|
96
|
+
(
|
|
97
|
+
ToolServerType.kiln_task,
|
|
98
|
+
{
|
|
99
|
+
"task_id": "task-123",
|
|
100
|
+
"run_config_id": "run-config-456",
|
|
101
|
+
"name": "test_task_tool",
|
|
102
|
+
"description": "A test task tool for unit testing",
|
|
103
|
+
"is_archived": False,
|
|
104
|
+
},
|
|
105
|
+
),
|
|
57
106
|
],
|
|
58
107
|
)
|
|
59
|
-
def test_valid_server_creation(self,
|
|
108
|
+
def test_valid_server_creation(self, server_type, properties):
|
|
60
109
|
"""Test creating valid servers of both types."""
|
|
61
110
|
server = ExternalToolServer(
|
|
62
|
-
name="
|
|
111
|
+
name="test_server",
|
|
63
112
|
type=server_type,
|
|
64
113
|
description="Test server",
|
|
65
114
|
properties=properties,
|
|
66
115
|
)
|
|
67
116
|
|
|
68
|
-
assert server.name == "
|
|
117
|
+
assert server.name == "test_server"
|
|
69
118
|
assert server.type == server_type
|
|
70
119
|
assert server.description == "Test server"
|
|
71
120
|
assert server.properties == properties
|
|
72
121
|
|
|
122
|
+
@pytest.mark.parametrize(
|
|
123
|
+
"server_url, expected_error",
|
|
124
|
+
[
|
|
125
|
+
(123, "Server URL must be a string"),
|
|
126
|
+
(" http://test.com", "Server URL must not have leading whitespace"),
|
|
127
|
+
("ftp://test.com", "Server URL must start with http:// or https://"),
|
|
128
|
+
("test.com", "Server URL is not a valid URL"),
|
|
129
|
+
],
|
|
130
|
+
)
|
|
131
|
+
def test_validate_server_url_invalid(self, server_url, expected_error):
|
|
132
|
+
"""Test validate_server_url."""
|
|
133
|
+
with pytest.raises(ValueError, match=expected_error):
|
|
134
|
+
ExternalToolServer(
|
|
135
|
+
name="test-server",
|
|
136
|
+
type=ToolServerType.remote_mcp,
|
|
137
|
+
properties={"server_url": server_url},
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
@pytest.mark.parametrize(
|
|
141
|
+
"server_url",
|
|
142
|
+
[
|
|
143
|
+
"http://test.com",
|
|
144
|
+
"https://test.com",
|
|
145
|
+
],
|
|
146
|
+
)
|
|
147
|
+
def test_validate_server_url_valid(self, server_url):
|
|
148
|
+
"""Test validate_server_url with valid inputs."""
|
|
149
|
+
# Should not raise any exception
|
|
150
|
+
ExternalToolServer(
|
|
151
|
+
name="test-server",
|
|
152
|
+
type=ToolServerType.remote_mcp,
|
|
153
|
+
properties={"server_url": server_url},
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
@pytest.mark.parametrize(
|
|
157
|
+
"headers, expected_error",
|
|
158
|
+
[
|
|
159
|
+
(123, "headers must be a dictionary"),
|
|
160
|
+
("not-a-dict", "headers must be a dictionary"),
|
|
161
|
+
({"": "ok"}, "Header name is required"),
|
|
162
|
+
({"X-Key": ""}, "Header value is required"),
|
|
163
|
+
({"X-Key": None}, "Header value is required"),
|
|
164
|
+
({"Bad Name": "ok"}, r'Invalid header name: "Bad Name"'),
|
|
165
|
+
({"X@Key": "ok"}, r'Invalid header name: "X@Key"'),
|
|
166
|
+
(
|
|
167
|
+
{"X-Key\n": "ok"},
|
|
168
|
+
"Header names/values must not contain invalid characters",
|
|
169
|
+
),
|
|
170
|
+
(
|
|
171
|
+
{"X-Key": "bad\nvalue"},
|
|
172
|
+
"Header names/values must not contain invalid characters",
|
|
173
|
+
),
|
|
174
|
+
],
|
|
175
|
+
)
|
|
176
|
+
def test_validate_headers_invalid(self, headers, expected_error):
|
|
177
|
+
"""Test validate_headers."""
|
|
178
|
+
with pytest.raises(ValueError, match=expected_error):
|
|
179
|
+
ExternalToolServer(
|
|
180
|
+
name="test-server",
|
|
181
|
+
type=ToolServerType.remote_mcp,
|
|
182
|
+
properties={
|
|
183
|
+
"server_url": "https://test.com",
|
|
184
|
+
"headers": headers,
|
|
185
|
+
},
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
@pytest.mark.parametrize(
|
|
189
|
+
"headers",
|
|
190
|
+
[
|
|
191
|
+
{"Authorization": "Bearer token123"},
|
|
192
|
+
{"X-API-Key": "api-key-456"},
|
|
193
|
+
],
|
|
194
|
+
)
|
|
195
|
+
def test_validate_headers_valid(self, headers):
|
|
196
|
+
"""Test validate_headers with valid inputs."""
|
|
197
|
+
# Should not raise any exception
|
|
198
|
+
ExternalToolServer(
|
|
199
|
+
name="test-server",
|
|
200
|
+
type=ToolServerType.remote_mcp,
|
|
201
|
+
properties={
|
|
202
|
+
"server_url": "https://test.com",
|
|
203
|
+
"headers": headers,
|
|
204
|
+
},
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
@pytest.mark.parametrize(
|
|
208
|
+
"secret_header_keys, expected_error",
|
|
209
|
+
[
|
|
210
|
+
(
|
|
211
|
+
123,
|
|
212
|
+
"secret_header_keys must be a list for external tools of type 'remote_mcp'",
|
|
213
|
+
),
|
|
214
|
+
(
|
|
215
|
+
"not-a-list",
|
|
216
|
+
"secret_header_keys must be a list for external tools of type 'remote_mcp'",
|
|
217
|
+
),
|
|
218
|
+
([123], "secret_header_keys must contain only strings"),
|
|
219
|
+
(["ABC", ""], "Secret key is required"),
|
|
220
|
+
],
|
|
221
|
+
)
|
|
222
|
+
def test_validate_secret_header_keys_invalid(
|
|
223
|
+
self, secret_header_keys, expected_error
|
|
224
|
+
):
|
|
225
|
+
"""Test validate_secret_header_keys with invalid inputs."""
|
|
226
|
+
with pytest.raises(ValueError, match=expected_error):
|
|
227
|
+
ExternalToolServer(
|
|
228
|
+
name="test-server",
|
|
229
|
+
type=ToolServerType.remote_mcp,
|
|
230
|
+
properties={
|
|
231
|
+
"server_url": "https://test.com",
|
|
232
|
+
"headers": {},
|
|
233
|
+
"secret_header_keys": secret_header_keys,
|
|
234
|
+
},
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
def test_validate_secret_header_keys_valid(self):
|
|
238
|
+
"""Test validate_secret_header_keys with valid inputs."""
|
|
239
|
+
# Should not raise any exception
|
|
240
|
+
ExternalToolServer(
|
|
241
|
+
name="test-server",
|
|
242
|
+
type=ToolServerType.remote_mcp,
|
|
243
|
+
properties={
|
|
244
|
+
"server_url": "https://test.com",
|
|
245
|
+
"headers": {
|
|
246
|
+
"Authorization": "Bearer token123",
|
|
247
|
+
"X-API-Key": "api-key-456",
|
|
248
|
+
},
|
|
249
|
+
"secret_header_keys": ["Authorization", "X-API-Key"],
|
|
250
|
+
},
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
@pytest.mark.parametrize(
|
|
254
|
+
"env_vars, expected_error",
|
|
255
|
+
[
|
|
256
|
+
# Non-dictionary inputs
|
|
257
|
+
(123, "environment variables must be a dictionary"),
|
|
258
|
+
("not-a-dict", "environment variables must be a dictionary"),
|
|
259
|
+
# Empty key
|
|
260
|
+
(
|
|
261
|
+
{"": "value"},
|
|
262
|
+
"Invalid environment variable key: . Must start with a letter or underscore.",
|
|
263
|
+
),
|
|
264
|
+
# Keys that don't start with letter or underscore
|
|
265
|
+
(
|
|
266
|
+
{"123INVALID": "value"},
|
|
267
|
+
"Invalid environment variable key: 123INVALID. Must start with a letter or underscore.",
|
|
268
|
+
),
|
|
269
|
+
(
|
|
270
|
+
{"-INVALID": "value"},
|
|
271
|
+
"Invalid environment variable key: -INVALID. Must start with a letter or underscore.",
|
|
272
|
+
),
|
|
273
|
+
# Keys with invalid characters
|
|
274
|
+
(
|
|
275
|
+
{"INVALID-KEY": "value"},
|
|
276
|
+
"Invalid environment variable key: INVALID-KEY. Can only contain letters, digits, and underscores.",
|
|
277
|
+
),
|
|
278
|
+
(
|
|
279
|
+
{"INVALID.KEY": "value"},
|
|
280
|
+
"Invalid environment variable key: INVALID.KEY. Can only contain letters, digits, and underscores.",
|
|
281
|
+
),
|
|
282
|
+
# Emojis
|
|
283
|
+
(
|
|
284
|
+
{"API_KEY👍": "value"},
|
|
285
|
+
"Invalid environment variable key: API_KEY👍. Can only contain letters, digits, and underscores.",
|
|
286
|
+
),
|
|
287
|
+
# Non-ASCII characters
|
|
288
|
+
(
|
|
289
|
+
{"API_KEY_密鑰": "value"},
|
|
290
|
+
"Invalid environment variable key: API_KEY_密鑰. Can only contain letters, digits, and underscores.",
|
|
291
|
+
),
|
|
292
|
+
# Newlines
|
|
293
|
+
(
|
|
294
|
+
{"API\nKEY": "value"},
|
|
295
|
+
"Invalid environment variable key: API\nKEY. Can only contain letters, digits, and underscores.",
|
|
296
|
+
),
|
|
297
|
+
# Tabs
|
|
298
|
+
(
|
|
299
|
+
{"Hello\tWorld": "value"},
|
|
300
|
+
"Invalid environment variable key: Hello\tWorld. Can only contain letters, digits, and underscores.",
|
|
301
|
+
),
|
|
302
|
+
],
|
|
303
|
+
)
|
|
304
|
+
def test_validate_env_vars_invalid(self, env_vars, expected_error):
|
|
305
|
+
"""Test validate_env_vars with invalid inputs."""
|
|
306
|
+
with pytest.raises(ValueError, match=expected_error):
|
|
307
|
+
ExternalToolServer(
|
|
308
|
+
name="test-server",
|
|
309
|
+
type=ToolServerType.local_mcp,
|
|
310
|
+
properties={
|
|
311
|
+
"command": "python",
|
|
312
|
+
"args": [],
|
|
313
|
+
"env_vars": env_vars,
|
|
314
|
+
},
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
@pytest.mark.parametrize(
|
|
318
|
+
"env_vars",
|
|
319
|
+
[
|
|
320
|
+
# Valid cases
|
|
321
|
+
{},
|
|
322
|
+
{"VALID_KEY": "value"},
|
|
323
|
+
{"VALID123": "value"},
|
|
324
|
+
{"_VALID123": "value"},
|
|
325
|
+
# Multiple valid keys
|
|
326
|
+
{"KEY1": "value1", "KEY2": "value2", "_KEY3": "value3"},
|
|
327
|
+
# With paths
|
|
328
|
+
{"PATH": "/usr/bin"},
|
|
329
|
+
],
|
|
330
|
+
)
|
|
331
|
+
def test_validate_env_vars_valid(self, env_vars):
|
|
332
|
+
"""Test validate_env_vars with valid inputs."""
|
|
333
|
+
# Should not raise any exception
|
|
334
|
+
ExternalToolServer(
|
|
335
|
+
name="test-server",
|
|
336
|
+
type=ToolServerType.local_mcp,
|
|
337
|
+
properties={
|
|
338
|
+
"command": "python",
|
|
339
|
+
"args": [],
|
|
340
|
+
"env_vars": env_vars,
|
|
341
|
+
},
|
|
342
|
+
)
|
|
343
|
+
|
|
73
344
|
@pytest.mark.parametrize(
|
|
74
345
|
"server_type, invalid_props, expected_error",
|
|
75
346
|
[
|
|
76
|
-
#
|
|
77
|
-
(ToolServerType.remote_mcp, {}, "server_url must be a string"),
|
|
78
|
-
(ToolServerType.remote_mcp, {"server_url": ""}, "server_url is required"),
|
|
347
|
+
# Missing type entirely
|
|
79
348
|
(
|
|
80
|
-
|
|
81
|
-
{
|
|
82
|
-
"
|
|
349
|
+
None,
|
|
350
|
+
{},
|
|
351
|
+
"type is required", # Required by pydantic in RemoteServerProperties
|
|
83
352
|
),
|
|
353
|
+
# Remote MCP missing server_url
|
|
84
354
|
(
|
|
85
355
|
ToolServerType.remote_mcp,
|
|
86
|
-
{
|
|
87
|
-
"
|
|
356
|
+
{},
|
|
357
|
+
"Server URL is required to connect to a remote MCP server",
|
|
88
358
|
),
|
|
359
|
+
# Remote MCP validation errors
|
|
89
360
|
(
|
|
90
361
|
ToolServerType.remote_mcp,
|
|
91
|
-
{"server_url": "
|
|
92
|
-
"
|
|
362
|
+
{"server_url": ""},
|
|
363
|
+
"Server URL is not a valid URL",
|
|
93
364
|
),
|
|
94
365
|
(
|
|
95
366
|
ToolServerType.remote_mcp,
|
|
@@ -110,14 +381,17 @@ class TestExternalToolServer:
|
|
|
110
381
|
"secret_header_keys must contain only strings",
|
|
111
382
|
),
|
|
112
383
|
# Local MCP validation errors
|
|
113
|
-
(ToolServerType.local_mcp, {}, "command must be a string"),
|
|
114
|
-
(ToolServerType.local_mcp, {"command": ""}, "command is required"),
|
|
115
|
-
(ToolServerType.local_mcp, {"command": 123}, "command must be a string"),
|
|
116
384
|
(
|
|
117
385
|
ToolServerType.local_mcp,
|
|
118
|
-
{
|
|
119
|
-
"
|
|
386
|
+
{},
|
|
387
|
+
"command is required to start a local MCP server",
|
|
120
388
|
),
|
|
389
|
+
(
|
|
390
|
+
ToolServerType.local_mcp,
|
|
391
|
+
{"command": ""},
|
|
392
|
+
"command must be a non-empty string",
|
|
393
|
+
), # Required by pydantic in LocalServerProperties
|
|
394
|
+
(ToolServerType.local_mcp, {"command": 123}, "command must be a string"),
|
|
121
395
|
(
|
|
122
396
|
ToolServerType.local_mcp,
|
|
123
397
|
{"command": "python", "args": "not-a-list"},
|
|
@@ -148,11 +422,56 @@ class TestExternalToolServer:
|
|
|
148
422
|
},
|
|
149
423
|
"secret_env_var_keys must contain only strings",
|
|
150
424
|
),
|
|
425
|
+
# Kiln task validation errors
|
|
426
|
+
(
|
|
427
|
+
ToolServerType.kiln_task,
|
|
428
|
+
{},
|
|
429
|
+
"Tool name cannot be empty",
|
|
430
|
+
),
|
|
431
|
+
(
|
|
432
|
+
ToolServerType.kiln_task,
|
|
433
|
+
{"name": ""},
|
|
434
|
+
"Tool name cannot be empty",
|
|
435
|
+
),
|
|
436
|
+
(
|
|
437
|
+
ToolServerType.kiln_task,
|
|
438
|
+
{"name": "test", "description": 123},
|
|
439
|
+
"description must be of type <class 'str'>",
|
|
440
|
+
),
|
|
441
|
+
(
|
|
442
|
+
ToolServerType.kiln_task,
|
|
443
|
+
{"name": "test", "description": "a" * 129},
|
|
444
|
+
"description must be 128 characters or less",
|
|
445
|
+
),
|
|
446
|
+
(
|
|
447
|
+
ToolServerType.kiln_task,
|
|
448
|
+
{"name": "test", "description": "test", "is_archived": "not-bool"},
|
|
449
|
+
"is_archived must be of type <class 'bool'>",
|
|
450
|
+
),
|
|
451
|
+
(
|
|
452
|
+
ToolServerType.kiln_task,
|
|
453
|
+
{
|
|
454
|
+
"name": "test",
|
|
455
|
+
"description": "test",
|
|
456
|
+
"is_archived": False,
|
|
457
|
+
"task_id": 123,
|
|
458
|
+
},
|
|
459
|
+
"task_id must be of type <class 'str'>",
|
|
460
|
+
),
|
|
461
|
+
(
|
|
462
|
+
ToolServerType.kiln_task,
|
|
463
|
+
{
|
|
464
|
+
"name": "test",
|
|
465
|
+
"description": "test",
|
|
466
|
+
"is_archived": False,
|
|
467
|
+
"task_id": "task-123",
|
|
468
|
+
"run_config_id": 456,
|
|
469
|
+
},
|
|
470
|
+
"run_config_id must be of type <class 'str'>",
|
|
471
|
+
),
|
|
151
472
|
],
|
|
152
473
|
)
|
|
153
|
-
def test_validation_errors(
|
|
154
|
-
self, mock_config, server_type, invalid_props, expected_error
|
|
155
|
-
):
|
|
474
|
+
def test_validation_errors(self, server_type, invalid_props, expected_error):
|
|
156
475
|
"""Test validation errors for invalid configurations."""
|
|
157
476
|
with pytest.raises((ValueError, Exception)) as exc_info:
|
|
158
477
|
ExternalToolServer(
|
|
@@ -161,7 +480,7 @@ class TestExternalToolServer:
|
|
|
161
480
|
# Check that the expected error message is in the exception string
|
|
162
481
|
assert expected_error in str(exc_info.value)
|
|
163
482
|
|
|
164
|
-
def test_get_secret_keys_remote_mcp(self,
|
|
483
|
+
def test_get_secret_keys_remote_mcp(self, remote_mcp_base_props):
|
|
165
484
|
"""Test get_secret_keys for remote MCP servers."""
|
|
166
485
|
# No secret keys defined
|
|
167
486
|
server = ExternalToolServer(
|
|
@@ -173,17 +492,18 @@ class TestExternalToolServer:
|
|
|
173
492
|
|
|
174
493
|
# With secret header keys
|
|
175
494
|
props_with_secrets = {
|
|
176
|
-
|
|
495
|
+
"server_url": remote_mcp_base_props["server_url"],
|
|
496
|
+
"headers": remote_mcp_base_props.get("headers", {}),
|
|
177
497
|
"secret_header_keys": ["Authorization", "X-API-Key"],
|
|
178
498
|
}
|
|
179
499
|
server = ExternalToolServer(
|
|
180
500
|
name="test-server",
|
|
181
501
|
type=ToolServerType.remote_mcp,
|
|
182
|
-
properties=props_with_secrets,
|
|
502
|
+
properties=props_with_secrets, # type: ignore
|
|
183
503
|
)
|
|
184
504
|
assert server.get_secret_keys() == ["Authorization", "X-API-Key"]
|
|
185
505
|
|
|
186
|
-
def test_get_secret_keys_local_mcp(self,
|
|
506
|
+
def test_get_secret_keys_local_mcp(self, local_mcp_base_props):
|
|
187
507
|
"""Test get_secret_keys for local MCP servers."""
|
|
188
508
|
# No secret keys defined
|
|
189
509
|
server = ExternalToolServer(
|
|
@@ -195,42 +515,45 @@ class TestExternalToolServer:
|
|
|
195
515
|
|
|
196
516
|
# With secret env var keys
|
|
197
517
|
props_with_secrets = {
|
|
198
|
-
|
|
518
|
+
"command": local_mcp_base_props["command"],
|
|
519
|
+
"args": local_mcp_base_props.get("args", []),
|
|
520
|
+
"env_vars": local_mcp_base_props.get("env_vars", {}),
|
|
199
521
|
"secret_env_var_keys": ["API_KEY", "SECRET_TOKEN"],
|
|
200
522
|
}
|
|
201
523
|
server = ExternalToolServer(
|
|
202
524
|
name="test-server",
|
|
203
525
|
type=ToolServerType.local_mcp,
|
|
204
|
-
properties=props_with_secrets,
|
|
526
|
+
properties=props_with_secrets, # type: ignore
|
|
205
527
|
)
|
|
206
528
|
assert server.get_secret_keys() == ["API_KEY", "SECRET_TOKEN"]
|
|
207
529
|
|
|
208
|
-
def
|
|
530
|
+
def test_get_secret_keys_kiln_task(self, kiln_task_base_props):
|
|
531
|
+
"""Test get_secret_keys for kiln task servers."""
|
|
532
|
+
server = ExternalToolServer(
|
|
533
|
+
name="test_server",
|
|
534
|
+
type=ToolServerType.kiln_task,
|
|
535
|
+
properties=kiln_task_base_props,
|
|
536
|
+
)
|
|
537
|
+
assert server.get_secret_keys() == []
|
|
538
|
+
|
|
539
|
+
def test_secret_processing_remote_mcp_initialization(
|
|
540
|
+
self, remote_mcp_props_with_secrets, sample_remote_mcp_secrets
|
|
541
|
+
):
|
|
209
542
|
"""Test secret processing during remote MCP server initialization."""
|
|
210
|
-
properties =
|
|
211
|
-
"server_url": "https://api.example.com/mcp",
|
|
212
|
-
"headers": {
|
|
213
|
-
"Content-Type": "application/json",
|
|
214
|
-
"Authorization": "Bearer secret123",
|
|
215
|
-
"X-API-Key": "api-key-456",
|
|
216
|
-
},
|
|
217
|
-
"secret_header_keys": ["Authorization", "X-API-Key"],
|
|
218
|
-
}
|
|
543
|
+
properties = remote_mcp_props_with_secrets
|
|
219
544
|
|
|
220
545
|
server = ExternalToolServer(
|
|
221
546
|
name="test-server", type=ToolServerType.remote_mcp, properties=properties
|
|
222
547
|
)
|
|
223
548
|
|
|
224
549
|
# Secrets should be extracted to _unsaved_secrets
|
|
225
|
-
assert server._unsaved_secrets ==
|
|
226
|
-
"Authorization": "Bearer secret123",
|
|
227
|
-
"X-API-Key": "api-key-456",
|
|
228
|
-
}
|
|
550
|
+
assert server._unsaved_secrets == sample_remote_mcp_secrets
|
|
229
551
|
|
|
230
552
|
# Secrets should be removed from headers
|
|
231
|
-
|
|
553
|
+
headers = server.properties.get("headers", {})
|
|
554
|
+
assert headers == {"Content-Type": "application/json"}
|
|
232
555
|
|
|
233
|
-
def test_secret_processing_local_mcp_initialization(self
|
|
556
|
+
def test_secret_processing_local_mcp_initialization(self):
|
|
234
557
|
"""Test secret processing during local MCP server initialization."""
|
|
235
558
|
properties = {
|
|
236
559
|
"command": "python",
|
|
@@ -244,7 +567,9 @@ class TestExternalToolServer:
|
|
|
244
567
|
}
|
|
245
568
|
|
|
246
569
|
server = ExternalToolServer(
|
|
247
|
-
name="test-server",
|
|
570
|
+
name="test-server",
|
|
571
|
+
type=ToolServerType.local_mcp,
|
|
572
|
+
properties=properties, # type: ignore
|
|
248
573
|
)
|
|
249
574
|
|
|
250
575
|
# Secrets should be extracted to _unsaved_secrets
|
|
@@ -254,108 +579,108 @@ class TestExternalToolServer:
|
|
|
254
579
|
}
|
|
255
580
|
|
|
256
581
|
# Secrets should be removed from env_vars
|
|
257
|
-
|
|
582
|
+
env_vars = server.properties.get("env_vars", {})
|
|
583
|
+
assert env_vars == {"PATH": "/usr/bin"}
|
|
584
|
+
|
|
585
|
+
def test_secret_processing_kiln_task_initialization(self, kiln_task_base_props):
|
|
586
|
+
"""Test secret processing during kiln task server initialization."""
|
|
587
|
+
server = ExternalToolServer(
|
|
588
|
+
name="test_server",
|
|
589
|
+
type=ToolServerType.kiln_task,
|
|
590
|
+
properties=kiln_task_base_props,
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
# Kiln task servers should have no secrets processed
|
|
594
|
+
assert server._unsaved_secrets == {}
|
|
595
|
+
|
|
596
|
+
# Properties should remain unchanged
|
|
597
|
+
assert server.properties == kiln_task_base_props
|
|
258
598
|
|
|
259
599
|
def test_secret_processing_property_update_remote_mcp(
|
|
260
|
-
self,
|
|
600
|
+
self, remote_mcp_props_with_secrets
|
|
261
601
|
):
|
|
262
602
|
"""Test secret processing when properties are updated via __setattr__ for remote MCP."""
|
|
263
603
|
server = ExternalToolServer(
|
|
264
604
|
name="test-server",
|
|
265
605
|
type=ToolServerType.remote_mcp,
|
|
266
|
-
properties=
|
|
267
|
-
**remote_mcp_base_props,
|
|
268
|
-
"secret_header_keys": ["Authorization"],
|
|
269
|
-
},
|
|
606
|
+
properties=remote_mcp_props_with_secrets,
|
|
270
607
|
)
|
|
271
608
|
|
|
272
609
|
# Clear any existing unsaved secrets
|
|
273
610
|
server._unsaved_secrets.clear()
|
|
274
611
|
|
|
275
|
-
# Update properties with secrets
|
|
612
|
+
# Update properties with new secrets
|
|
276
613
|
new_properties = {
|
|
277
|
-
|
|
614
|
+
"server_url": remote_mcp_props_with_secrets["server_url"],
|
|
278
615
|
"headers": {
|
|
279
|
-
|
|
280
|
-
"Authorization": "Bearer new-token",
|
|
616
|
+
"New-Secret-Header": "Bearer new-token",
|
|
281
617
|
},
|
|
282
|
-
"secret_header_keys": ["
|
|
618
|
+
"secret_header_keys": ["New-Secret-Header"],
|
|
283
619
|
}
|
|
284
620
|
|
|
285
|
-
server.properties = new_properties
|
|
621
|
+
server.properties = new_properties # type: ignore
|
|
286
622
|
|
|
287
623
|
# Secret should be processed (extracted and removed from headers)
|
|
288
|
-
assert server._unsaved_secrets == {"
|
|
289
|
-
|
|
624
|
+
assert server._unsaved_secrets == {"New-Secret-Header": "Bearer new-token"}
|
|
625
|
+
headers = server.properties.get("headers", {})
|
|
626
|
+
assert "New-Secret-Header" not in headers
|
|
290
627
|
|
|
291
628
|
def test_secret_processing_clears_existing_secrets(
|
|
292
|
-
self,
|
|
629
|
+
self,
|
|
630
|
+
remote_mcp_base_props,
|
|
631
|
+
remote_mcp_props_with_secrets,
|
|
632
|
+
sample_remote_mcp_secrets,
|
|
293
633
|
):
|
|
294
634
|
"""Test that secret processing clears existing _unsaved_secrets."""
|
|
295
635
|
server = ExternalToolServer(
|
|
296
636
|
name="test-server",
|
|
297
637
|
type=ToolServerType.remote_mcp,
|
|
298
|
-
properties=
|
|
299
|
-
**remote_mcp_base_props,
|
|
300
|
-
"secret_header_keys": ["Authorization"],
|
|
301
|
-
},
|
|
638
|
+
properties=remote_mcp_base_props,
|
|
302
639
|
)
|
|
303
640
|
|
|
304
641
|
# Manually add some unsaved secrets
|
|
305
642
|
server._unsaved_secrets = {"OldSecret": "old-value"}
|
|
306
643
|
|
|
307
|
-
# Update properties - should clear old secrets
|
|
308
|
-
|
|
309
|
-
**remote_mcp_base_props,
|
|
310
|
-
"headers": {
|
|
311
|
-
**remote_mcp_base_props["headers"],
|
|
312
|
-
"Authorization": "Bearer new-token",
|
|
313
|
-
},
|
|
314
|
-
"secret_header_keys": ["Authorization"],
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
server.properties = new_properties
|
|
644
|
+
# Update properties with new secrets - this should clear old secrets
|
|
645
|
+
server.properties = remote_mcp_props_with_secrets
|
|
318
646
|
|
|
319
647
|
# Only new secret should remain
|
|
320
|
-
assert server._unsaved_secrets ==
|
|
648
|
+
assert server._unsaved_secrets == sample_remote_mcp_secrets
|
|
321
649
|
assert "OldSecret" not in server._unsaved_secrets
|
|
322
650
|
|
|
323
|
-
def test_retrieve_secrets_from_config(
|
|
651
|
+
def test_retrieve_secrets_from_config(
|
|
652
|
+
self, mock_config, remote_mcp_props_with_secrets, sample_remote_mcp_secrets
|
|
653
|
+
):
|
|
324
654
|
"""Test retrieving secrets from config storage."""
|
|
325
655
|
server = ExternalToolServer(
|
|
326
656
|
name="test-server",
|
|
327
657
|
type=ToolServerType.remote_mcp,
|
|
328
|
-
properties=
|
|
329
|
-
**remote_mcp_base_props,
|
|
330
|
-
"secret_header_keys": ["Authorization", "X-API-Key"],
|
|
331
|
-
},
|
|
658
|
+
properties=remote_mcp_props_with_secrets,
|
|
332
659
|
)
|
|
660
|
+
|
|
661
|
+
# Set ID to test retrieval from config
|
|
333
662
|
server.id = "server-123"
|
|
334
663
|
|
|
335
664
|
# Mock config to return saved secrets
|
|
336
665
|
mock_config.get_value.return_value = {
|
|
337
|
-
"server-123::Authorization": "Bearer
|
|
338
|
-
"server-123::X-API-Key": "
|
|
666
|
+
"server-123::Authorization": "Bearer token123",
|
|
667
|
+
"server-123::X-API-Key": "api-key-456",
|
|
339
668
|
"other-server::Authorization": "other-token",
|
|
340
669
|
}
|
|
341
670
|
|
|
342
671
|
secrets, missing = server.retrieve_secrets()
|
|
343
672
|
|
|
344
|
-
assert secrets ==
|
|
345
|
-
"Authorization": "Bearer config-token",
|
|
346
|
-
"X-API-Key": "config-api-key",
|
|
347
|
-
}
|
|
673
|
+
assert secrets == sample_remote_mcp_secrets
|
|
348
674
|
assert missing == []
|
|
349
675
|
|
|
350
|
-
def test_retrieve_secrets_from_unsaved(
|
|
676
|
+
def test_retrieve_secrets_from_unsaved(
|
|
677
|
+
self, mock_config, remote_mcp_props_with_secrets
|
|
678
|
+
):
|
|
351
679
|
"""Test retrieving secrets from unsaved storage when not in config."""
|
|
352
680
|
server = ExternalToolServer(
|
|
353
681
|
name="test-server",
|
|
354
682
|
type=ToolServerType.remote_mcp,
|
|
355
|
-
properties=
|
|
356
|
-
**remote_mcp_base_props,
|
|
357
|
-
"secret_header_keys": ["Authorization", "X-API-Key"],
|
|
358
|
-
},
|
|
683
|
+
properties=remote_mcp_props_with_secrets,
|
|
359
684
|
)
|
|
360
685
|
server.id = "server-123"
|
|
361
686
|
server._unsaved_secrets = {
|
|
@@ -382,7 +707,8 @@ class TestExternalToolServer:
|
|
|
382
707
|
name="test-server",
|
|
383
708
|
type=ToolServerType.remote_mcp,
|
|
384
709
|
properties={
|
|
385
|
-
|
|
710
|
+
"server_url": remote_mcp_base_props["server_url"],
|
|
711
|
+
"headers": remote_mcp_base_props.get("headers", {}),
|
|
386
712
|
"secret_header_keys": ["Authorization"],
|
|
387
713
|
},
|
|
388
714
|
)
|
|
@@ -407,7 +733,8 @@ class TestExternalToolServer:
|
|
|
407
733
|
name="test-server",
|
|
408
734
|
type=ToolServerType.remote_mcp,
|
|
409
735
|
properties={
|
|
410
|
-
|
|
736
|
+
"server_url": remote_mcp_base_props["server_url"],
|
|
737
|
+
"headers": remote_mcp_base_props.get("headers", {}),
|
|
411
738
|
"secret_header_keys": ["Authorization", "X-API-Key", "Missing-Key"],
|
|
412
739
|
},
|
|
413
740
|
)
|
|
@@ -423,7 +750,7 @@ class TestExternalToolServer:
|
|
|
423
750
|
assert secrets == {"Authorization": "Bearer config-token"}
|
|
424
751
|
assert set(missing) == {"X-API-Key", "Missing-Key"}
|
|
425
752
|
|
|
426
|
-
def test_retrieve_secrets_no_secret_keys(self,
|
|
753
|
+
def test_retrieve_secrets_no_secret_keys(self, remote_mcp_base_props):
|
|
427
754
|
"""Test retrieving secrets when no secret keys are defined."""
|
|
428
755
|
server = ExternalToolServer(
|
|
429
756
|
name="test-server",
|
|
@@ -436,21 +763,30 @@ class TestExternalToolServer:
|
|
|
436
763
|
assert secrets == {}
|
|
437
764
|
assert missing == []
|
|
438
765
|
|
|
439
|
-
def
|
|
766
|
+
def test_retrieve_secrets_kiln_task(self, kiln_task_base_props):
|
|
767
|
+
"""Test retrieving secrets for kiln task servers (should return empty)."""
|
|
768
|
+
server = ExternalToolServer(
|
|
769
|
+
name="test_server",
|
|
770
|
+
type=ToolServerType.kiln_task,
|
|
771
|
+
properties=kiln_task_base_props,
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
secrets, missing = server.retrieve_secrets()
|
|
775
|
+
|
|
776
|
+
assert secrets == {}
|
|
777
|
+
assert missing == []
|
|
778
|
+
|
|
779
|
+
def test_save_secrets(
|
|
780
|
+
self, mock_config, remote_mcp_props_with_secrets, sample_remote_mcp_secrets
|
|
781
|
+
):
|
|
440
782
|
"""Test saving unsaved secrets to config."""
|
|
441
783
|
server = ExternalToolServer(
|
|
442
784
|
name="test-server",
|
|
443
785
|
type=ToolServerType.remote_mcp,
|
|
444
|
-
properties=
|
|
445
|
-
**remote_mcp_base_props,
|
|
446
|
-
"secret_header_keys": ["Authorization", "X-API-Key"],
|
|
447
|
-
},
|
|
786
|
+
properties=remote_mcp_props_with_secrets,
|
|
448
787
|
)
|
|
449
788
|
server.id = "server-123"
|
|
450
|
-
server._unsaved_secrets =
|
|
451
|
-
"Authorization": "Bearer token",
|
|
452
|
-
"X-API-Key": "api-key",
|
|
453
|
-
}
|
|
789
|
+
server._unsaved_secrets = sample_remote_mcp_secrets
|
|
454
790
|
|
|
455
791
|
# Mock existing config secrets
|
|
456
792
|
existing_secrets = {"other-server::key": "other-value"}
|
|
@@ -461,8 +797,8 @@ class TestExternalToolServer:
|
|
|
461
797
|
# Should update config with new secrets
|
|
462
798
|
expected_secrets = {
|
|
463
799
|
"other-server::key": "other-value",
|
|
464
|
-
"server-123::Authorization": "Bearer
|
|
465
|
-
"server-123::X-API-Key": "api-key",
|
|
800
|
+
"server-123::Authorization": "Bearer token123",
|
|
801
|
+
"server-123::X-API-Key": "api-key-456",
|
|
466
802
|
}
|
|
467
803
|
mock_config.update_settings.assert_called_once_with(
|
|
468
804
|
{MCP_SECRETS_KEY: expected_secrets}
|
|
@@ -471,18 +807,14 @@ class TestExternalToolServer:
|
|
|
471
807
|
# Should clear unsaved secrets
|
|
472
808
|
assert server._unsaved_secrets == {}
|
|
473
809
|
|
|
474
|
-
def test_save_secrets_no_id_error(self,
|
|
810
|
+
def test_save_secrets_no_id_error(self, remote_mcp_props_with_secrets):
|
|
475
811
|
"""Test that saving secrets without ID raises error."""
|
|
476
812
|
server = ExternalToolServer(
|
|
477
813
|
name="test-server",
|
|
478
814
|
type=ToolServerType.remote_mcp,
|
|
479
|
-
properties=
|
|
480
|
-
**remote_mcp_base_props,
|
|
481
|
-
"secret_header_keys": ["Authorization"],
|
|
482
|
-
},
|
|
815
|
+
properties=remote_mcp_props_with_secrets,
|
|
483
816
|
)
|
|
484
|
-
|
|
485
|
-
server._unsaved_secrets = {"Authorization": "Bearer token"}
|
|
817
|
+
|
|
486
818
|
# Explicitly set ID to None to test the error condition
|
|
487
819
|
server.id = None
|
|
488
820
|
|
|
@@ -492,42 +824,37 @@ class TestExternalToolServer:
|
|
|
492
824
|
server._save_secrets()
|
|
493
825
|
|
|
494
826
|
def test_save_secrets_with_no_unsaved_secrets(
|
|
495
|
-
self, mock_config,
|
|
827
|
+
self, mock_config, remote_mcp_props_with_secrets
|
|
496
828
|
):
|
|
497
829
|
"""Test that saving secrets with no unsaved secrets does nothing."""
|
|
498
830
|
server = ExternalToolServer(
|
|
499
831
|
name="test-server",
|
|
500
832
|
type=ToolServerType.remote_mcp,
|
|
501
|
-
properties=
|
|
502
|
-
**remote_mcp_base_props,
|
|
503
|
-
"secret_header_keys": ["Authorization"],
|
|
504
|
-
},
|
|
833
|
+
properties=remote_mcp_props_with_secrets,
|
|
505
834
|
)
|
|
506
835
|
server.id = "server-123"
|
|
507
836
|
|
|
508
|
-
#
|
|
837
|
+
# Override _unsaved_secrets to empty
|
|
838
|
+
server._unsaved_secrets = {}
|
|
509
839
|
|
|
510
840
|
server._save_secrets()
|
|
511
841
|
|
|
512
842
|
# Should not call update_settings
|
|
513
843
|
mock_config.update_settings.assert_not_called()
|
|
514
844
|
|
|
515
|
-
def test_delete_secrets(self, mock_config,
|
|
845
|
+
def test_delete_secrets(self, mock_config, remote_mcp_props_with_secrets):
|
|
516
846
|
"""Test deleting secrets from config."""
|
|
517
847
|
server = ExternalToolServer(
|
|
518
848
|
name="test-server",
|
|
519
849
|
type=ToolServerType.remote_mcp,
|
|
520
|
-
properties=
|
|
521
|
-
**remote_mcp_base_props,
|
|
522
|
-
"secret_header_keys": ["Authorization", "X-API-Key"],
|
|
523
|
-
},
|
|
850
|
+
properties=remote_mcp_props_with_secrets,
|
|
524
851
|
)
|
|
525
852
|
server.id = "server-123"
|
|
526
853
|
|
|
527
854
|
# Mock existing config secrets
|
|
528
855
|
existing_secrets = {
|
|
529
|
-
"server-123::Authorization": "Bearer
|
|
530
|
-
"server-123::X-API-Key": "api-key",
|
|
856
|
+
"server-123::Authorization": "Bearer token123",
|
|
857
|
+
"server-123::X-API-Key": "api-key-456",
|
|
531
858
|
"other-server::Authorization": "other-token",
|
|
532
859
|
}
|
|
533
860
|
mock_config.get_value.return_value = existing_secrets
|
|
@@ -541,16 +868,13 @@ class TestExternalToolServer:
|
|
|
541
868
|
)
|
|
542
869
|
|
|
543
870
|
def test_delete_secrets_with_no_existing_secrets(
|
|
544
|
-
self, mock_config,
|
|
871
|
+
self, mock_config, remote_mcp_props_with_secrets
|
|
545
872
|
):
|
|
546
873
|
"""Test deleting secrets when none exist in config."""
|
|
547
874
|
server = ExternalToolServer(
|
|
548
875
|
name="test-server",
|
|
549
876
|
type=ToolServerType.remote_mcp,
|
|
550
|
-
properties=
|
|
551
|
-
**remote_mcp_base_props,
|
|
552
|
-
"secret_header_keys": ["Authorization"],
|
|
553
|
-
},
|
|
877
|
+
properties=remote_mcp_props_with_secrets,
|
|
554
878
|
)
|
|
555
879
|
server.id = "server-123"
|
|
556
880
|
|
|
@@ -562,18 +886,17 @@ class TestExternalToolServer:
|
|
|
562
886
|
# Should still call update_settings with empty dict
|
|
563
887
|
mock_config.update_settings.assert_called_once_with({MCP_SECRETS_KEY: {}})
|
|
564
888
|
|
|
565
|
-
def test_save_to_file_saves_secrets_first(
|
|
889
|
+
def test_save_to_file_saves_secrets_first(
|
|
890
|
+
self, mock_config, remote_mcp_props_with_secrets, sample_remote_mcp_secrets
|
|
891
|
+
):
|
|
566
892
|
"""Test that save_to_file automatically saves unsaved secrets first."""
|
|
567
893
|
server = ExternalToolServer(
|
|
568
894
|
name="test-server",
|
|
569
895
|
type=ToolServerType.remote_mcp,
|
|
570
|
-
properties=
|
|
571
|
-
**remote_mcp_base_props,
|
|
572
|
-
"secret_header_keys": ["Authorization"],
|
|
573
|
-
},
|
|
896
|
+
properties=remote_mcp_props_with_secrets,
|
|
574
897
|
)
|
|
575
898
|
server.id = "server-123"
|
|
576
|
-
server._unsaved_secrets =
|
|
899
|
+
server._unsaved_secrets = sample_remote_mcp_secrets
|
|
577
900
|
|
|
578
901
|
mock_config.get_value.return_value = {}
|
|
579
902
|
|
|
@@ -608,19 +931,19 @@ class TestExternalToolServer:
|
|
|
608
931
|
# Should still call parent save_to_file
|
|
609
932
|
mock_parent_save.assert_called_once()
|
|
610
933
|
|
|
611
|
-
def test_config_secret_key_format(self,
|
|
934
|
+
def test_config_secret_key_format(self, remote_mcp_props_with_secrets):
|
|
612
935
|
"""Test the _config_secret_key method formats keys correctly."""
|
|
613
936
|
server = ExternalToolServer(
|
|
614
937
|
name="test-server",
|
|
615
938
|
type=ToolServerType.remote_mcp,
|
|
616
|
-
properties=
|
|
939
|
+
properties=remote_mcp_props_with_secrets,
|
|
617
940
|
)
|
|
618
941
|
server.id = "server-123"
|
|
619
942
|
|
|
620
943
|
assert server._config_secret_key("Authorization") == "server-123::Authorization"
|
|
621
944
|
assert server._config_secret_key("X-API-Key") == "server-123::X-API-Key"
|
|
622
945
|
|
|
623
|
-
def test_model_serialization_excludes_secrets(self
|
|
946
|
+
def test_model_serialization_excludes_secrets(self):
|
|
624
947
|
"""Test that model serialization excludes _unsaved_secrets private attribute and secrets from properties."""
|
|
625
948
|
# Test all server types to ensure we update this test when new types are added
|
|
626
949
|
for server_type in ToolServerType:
|
|
@@ -654,15 +977,42 @@ class TestExternalToolServer:
|
|
|
654
977
|
assert "_unsaved_secrets" not in data
|
|
655
978
|
assert "API_KEY" not in data["properties"]["env_vars"]
|
|
656
979
|
|
|
980
|
+
case ToolServerType.kiln_task:
|
|
981
|
+
server = ExternalToolServer(
|
|
982
|
+
name="test_kiln_task_server",
|
|
983
|
+
type=server_type,
|
|
984
|
+
properties={
|
|
985
|
+
"task_id": "task-123",
|
|
986
|
+
"run_config_id": "run-config-456",
|
|
987
|
+
"name": "test_task_tool",
|
|
988
|
+
"description": "A test task tool",
|
|
989
|
+
"is_archived": False,
|
|
990
|
+
},
|
|
991
|
+
)
|
|
992
|
+
data = server.model_dump()
|
|
993
|
+
assert "_unsaved_secrets" not in data
|
|
994
|
+
# Kiln task properties should be preserved as-is since there are no secrets
|
|
995
|
+
assert data["properties"]["task_id"] == "task-123"
|
|
996
|
+
assert data["properties"]["run_config_id"] == "run-config-456"
|
|
997
|
+
assert data["properties"]["name"] == "test_task_tool"
|
|
998
|
+
assert data["properties"]["description"] == "A test task tool"
|
|
999
|
+
assert data["properties"]["is_archived"] is False
|
|
1000
|
+
|
|
657
1001
|
case _:
|
|
658
1002
|
raise_exhaustive_enum_error(server_type)
|
|
659
1003
|
|
|
660
|
-
def test_empty_secret_keys_list(self,
|
|
1004
|
+
def test_empty_secret_keys_list(self, remote_mcp_base_props):
|
|
661
1005
|
"""Test behavior with empty secret_header_keys list."""
|
|
662
|
-
properties = {
|
|
1006
|
+
properties = {
|
|
1007
|
+
"server_url": remote_mcp_base_props["server_url"],
|
|
1008
|
+
"headers": remote_mcp_base_props.get("headers", {}),
|
|
1009
|
+
"secret_header_keys": [],
|
|
1010
|
+
}
|
|
663
1011
|
|
|
664
1012
|
server = ExternalToolServer(
|
|
665
|
-
name="test-server",
|
|
1013
|
+
name="test-server",
|
|
1014
|
+
type=ToolServerType.remote_mcp,
|
|
1015
|
+
properties=properties, # type: ignore
|
|
666
1016
|
)
|
|
667
1017
|
|
|
668
1018
|
assert server.get_secret_keys() == []
|
|
@@ -670,22 +1020,54 @@ class TestExternalToolServer:
|
|
|
670
1020
|
assert secrets == {}
|
|
671
1021
|
assert missing == []
|
|
672
1022
|
|
|
673
|
-
def test_none_mcp_secrets_in_config(
|
|
1023
|
+
def test_none_mcp_secrets_in_config(
|
|
1024
|
+
self, mock_config, remote_mcp_props_with_secrets
|
|
1025
|
+
):
|
|
674
1026
|
"""Test behavior when MCP_SECRETS_KEY returns None from config."""
|
|
675
1027
|
server = ExternalToolServer(
|
|
676
1028
|
name="test-server",
|
|
677
1029
|
type=ToolServerType.remote_mcp,
|
|
678
|
-
properties=
|
|
679
|
-
**remote_mcp_base_props,
|
|
680
|
-
"secret_header_keys": ["Authorization"],
|
|
681
|
-
},
|
|
1030
|
+
properties=remote_mcp_props_with_secrets,
|
|
682
1031
|
)
|
|
683
1032
|
server.id = "server-123"
|
|
684
1033
|
|
|
1034
|
+
# Override _unsaved_secrets to empty
|
|
1035
|
+
server._unsaved_secrets = {}
|
|
1036
|
+
|
|
685
1037
|
# Mock config returning None for MCP_SECRETS_KEY
|
|
686
1038
|
mock_config.get_value.return_value = None
|
|
687
1039
|
|
|
688
1040
|
secrets, missing = server.retrieve_secrets()
|
|
689
1041
|
|
|
690
1042
|
assert secrets == {}
|
|
691
|
-
assert missing == ["Authorization"]
|
|
1043
|
+
assert missing == ["Authorization", "X-API-Key"]
|
|
1044
|
+
|
|
1045
|
+
@pytest.mark.parametrize(
|
|
1046
|
+
"data, expected_type",
|
|
1047
|
+
[
|
|
1048
|
+
({"type": "remote_mcp"}, ToolServerType.remote_mcp),
|
|
1049
|
+
({"type": "local_mcp"}, ToolServerType.local_mcp),
|
|
1050
|
+
({"type": "kiln_task"}, ToolServerType.kiln_task),
|
|
1051
|
+
],
|
|
1052
|
+
)
|
|
1053
|
+
def test_type_from_data_valid(self, data, expected_type):
|
|
1054
|
+
"""Test type_from_data with valid data."""
|
|
1055
|
+
result = ExternalToolServer.type_from_data(data)
|
|
1056
|
+
assert result == expected_type
|
|
1057
|
+
|
|
1058
|
+
def test_type_from_data_invalid(self):
|
|
1059
|
+
"""Test type_from_data with invalid data."""
|
|
1060
|
+
valid_types = ", ".join(type.value for type in ToolServerType)
|
|
1061
|
+
invalid_type_error = f"type must be one of: {valid_types}"
|
|
1062
|
+
|
|
1063
|
+
test_cases = [
|
|
1064
|
+
({}, "type is required"),
|
|
1065
|
+
({"type": None}, "type is required"),
|
|
1066
|
+
({"type": "invalid_type"}, invalid_type_error),
|
|
1067
|
+
({"type": 123}, invalid_type_error),
|
|
1068
|
+
({"type": ""}, invalid_type_error),
|
|
1069
|
+
]
|
|
1070
|
+
|
|
1071
|
+
for data, expected_error in test_cases:
|
|
1072
|
+
with pytest.raises(ValueError, match=expected_error):
|
|
1073
|
+
ExternalToolServer.type_from_data(data)
|