kubectl-mcp-server 1.13.0__py3-none-any.whl → 1.14.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.
- {kubectl_mcp_server-1.13.0.dist-info → kubectl_mcp_server-1.14.0.dist-info}/METADATA +1 -1
- {kubectl_mcp_server-1.13.0.dist-info → kubectl_mcp_server-1.14.0.dist-info}/RECORD +14 -11
- kubectl_mcp_tool/__init__.py +1 -1
- kubectl_mcp_tool/cli/__init__.py +53 -1
- kubectl_mcp_tool/cli/cli.py +476 -17
- kubectl_mcp_tool/cli/errors.py +262 -0
- kubectl_mcp_tool/cli/output.py +377 -0
- kubectl_mcp_tool/tools/browser.py +316 -28
- tests/test_browser.py +167 -5
- tests/test_cli.py +299 -0
- {kubectl_mcp_server-1.13.0.dist-info → kubectl_mcp_server-1.14.0.dist-info}/WHEEL +0 -0
- {kubectl_mcp_server-1.13.0.dist-info → kubectl_mcp_server-1.14.0.dist-info}/entry_points.txt +0 -0
- {kubectl_mcp_server-1.13.0.dist-info → kubectl_mcp_server-1.14.0.dist-info}/licenses/LICENSE +0 -0
- {kubectl_mcp_server-1.13.0.dist-info → kubectl_mcp_server-1.14.0.dist-info}/top_level.txt +0 -0
tests/test_browser.py
CHANGED
|
@@ -172,8 +172,8 @@ class TestBrowserToolFunctions:
|
|
|
172
172
|
assert "browser_screenshot" in tool_names
|
|
173
173
|
|
|
174
174
|
@pytest.mark.unit
|
|
175
|
-
def
|
|
176
|
-
"""Verify all
|
|
175
|
+
def test_all_26_browser_tools_registered(self):
|
|
176
|
+
"""Verify all 26 browser tools are registered (v0.7+)."""
|
|
177
177
|
from kubectl_mcp_tool.tools.browser import register_browser_tools
|
|
178
178
|
from fastmcp import FastMCP
|
|
179
179
|
import asyncio
|
|
@@ -182,9 +182,10 @@ class TestBrowserToolFunctions:
|
|
|
182
182
|
register_browser_tools(server, non_destructive=False)
|
|
183
183
|
|
|
184
184
|
tools = asyncio.run(server.list_tools())
|
|
185
|
-
assert len(tools) ==
|
|
185
|
+
assert len(tools) == 26, f"Expected 26 browser tools, got {len(tools)}"
|
|
186
186
|
|
|
187
187
|
expected_tools = [
|
|
188
|
+
# Core browser tools
|
|
188
189
|
"browser_open",
|
|
189
190
|
"browser_snapshot",
|
|
190
191
|
"browser_click",
|
|
@@ -194,6 +195,15 @@ class TestBrowserToolFunctions:
|
|
|
194
195
|
"browser_get_url",
|
|
195
196
|
"browser_wait",
|
|
196
197
|
"browser_close",
|
|
198
|
+
# NEW v0.7 tools
|
|
199
|
+
"browser_connect_cdp",
|
|
200
|
+
"browser_install",
|
|
201
|
+
"browser_set_provider",
|
|
202
|
+
"browser_session_list",
|
|
203
|
+
"browser_session_switch",
|
|
204
|
+
"browser_open_with_headers",
|
|
205
|
+
"browser_set_viewport",
|
|
206
|
+
# K8s integration tools
|
|
197
207
|
"browser_test_ingress",
|
|
198
208
|
"browser_screenshot_service",
|
|
199
209
|
"browser_screenshot_grafana",
|
|
@@ -211,6 +221,157 @@ class TestBrowserToolFunctions:
|
|
|
211
221
|
assert not missing, f"Missing browser tools: {missing}"
|
|
212
222
|
|
|
213
223
|
|
|
224
|
+
class TestBrowserV07Features:
|
|
225
|
+
"""Tests for agent-browser v0.7 features."""
|
|
226
|
+
|
|
227
|
+
@pytest.mark.unit
|
|
228
|
+
def test_get_global_options_empty(self):
|
|
229
|
+
"""Test _get_global_options with no env vars set."""
|
|
230
|
+
from kubectl_mcp_tool.tools.browser import _get_global_options
|
|
231
|
+
|
|
232
|
+
with patch.dict(os.environ, {}, clear=True):
|
|
233
|
+
# Need to reload to pick up cleared env
|
|
234
|
+
import importlib
|
|
235
|
+
import kubectl_mcp_tool.tools.browser as browser_module
|
|
236
|
+
importlib.reload(browser_module)
|
|
237
|
+
|
|
238
|
+
opts = browser_module._get_global_options()
|
|
239
|
+
# Should return empty list when no env vars set
|
|
240
|
+
assert isinstance(opts, list)
|
|
241
|
+
|
|
242
|
+
@pytest.mark.unit
|
|
243
|
+
def test_get_global_options_with_provider(self):
|
|
244
|
+
"""Test _get_global_options with cloud provider."""
|
|
245
|
+
with patch.dict(os.environ, {"MCP_BROWSER_PROVIDER": "browserbase"}):
|
|
246
|
+
import importlib
|
|
247
|
+
import kubectl_mcp_tool.tools.browser as browser_module
|
|
248
|
+
importlib.reload(browser_module)
|
|
249
|
+
|
|
250
|
+
opts = browser_module._get_global_options()
|
|
251
|
+
assert "-p" in opts
|
|
252
|
+
assert "browserbase" in opts
|
|
253
|
+
|
|
254
|
+
@pytest.mark.unit
|
|
255
|
+
def test_get_global_options_with_profile(self):
|
|
256
|
+
"""Test _get_global_options with persistent profile."""
|
|
257
|
+
with patch.dict(os.environ, {"MCP_BROWSER_PROFILE": "~/.k8s-browser"}):
|
|
258
|
+
import importlib
|
|
259
|
+
import kubectl_mcp_tool.tools.browser as browser_module
|
|
260
|
+
importlib.reload(browser_module)
|
|
261
|
+
|
|
262
|
+
opts = browser_module._get_global_options()
|
|
263
|
+
assert "--profile" in opts
|
|
264
|
+
|
|
265
|
+
@pytest.mark.unit
|
|
266
|
+
def test_get_global_options_with_session(self):
|
|
267
|
+
"""Test _get_global_options with session name."""
|
|
268
|
+
with patch.dict(os.environ, {"MCP_BROWSER_SESSION": "test-session"}):
|
|
269
|
+
import importlib
|
|
270
|
+
import kubectl_mcp_tool.tools.browser as browser_module
|
|
271
|
+
importlib.reload(browser_module)
|
|
272
|
+
|
|
273
|
+
opts = browser_module._get_global_options()
|
|
274
|
+
assert "--session" in opts
|
|
275
|
+
assert "test-session" in opts
|
|
276
|
+
|
|
277
|
+
@pytest.mark.unit
|
|
278
|
+
def test_get_global_options_with_headed(self):
|
|
279
|
+
"""Test _get_global_options with headed mode."""
|
|
280
|
+
with patch.dict(os.environ, {"MCP_BROWSER_HEADED": "true"}):
|
|
281
|
+
import importlib
|
|
282
|
+
import kubectl_mcp_tool.tools.browser as browser_module
|
|
283
|
+
importlib.reload(browser_module)
|
|
284
|
+
|
|
285
|
+
opts = browser_module._get_global_options()
|
|
286
|
+
assert "--headed" in opts
|
|
287
|
+
|
|
288
|
+
@pytest.mark.unit
|
|
289
|
+
def test_is_transient_error(self):
|
|
290
|
+
"""Test transient error detection."""
|
|
291
|
+
from kubectl_mcp_tool.tools.browser import _is_transient_error
|
|
292
|
+
|
|
293
|
+
# Transient errors
|
|
294
|
+
assert _is_transient_error("ECONNREFUSED") is True
|
|
295
|
+
assert _is_transient_error("Connection refused") is True
|
|
296
|
+
assert _is_transient_error("ETIMEDOUT") is True
|
|
297
|
+
assert _is_transient_error("timeout occurred") is True
|
|
298
|
+
|
|
299
|
+
# Non-transient errors
|
|
300
|
+
assert _is_transient_error("File not found") is False
|
|
301
|
+
assert _is_transient_error("Invalid argument") is False
|
|
302
|
+
|
|
303
|
+
@pytest.mark.unit
|
|
304
|
+
def test_run_browser_with_retry_success(self):
|
|
305
|
+
"""Test retry logic with successful result."""
|
|
306
|
+
from kubectl_mcp_tool.tools.browser import _run_browser_with_retry
|
|
307
|
+
|
|
308
|
+
with patch("kubectl_mcp_tool.tools.browser._run_browser") as mock_run:
|
|
309
|
+
mock_run.return_value = {"success": True, "output": "OK"}
|
|
310
|
+
|
|
311
|
+
result = _run_browser_with_retry(["open", "https://example.com"])
|
|
312
|
+
|
|
313
|
+
assert result["success"] is True
|
|
314
|
+
# Should only call once on success
|
|
315
|
+
assert mock_run.call_count == 1
|
|
316
|
+
|
|
317
|
+
@pytest.mark.unit
|
|
318
|
+
def test_run_browser_with_retry_transient_error(self):
|
|
319
|
+
"""Test retry logic with transient error."""
|
|
320
|
+
from kubectl_mcp_tool.tools.browser import _run_browser_with_retry
|
|
321
|
+
|
|
322
|
+
with patch("kubectl_mcp_tool.tools.browser._run_browser") as mock_run:
|
|
323
|
+
with patch("time.sleep"): # Skip actual sleep
|
|
324
|
+
# First two calls fail with transient error, third succeeds
|
|
325
|
+
mock_run.side_effect = [
|
|
326
|
+
{"success": False, "error": "ECONNREFUSED"},
|
|
327
|
+
{"success": False, "error": "ECONNREFUSED"},
|
|
328
|
+
{"success": True, "output": "OK"},
|
|
329
|
+
]
|
|
330
|
+
|
|
331
|
+
result = _run_browser_with_retry(["open", "https://example.com"], max_retries=3)
|
|
332
|
+
|
|
333
|
+
assert result["success"] is True
|
|
334
|
+
assert mock_run.call_count == 3
|
|
335
|
+
|
|
336
|
+
@pytest.mark.unit
|
|
337
|
+
def test_run_browser_with_retry_non_transient_error(self):
|
|
338
|
+
"""Test retry logic with non-transient error (no retry)."""
|
|
339
|
+
from kubectl_mcp_tool.tools.browser import _run_browser_with_retry
|
|
340
|
+
|
|
341
|
+
with patch("kubectl_mcp_tool.tools.browser._run_browser") as mock_run:
|
|
342
|
+
mock_run.return_value = {"success": False, "error": "Invalid argument"}
|
|
343
|
+
|
|
344
|
+
result = _run_browser_with_retry(["open", "https://example.com"])
|
|
345
|
+
|
|
346
|
+
assert result["success"] is False
|
|
347
|
+
# Should not retry non-transient errors
|
|
348
|
+
assert mock_run.call_count == 1
|
|
349
|
+
|
|
350
|
+
@pytest.mark.unit
|
|
351
|
+
def test_debug_mode(self):
|
|
352
|
+
"""Test debug logging."""
|
|
353
|
+
with patch.dict(os.environ, {"MCP_BROWSER_DEBUG": "true"}):
|
|
354
|
+
import importlib
|
|
355
|
+
import kubectl_mcp_tool.tools.browser as browser_module
|
|
356
|
+
importlib.reload(browser_module)
|
|
357
|
+
|
|
358
|
+
assert browser_module.MCP_BROWSER_DEBUG is True
|
|
359
|
+
|
|
360
|
+
@pytest.mark.unit
|
|
361
|
+
def test_retry_configuration(self):
|
|
362
|
+
"""Test retry configuration from environment."""
|
|
363
|
+
with patch.dict(os.environ, {
|
|
364
|
+
"MCP_BROWSER_MAX_RETRIES": "5",
|
|
365
|
+
"MCP_BROWSER_RETRY_DELAY": "2000"
|
|
366
|
+
}):
|
|
367
|
+
import importlib
|
|
368
|
+
import kubectl_mcp_tool.tools.browser as browser_module
|
|
369
|
+
importlib.reload(browser_module)
|
|
370
|
+
|
|
371
|
+
assert browser_module.MCP_BROWSER_MAX_RETRIES == 5
|
|
372
|
+
assert browser_module.MCP_BROWSER_RETRY_DELAY == 2000
|
|
373
|
+
|
|
374
|
+
|
|
214
375
|
class TestK8sIntegration:
|
|
215
376
|
"""Tests for Kubernetes-specific browser tools."""
|
|
216
377
|
|
|
@@ -340,10 +501,11 @@ class TestServerIntegration:
|
|
|
340
501
|
tools = asyncio.run(server.server.list_tools())
|
|
341
502
|
tool_names = [t.name for t in tools]
|
|
342
503
|
|
|
343
|
-
# Should have browser tools (127 +
|
|
504
|
+
# Should have browser tools (127 + 26 = 153)
|
|
344
505
|
assert "browser_open" in tool_names
|
|
345
506
|
assert "browser_screenshot" in tool_names
|
|
346
|
-
assert
|
|
507
|
+
assert "browser_connect_cdp" in tool_names # v0.7 tool
|
|
508
|
+
assert len(tools) == 153, f"Expected 153 tools (127 + 26), got {len(tools)}"
|
|
347
509
|
|
|
348
510
|
|
|
349
511
|
import asyncio
|
tests/test_cli.py
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""Unit tests for the enhanced CLI module."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import json
|
|
7
|
+
from unittest.mock import patch, MagicMock
|
|
8
|
+
from io import StringIO
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestCliErrors:
|
|
12
|
+
"""Tests for CLI error handling module."""
|
|
13
|
+
|
|
14
|
+
@pytest.mark.unit
|
|
15
|
+
def test_error_code_values(self):
|
|
16
|
+
"""Test ErrorCode enum values."""
|
|
17
|
+
from kubectl_mcp_tool.cli.errors import ErrorCode
|
|
18
|
+
|
|
19
|
+
assert ErrorCode.SUCCESS == 0
|
|
20
|
+
assert ErrorCode.CLIENT_ERROR == 1
|
|
21
|
+
assert ErrorCode.SERVER_ERROR == 2
|
|
22
|
+
assert ErrorCode.K8S_ERROR == 3
|
|
23
|
+
assert ErrorCode.BROWSER_ERROR == 4
|
|
24
|
+
assert ErrorCode.NETWORK_ERROR == 5
|
|
25
|
+
|
|
26
|
+
@pytest.mark.unit
|
|
27
|
+
def test_cli_error_dataclass(self):
|
|
28
|
+
"""Test CliError dataclass."""
|
|
29
|
+
from kubectl_mcp_tool.cli.errors import CliError, ErrorCode
|
|
30
|
+
|
|
31
|
+
error = CliError(
|
|
32
|
+
code=ErrorCode.CLIENT_ERROR,
|
|
33
|
+
type="TEST_ERROR",
|
|
34
|
+
message="Test message",
|
|
35
|
+
details="Test details",
|
|
36
|
+
suggestion="Test suggestion"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
assert error.code == ErrorCode.CLIENT_ERROR
|
|
40
|
+
assert error.type == "TEST_ERROR"
|
|
41
|
+
assert error.message == "Test message"
|
|
42
|
+
assert error.details == "Test details"
|
|
43
|
+
assert error.suggestion == "Test suggestion"
|
|
44
|
+
|
|
45
|
+
@pytest.mark.unit
|
|
46
|
+
def test_format_cli_error(self):
|
|
47
|
+
"""Test format_cli_error function."""
|
|
48
|
+
from kubectl_mcp_tool.cli.errors import CliError, ErrorCode, format_cli_error
|
|
49
|
+
|
|
50
|
+
error = CliError(
|
|
51
|
+
code=ErrorCode.CLIENT_ERROR,
|
|
52
|
+
type="TEST_ERROR",
|
|
53
|
+
message="Something went wrong",
|
|
54
|
+
details="More info here",
|
|
55
|
+
suggestion="Try this instead"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
formatted = format_cli_error(error)
|
|
59
|
+
|
|
60
|
+
assert "Error [TEST_ERROR]: Something went wrong" in formatted
|
|
61
|
+
assert "Details: More info here" in formatted
|
|
62
|
+
assert "Suggestion: Try this instead" in formatted
|
|
63
|
+
|
|
64
|
+
@pytest.mark.unit
|
|
65
|
+
def test_tool_not_found_error(self):
|
|
66
|
+
"""Test tool_not_found_error factory."""
|
|
67
|
+
from kubectl_mcp_tool.cli.errors import tool_not_found_error, ErrorCode
|
|
68
|
+
|
|
69
|
+
error = tool_not_found_error("nonexistent_tool", ["get_pods", "list_namespaces"])
|
|
70
|
+
|
|
71
|
+
assert error.code == ErrorCode.CLIENT_ERROR
|
|
72
|
+
assert error.type == "TOOL_NOT_FOUND"
|
|
73
|
+
assert "nonexistent_tool" in error.message
|
|
74
|
+
assert "get_pods" in error.details
|
|
75
|
+
|
|
76
|
+
@pytest.mark.unit
|
|
77
|
+
def test_invalid_json_error(self):
|
|
78
|
+
"""Test invalid_json_error factory."""
|
|
79
|
+
from kubectl_mcp_tool.cli.errors import invalid_json_error, ErrorCode
|
|
80
|
+
|
|
81
|
+
error = invalid_json_error("{invalid json}", "Expecting value")
|
|
82
|
+
|
|
83
|
+
assert error.code == ErrorCode.CLIENT_ERROR
|
|
84
|
+
assert error.type == "INVALID_JSON"
|
|
85
|
+
assert "Expecting value" in error.details
|
|
86
|
+
|
|
87
|
+
@pytest.mark.unit
|
|
88
|
+
def test_unknown_subcommand_error(self):
|
|
89
|
+
"""Test unknown_subcommand_error factory with suggestions."""
|
|
90
|
+
from kubectl_mcp_tool.cli.errors import unknown_subcommand_error
|
|
91
|
+
|
|
92
|
+
# Test known alias
|
|
93
|
+
error = unknown_subcommand_error("run")
|
|
94
|
+
assert "call" in error.suggestion
|
|
95
|
+
|
|
96
|
+
# Test unknown command
|
|
97
|
+
error = unknown_subcommand_error("unknown")
|
|
98
|
+
assert "help" in error.suggestion.lower()
|
|
99
|
+
|
|
100
|
+
@pytest.mark.unit
|
|
101
|
+
def test_browser_not_found_error(self):
|
|
102
|
+
"""Test browser_not_found_error factory."""
|
|
103
|
+
from kubectl_mcp_tool.cli.errors import browser_not_found_error, ErrorCode
|
|
104
|
+
|
|
105
|
+
error = browser_not_found_error()
|
|
106
|
+
|
|
107
|
+
assert error.code == ErrorCode.BROWSER_ERROR
|
|
108
|
+
assert "agent-browser" in error.message
|
|
109
|
+
assert "npm install" in error.suggestion
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class TestCliOutput:
|
|
113
|
+
"""Tests for CLI output formatting module."""
|
|
114
|
+
|
|
115
|
+
@pytest.mark.unit
|
|
116
|
+
def test_should_colorize_respects_no_color(self):
|
|
117
|
+
"""Test that NO_COLOR env var disables colors."""
|
|
118
|
+
from kubectl_mcp_tool.cli.output import should_colorize
|
|
119
|
+
|
|
120
|
+
with patch.dict(os.environ, {"NO_COLOR": "1"}):
|
|
121
|
+
assert should_colorize() is False
|
|
122
|
+
|
|
123
|
+
@pytest.mark.unit
|
|
124
|
+
def test_format_tools_list_json(self):
|
|
125
|
+
"""Test format_tools_list with JSON output."""
|
|
126
|
+
from kubectl_mcp_tool.cli.output import format_tools_list
|
|
127
|
+
|
|
128
|
+
tools = [
|
|
129
|
+
{"name": "get_pods", "description": "Get pods", "category": "pods"},
|
|
130
|
+
{"name": "list_namespaces", "description": "List namespaces", "category": "core"},
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
result = format_tools_list(tools, as_json=True)
|
|
134
|
+
parsed = json.loads(result)
|
|
135
|
+
|
|
136
|
+
assert len(parsed) == 2
|
|
137
|
+
assert parsed[0]["name"] == "get_pods"
|
|
138
|
+
|
|
139
|
+
@pytest.mark.unit
|
|
140
|
+
def test_format_tools_list_text(self):
|
|
141
|
+
"""Test format_tools_list with text output."""
|
|
142
|
+
from kubectl_mcp_tool.cli.output import format_tools_list
|
|
143
|
+
|
|
144
|
+
tools = [
|
|
145
|
+
{"name": "get_pods", "description": "Get pods", "category": "pods"},
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
# Disable colors for predictable output
|
|
149
|
+
with patch.dict(os.environ, {"NO_COLOR": "1"}):
|
|
150
|
+
result = format_tools_list(tools, with_descriptions=True)
|
|
151
|
+
|
|
152
|
+
assert "get_pods" in result
|
|
153
|
+
assert "Get pods" in result
|
|
154
|
+
|
|
155
|
+
@pytest.mark.unit
|
|
156
|
+
def test_format_tool_schema(self):
|
|
157
|
+
"""Test format_tool_schema function."""
|
|
158
|
+
from kubectl_mcp_tool.cli.output import format_tool_schema
|
|
159
|
+
|
|
160
|
+
tool = {
|
|
161
|
+
"name": "get_pods",
|
|
162
|
+
"description": "Get pods in a namespace",
|
|
163
|
+
"inputSchema": {
|
|
164
|
+
"type": "object",
|
|
165
|
+
"properties": {
|
|
166
|
+
"namespace": {"type": "string", "description": "Namespace name"}
|
|
167
|
+
},
|
|
168
|
+
"required": ["namespace"]
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
with patch.dict(os.environ, {"NO_COLOR": "1"}):
|
|
173
|
+
result = format_tool_schema(tool)
|
|
174
|
+
|
|
175
|
+
assert "get_pods" in result
|
|
176
|
+
assert "Get pods in a namespace" in result
|
|
177
|
+
assert "namespace" in result
|
|
178
|
+
assert "required" in result
|
|
179
|
+
|
|
180
|
+
@pytest.mark.unit
|
|
181
|
+
def test_format_server_info(self):
|
|
182
|
+
"""Test format_server_info function."""
|
|
183
|
+
from kubectl_mcp_tool.cli.output import format_server_info
|
|
184
|
+
|
|
185
|
+
with patch.dict(os.environ, {"NO_COLOR": "1"}):
|
|
186
|
+
result = format_server_info(
|
|
187
|
+
version="1.14.0",
|
|
188
|
+
tool_count=127,
|
|
189
|
+
resource_count=8,
|
|
190
|
+
prompt_count=8,
|
|
191
|
+
context="minikube"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
assert "1.14.0" in result
|
|
195
|
+
assert "127" in result
|
|
196
|
+
assert "minikube" in result
|
|
197
|
+
|
|
198
|
+
@pytest.mark.unit
|
|
199
|
+
def test_format_doctor_results(self):
|
|
200
|
+
"""Test format_doctor_results function."""
|
|
201
|
+
from kubectl_mcp_tool.cli.output import format_doctor_results
|
|
202
|
+
|
|
203
|
+
checks = [
|
|
204
|
+
{"name": "kubectl", "status": "ok", "version": "v1.28.0"},
|
|
205
|
+
{"name": "helm", "status": "warning", "details": "Not installed"},
|
|
206
|
+
{"name": "kubernetes", "status": "error", "details": "Connection failed"},
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
with patch.dict(os.environ, {"NO_COLOR": "1"}):
|
|
210
|
+
result = format_doctor_results(checks)
|
|
211
|
+
|
|
212
|
+
assert "kubectl" in result
|
|
213
|
+
assert "v1.28.0" in result
|
|
214
|
+
assert "Some checks failed" in result
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class TestCliCommands:
|
|
218
|
+
"""Tests for CLI command handlers."""
|
|
219
|
+
|
|
220
|
+
@pytest.mark.unit
|
|
221
|
+
def test_main_help(self):
|
|
222
|
+
"""Test that --help works."""
|
|
223
|
+
from kubectl_mcp_tool.cli.cli import main
|
|
224
|
+
|
|
225
|
+
with patch.object(sys, 'argv', ['kubectl-mcp-server', '--help']):
|
|
226
|
+
with pytest.raises(SystemExit) as exc_info:
|
|
227
|
+
main()
|
|
228
|
+
# argparse exits with 0 for --help
|
|
229
|
+
assert exc_info.value.code == 0
|
|
230
|
+
|
|
231
|
+
@pytest.mark.unit
|
|
232
|
+
def test_get_tool_category(self):
|
|
233
|
+
"""Test tool category detection."""
|
|
234
|
+
from kubectl_mcp_tool.cli.cli import _get_tool_category
|
|
235
|
+
|
|
236
|
+
assert _get_tool_category("get_pods") == "pods"
|
|
237
|
+
assert _get_tool_category("list_deployments") == "deployments"
|
|
238
|
+
assert _get_tool_category("helm_install") == "helm"
|
|
239
|
+
assert _get_tool_category("browser_open") == "browser"
|
|
240
|
+
assert _get_tool_category("unknown_tool") == "other"
|
|
241
|
+
|
|
242
|
+
@pytest.mark.unit
|
|
243
|
+
def test_cmd_doctor_checks_kubectl(self):
|
|
244
|
+
"""Test that doctor command checks for kubectl."""
|
|
245
|
+
from kubectl_mcp_tool.cli.cli import cmd_doctor
|
|
246
|
+
|
|
247
|
+
args = MagicMock()
|
|
248
|
+
args.json = True
|
|
249
|
+
|
|
250
|
+
# Mock shutil.which to simulate kubectl present
|
|
251
|
+
with patch("shutil.which") as mock_which:
|
|
252
|
+
mock_which.side_effect = lambda x: f"/usr/bin/{x}" if x == "kubectl" else None
|
|
253
|
+
|
|
254
|
+
with patch("subprocess.run") as mock_run:
|
|
255
|
+
mock_run.return_value = MagicMock(
|
|
256
|
+
returncode=0,
|
|
257
|
+
stdout='{"clientVersion": {"gitVersion": "v1.28.0"}}'
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
with patch("kubectl_mcp_tool.cli.cli.format_doctor_results") as mock_format:
|
|
261
|
+
mock_format.return_value = "{}"
|
|
262
|
+
cmd_doctor(args)
|
|
263
|
+
|
|
264
|
+
# Should have called shutil.which for kubectl
|
|
265
|
+
mock_which.assert_any_call("kubectl")
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class TestCliIntegration:
|
|
269
|
+
"""Integration tests for CLI module."""
|
|
270
|
+
|
|
271
|
+
@pytest.mark.unit
|
|
272
|
+
def test_cli_module_imports(self):
|
|
273
|
+
"""Test that all CLI modules can be imported."""
|
|
274
|
+
from kubectl_mcp_tool.cli import (
|
|
275
|
+
main,
|
|
276
|
+
CliError,
|
|
277
|
+
ErrorCode,
|
|
278
|
+
format_cli_error,
|
|
279
|
+
format_tools_list,
|
|
280
|
+
format_server_info,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
assert main is not None
|
|
284
|
+
assert CliError is not None
|
|
285
|
+
assert ErrorCode is not None
|
|
286
|
+
|
|
287
|
+
@pytest.mark.unit
|
|
288
|
+
def test_error_str_representation(self):
|
|
289
|
+
"""Test CliError __str__ method."""
|
|
290
|
+
from kubectl_mcp_tool.cli.errors import CliError, ErrorCode
|
|
291
|
+
|
|
292
|
+
error = CliError(
|
|
293
|
+
code=ErrorCode.CLIENT_ERROR,
|
|
294
|
+
type="TEST",
|
|
295
|
+
message="Test message"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
assert "TEST" in str(error)
|
|
299
|
+
assert "Test message" in str(error)
|
|
File without changes
|
{kubectl_mcp_server-1.13.0.dist-info → kubectl_mcp_server-1.14.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{kubectl_mcp_server-1.13.0.dist-info → kubectl_mcp_server-1.14.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|