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.
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 test_all_19_browser_tools_registered(self):
176
- """Verify all 19 browser tools are registered."""
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) == 19, f"Expected 19 browser tools, got {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 + 19 = 146)
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 len(tools) == 146, f"Expected 146 tools (127 + 19), got {len(tools)}"
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)