kubectl-mcp-server 1.19.2__py3-none-any.whl → 1.21.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_kind.py ADDED
@@ -0,0 +1,1206 @@
1
+ """
2
+ Unit tests for kind (Kubernetes IN Docker) tools.
3
+
4
+ This module tests the kind local cluster management toolset.
5
+ """
6
+
7
+ import pytest
8
+ import json
9
+ from unittest.mock import patch, MagicMock
10
+ import subprocess
11
+
12
+
13
+ class TestKindHelpers:
14
+ """Tests for kind helper functions."""
15
+
16
+ @pytest.mark.unit
17
+ def test_kind_module_imports(self):
18
+ """Test that kind module can be imported."""
19
+ from kubectl_mcp_tool.tools.kind import (
20
+ register_kind_tools,
21
+ _kind_available,
22
+ _get_kind_version,
23
+ _run_kind,
24
+ _run_docker,
25
+ kind_detect,
26
+ kind_version,
27
+ kind_list_clusters,
28
+ kind_get_nodes,
29
+ kind_get_kubeconfig,
30
+ kind_export_logs,
31
+ kind_create_cluster,
32
+ kind_delete_cluster,
33
+ kind_delete_all_clusters,
34
+ kind_load_image,
35
+ kind_load_image_archive,
36
+ kind_build_node_image,
37
+ kind_cluster_info,
38
+ kind_node_labels,
39
+ kind_config_validate,
40
+ kind_config_generate,
41
+ kind_config_show,
42
+ kind_available_images,
43
+ kind_registry_create,
44
+ kind_registry_connect,
45
+ kind_registry_status,
46
+ kind_node_exec,
47
+ kind_node_logs,
48
+ kind_node_inspect,
49
+ kind_node_restart,
50
+ kind_network_inspect,
51
+ kind_port_mappings,
52
+ kind_ingress_setup,
53
+ kind_cluster_status,
54
+ kind_images_list,
55
+ kind_provider_info,
56
+ )
57
+ assert callable(register_kind_tools)
58
+ assert callable(_kind_available)
59
+ assert callable(_get_kind_version)
60
+ assert callable(_run_kind)
61
+ assert callable(_run_docker)
62
+ assert callable(kind_detect)
63
+ assert callable(kind_version)
64
+ assert callable(kind_list_clusters)
65
+ assert callable(kind_get_nodes)
66
+ assert callable(kind_get_kubeconfig)
67
+ assert callable(kind_export_logs)
68
+ assert callable(kind_create_cluster)
69
+ assert callable(kind_delete_cluster)
70
+ assert callable(kind_delete_all_clusters)
71
+ assert callable(kind_load_image)
72
+ assert callable(kind_load_image_archive)
73
+ assert callable(kind_build_node_image)
74
+ assert callable(kind_cluster_info)
75
+ assert callable(kind_node_labels)
76
+ assert callable(kind_config_validate)
77
+ assert callable(kind_config_generate)
78
+ assert callable(kind_config_show)
79
+ assert callable(kind_available_images)
80
+ assert callable(kind_registry_create)
81
+ assert callable(kind_registry_connect)
82
+ assert callable(kind_registry_status)
83
+ assert callable(kind_node_exec)
84
+ assert callable(kind_node_logs)
85
+ assert callable(kind_node_inspect)
86
+ assert callable(kind_node_restart)
87
+ assert callable(kind_network_inspect)
88
+ assert callable(kind_port_mappings)
89
+ assert callable(kind_ingress_setup)
90
+ assert callable(kind_cluster_status)
91
+ assert callable(kind_images_list)
92
+ assert callable(kind_provider_info)
93
+
94
+ @pytest.mark.unit
95
+ def test_kind_available_when_installed(self):
96
+ """Test _kind_available returns True when CLI is installed."""
97
+ from kubectl_mcp_tool.tools.kind import _kind_available
98
+
99
+ with patch("subprocess.run") as mock_run:
100
+ mock_run.return_value = MagicMock(returncode=0)
101
+ result = _kind_available()
102
+ assert result is True
103
+
104
+ @pytest.mark.unit
105
+ def test_kind_available_when_not_installed(self):
106
+ """Test _kind_available returns False when CLI is not installed."""
107
+ from kubectl_mcp_tool.tools.kind import _kind_available
108
+
109
+ with patch("subprocess.run") as mock_run:
110
+ mock_run.side_effect = FileNotFoundError()
111
+ result = _kind_available()
112
+ assert result is False
113
+
114
+ @pytest.mark.unit
115
+ def test_get_kind_version(self):
116
+ """Test _get_kind_version extracts version correctly."""
117
+ from kubectl_mcp_tool.tools.kind import _get_kind_version
118
+
119
+ with patch("subprocess.run") as mock_run:
120
+ mock_run.return_value = MagicMock(
121
+ returncode=0,
122
+ stdout="kind v0.23.0 go1.21.0 darwin/arm64"
123
+ )
124
+ result = _get_kind_version()
125
+ assert result == "v0.23.0"
126
+
127
+ @pytest.mark.unit
128
+ def test_get_kind_version_not_installed(self):
129
+ """Test _get_kind_version returns None when not installed."""
130
+ from kubectl_mcp_tool.tools.kind import _get_kind_version
131
+
132
+ with patch("subprocess.run") as mock_run:
133
+ mock_run.side_effect = FileNotFoundError()
134
+ result = _get_kind_version()
135
+ assert result is None
136
+
137
+ @pytest.mark.unit
138
+ def test_run_kind_not_available(self):
139
+ """Test _run_kind returns error when CLI not available."""
140
+ from kubectl_mcp_tool.tools.kind import _run_kind
141
+
142
+ with patch("subprocess.run") as mock_run:
143
+ mock_run.side_effect = FileNotFoundError()
144
+ result = _run_kind(["get", "clusters"])
145
+ assert result["success"] is False
146
+ assert "not available" in result["error"]
147
+
148
+ @pytest.mark.unit
149
+ def test_run_kind_success(self):
150
+ """Test _run_kind returns success on successful command."""
151
+ from kubectl_mcp_tool.tools.kind import _run_kind
152
+
153
+ with patch("subprocess.run") as mock_run:
154
+ mock_run.return_value = MagicMock(
155
+ returncode=0,
156
+ stdout="test-cluster",
157
+ stderr=""
158
+ )
159
+ result = _run_kind(["get", "clusters"])
160
+ assert result["success"] is True
161
+ assert result["output"] == "test-cluster"
162
+
163
+ @pytest.mark.unit
164
+ def test_run_kind_timeout(self):
165
+ """Test _run_kind handles timeout."""
166
+ from kubectl_mcp_tool.tools.kind import _run_kind
167
+
168
+ with patch("subprocess.run") as mock_run:
169
+ mock_run.side_effect = [
170
+ MagicMock(returncode=0),
171
+ subprocess.TimeoutExpired(cmd="kind", timeout=300)
172
+ ]
173
+ result = _run_kind(["create", "cluster"])
174
+ assert result["success"] is False
175
+ assert "timed out" in result["error"]
176
+
177
+
178
+ class TestKindDetect:
179
+ """Tests for kind_detect function."""
180
+
181
+ @pytest.mark.unit
182
+ def test_kind_detect_installed(self):
183
+ """Test kind_detect when kind is installed."""
184
+ from kubectl_mcp_tool.tools.kind import kind_detect
185
+
186
+ with patch("subprocess.run") as mock_run:
187
+ mock_run.return_value = MagicMock(
188
+ returncode=0,
189
+ stdout="kind v0.23.0 go1.21.0 darwin/arm64"
190
+ )
191
+ result = kind_detect()
192
+ assert result["installed"] is True
193
+ assert result["cli_available"] is True
194
+ assert result["version"] == "v0.23.0"
195
+ assert result["install_instructions"] is None
196
+
197
+ @pytest.mark.unit
198
+ def test_kind_detect_not_installed(self):
199
+ """Test kind_detect when kind is not installed."""
200
+ from kubectl_mcp_tool.tools.kind import kind_detect
201
+
202
+ with patch("subprocess.run") as mock_run:
203
+ mock_run.side_effect = FileNotFoundError()
204
+ result = kind_detect()
205
+ assert result["installed"] is False
206
+ assert result["cli_available"] is False
207
+ assert result["version"] is None
208
+ assert result["install_instructions"] is not None
209
+
210
+
211
+ class TestKindListClusters:
212
+ """Tests for kind_list_clusters function."""
213
+
214
+ @pytest.mark.unit
215
+ def test_kind_list_clusters_success(self):
216
+ """Test kind_list_clusters returns cluster list."""
217
+ from kubectl_mcp_tool.tools.kind import kind_list_clusters
218
+
219
+ with patch("kubectl_mcp_tool.tools.kind._kind_available", return_value=True):
220
+ with patch("kubectl_mcp_tool.tools.kind.subprocess.run") as mock_run:
221
+ mock_run.return_value = MagicMock(
222
+ returncode=0,
223
+ stdout="dev-cluster\ntest-cluster",
224
+ stderr=""
225
+ )
226
+ result = kind_list_clusters()
227
+ assert result["success"] is True
228
+ assert result["total"] == 2
229
+ assert "dev-cluster" in result["clusters"]
230
+ assert "test-cluster" in result["clusters"]
231
+
232
+ @pytest.mark.unit
233
+ def test_kind_list_clusters_empty(self):
234
+ """Test kind_list_clusters returns empty list."""
235
+ from kubectl_mcp_tool.tools.kind import kind_list_clusters
236
+
237
+ with patch("kubectl_mcp_tool.tools.kind._kind_available", return_value=True):
238
+ with patch("kubectl_mcp_tool.tools.kind.subprocess.run") as mock_run:
239
+ mock_run.return_value = MagicMock(
240
+ returncode=0,
241
+ stdout="",
242
+ stderr=""
243
+ )
244
+ result = kind_list_clusters()
245
+ assert result["success"] is True
246
+ assert result["total"] == 0
247
+
248
+
249
+ class TestKindGetNodes:
250
+ """Tests for kind_get_nodes function."""
251
+
252
+ @pytest.mark.unit
253
+ def test_kind_get_nodes_success(self):
254
+ """Test kind_get_nodes returns node list."""
255
+ from kubectl_mcp_tool.tools.kind import kind_get_nodes
256
+
257
+ with patch("kubectl_mcp_tool.tools.kind._kind_available", return_value=True):
258
+ with patch("kubectl_mcp_tool.tools.kind.subprocess.run") as mock_run:
259
+ mock_run.return_value = MagicMock(
260
+ returncode=0,
261
+ stdout="kind-control-plane\nkind-worker\nkind-worker2",
262
+ stderr=""
263
+ )
264
+ result = kind_get_nodes(name="kind")
265
+ assert result["success"] is True
266
+ assert result["total"] == 3
267
+ assert "kind-control-plane" in result["nodes"]
268
+
269
+
270
+ class TestKindCreateCluster:
271
+ """Tests for kind_create_cluster function."""
272
+
273
+ @pytest.mark.unit
274
+ def test_kind_create_cluster_basic(self):
275
+ """Test kind_create_cluster with basic options."""
276
+ from kubectl_mcp_tool.tools.kind import kind_create_cluster
277
+
278
+ with patch("subprocess.run") as mock_run:
279
+ mock_run.return_value = MagicMock(
280
+ returncode=0,
281
+ stdout="Creating cluster \"test\" ...",
282
+ stderr=""
283
+ )
284
+ result = kind_create_cluster(name="test")
285
+ assert result["success"] is True
286
+ assert "created" in result["message"].lower()
287
+
288
+ @pytest.mark.unit
289
+ def test_kind_create_cluster_with_options(self):
290
+ """Test kind_create_cluster with all options."""
291
+ from kubectl_mcp_tool.tools.kind import kind_create_cluster
292
+
293
+ with patch("subprocess.run") as mock_run:
294
+ mock_run.return_value = MagicMock(
295
+ returncode=0,
296
+ stdout="Creating cluster \"prod\" ...",
297
+ stderr=""
298
+ )
299
+ result = kind_create_cluster(
300
+ name="prod",
301
+ image="kindest/node:v1.29.0",
302
+ wait="10m"
303
+ )
304
+ assert result["success"] is True
305
+
306
+
307
+ class TestKindDeleteCluster:
308
+ """Tests for kind_delete_cluster function."""
309
+
310
+ @pytest.mark.unit
311
+ def test_kind_delete_cluster_success(self):
312
+ """Test kind_delete_cluster deletes cluster."""
313
+ from kubectl_mcp_tool.tools.kind import kind_delete_cluster
314
+
315
+ with patch("subprocess.run") as mock_run:
316
+ mock_run.return_value = MagicMock(
317
+ returncode=0,
318
+ stdout="Deleting cluster \"test\" ...",
319
+ stderr=""
320
+ )
321
+ result = kind_delete_cluster(name="test")
322
+ assert result["success"] is True
323
+ assert "deleted" in result["message"].lower()
324
+
325
+
326
+ class TestKindDeleteAllClusters:
327
+ """Tests for kind_delete_all_clusters function."""
328
+
329
+ @pytest.mark.unit
330
+ def test_kind_delete_all_clusters_success(self):
331
+ """Test kind_delete_all_clusters deletes all clusters."""
332
+ from kubectl_mcp_tool.tools.kind import kind_delete_all_clusters
333
+
334
+ with patch("subprocess.run") as mock_run:
335
+ mock_run.return_value = MagicMock(
336
+ returncode=0,
337
+ stdout="Deleted clusters: [\"test1\" \"test2\"]",
338
+ stderr=""
339
+ )
340
+ result = kind_delete_all_clusters()
341
+ assert result["success"] is True
342
+ assert "deleted" in result["message"].lower()
343
+
344
+
345
+ class TestKindLoadImage:
346
+ """Tests for kind_load_image function."""
347
+
348
+ @pytest.mark.unit
349
+ def test_kind_load_image_success(self):
350
+ """Test kind_load_image loads images."""
351
+ from kubectl_mcp_tool.tools.kind import kind_load_image
352
+
353
+ with patch("subprocess.run") as mock_run:
354
+ mock_run.return_value = MagicMock(
355
+ returncode=0,
356
+ stdout="Image loaded",
357
+ stderr=""
358
+ )
359
+ result = kind_load_image(images=["myapp:latest"], name="test")
360
+ assert result["success"] is True
361
+ assert result["images"] == ["myapp:latest"]
362
+
363
+ @pytest.mark.unit
364
+ def test_kind_load_image_no_images(self):
365
+ """Test kind_load_image with no images."""
366
+ from kubectl_mcp_tool.tools.kind import kind_load_image
367
+
368
+ result = kind_load_image(images=[], name="test")
369
+ assert result["success"] is False
370
+ assert "no images" in result["error"].lower()
371
+
372
+
373
+ class TestKindLoadImageArchive:
374
+ """Tests for kind_load_image_archive function."""
375
+
376
+ @pytest.mark.unit
377
+ def test_kind_load_image_archive_file_not_found(self):
378
+ """Test kind_load_image_archive with missing file."""
379
+ from kubectl_mcp_tool.tools.kind import kind_load_image_archive
380
+
381
+ result = kind_load_image_archive(archive="/nonexistent/file.tar", name="test")
382
+ assert result["success"] is False
383
+ assert "not found" in result["error"].lower()
384
+
385
+
386
+ class TestKindToolsRegistration:
387
+ """Tests for kind tools registration."""
388
+
389
+ @pytest.mark.unit
390
+ def test_kind_tools_import(self):
391
+ """Test that kind tools can be imported."""
392
+ from kubectl_mcp_tool.tools.kind import register_kind_tools
393
+ assert callable(register_kind_tools)
394
+
395
+ @pytest.mark.unit
396
+ @pytest.mark.asyncio
397
+ async def test_kind_tools_register(self, mock_all_kubernetes_apis):
398
+ """Test that kind tools register correctly."""
399
+ from kubectl_mcp_tool.mcp_server import MCPServer
400
+
401
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
402
+ server = MCPServer(name="test")
403
+
404
+ tools = await server.server.list_tools()
405
+ tool_names = {t.name for t in tools}
406
+
407
+ kind_tools = [
408
+ "kind_detect_tool",
409
+ "kind_version_tool",
410
+ "kind_list_clusters_tool",
411
+ "kind_get_nodes_tool",
412
+ "kind_get_kubeconfig_tool",
413
+ "kind_export_logs_tool",
414
+ "kind_cluster_info_tool",
415
+ "kind_node_labels_tool",
416
+ "kind_create_cluster_tool",
417
+ "kind_delete_cluster_tool",
418
+ "kind_delete_all_clusters_tool",
419
+ "kind_load_image_tool",
420
+ "kind_load_image_archive_tool",
421
+ "kind_build_node_image_tool",
422
+ "kind_set_kubeconfig_tool",
423
+ "kind_config_validate_tool",
424
+ "kind_config_generate_tool",
425
+ "kind_config_show_tool",
426
+ "kind_available_images_tool",
427
+ "kind_registry_create_tool",
428
+ "kind_registry_connect_tool",
429
+ "kind_registry_status_tool",
430
+ "kind_node_exec_tool",
431
+ "kind_node_logs_tool",
432
+ "kind_node_inspect_tool",
433
+ "kind_node_restart_tool",
434
+ "kind_network_inspect_tool",
435
+ "kind_port_mappings_tool",
436
+ "kind_ingress_setup_tool",
437
+ "kind_cluster_status_tool",
438
+ "kind_images_list_tool",
439
+ "kind_provider_info_tool",
440
+ ]
441
+ for tool in kind_tools:
442
+ assert tool in tool_names, f"kind tool '{tool}' not registered"
443
+
444
+ @pytest.mark.unit
445
+ @pytest.mark.asyncio
446
+ async def test_kind_tool_count(self, mock_all_kubernetes_apis):
447
+ """Test that correct number of kind tools are registered."""
448
+ from kubectl_mcp_tool.mcp_server import MCPServer
449
+
450
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
451
+ server = MCPServer(name="test")
452
+
453
+ tools = await server.server.list_tools()
454
+ tool_names = {t.name for t in tools}
455
+ kind_tools = [name for name in tool_names if name.startswith("kind_")]
456
+ assert len(kind_tools) == 32, f"Expected 32 kind tools, got {len(kind_tools)}: {kind_tools}"
457
+
458
+ @pytest.mark.unit
459
+ def test_kind_non_destructive_mode(self, mock_all_kubernetes_apis):
460
+ """Test that kind write operations are blocked in non-destructive mode."""
461
+ from kubectl_mcp_tool.mcp_server import MCPServer
462
+
463
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
464
+ server = MCPServer(name="test", disable_destructive=True)
465
+
466
+ assert server.non_destructive is True
467
+
468
+ @pytest.mark.unit
469
+ @pytest.mark.asyncio
470
+ async def test_kind_tools_have_descriptions(self, mock_all_kubernetes_apis):
471
+ """Test that all kind tools have descriptions."""
472
+ from kubectl_mcp_tool.mcp_server import MCPServer
473
+
474
+ with patch("kubectl_mcp_tool.mcp_server.MCPServer._check_dependencies", return_value=True):
475
+ server = MCPServer(name="test")
476
+
477
+ tools = await server.server.list_tools()
478
+ kind_tools = [t for t in tools if t.name.startswith("kind_")]
479
+ tools_without_description = [
480
+ t.name for t in kind_tools
481
+ if not t.description or len(t.description.strip()) == 0
482
+ ]
483
+ assert not tools_without_description, f"kind tools without descriptions: {tools_without_description}"
484
+
485
+
486
+ class TestKindNonDestructiveBlocking:
487
+ """Tests for non-destructive mode blocking of kind write operations."""
488
+
489
+ @pytest.mark.unit
490
+ @pytest.mark.asyncio
491
+ async def test_create_blocked_in_non_destructive(self, mock_all_kubernetes_apis):
492
+ """Test that kind_create_cluster_tool is blocked in non-destructive mode."""
493
+ from kubectl_mcp_tool.tools.kind import register_kind_tools
494
+
495
+ try:
496
+ from fastmcp import FastMCP
497
+ except ImportError:
498
+ from mcp.server.fastmcp import FastMCP
499
+
500
+ mcp = FastMCP(name="test")
501
+ register_kind_tools(mcp, non_destructive=True)
502
+
503
+ tool = await mcp.get_tool("kind_create_cluster_tool")
504
+ result = tool.fn(name="test")
505
+ result_dict = json.loads(result)
506
+ assert result_dict["success"] is False
507
+ assert "non-destructive" in result_dict["error"].lower()
508
+
509
+ @pytest.mark.unit
510
+ @pytest.mark.asyncio
511
+ async def test_delete_blocked_in_non_destructive(self, mock_all_kubernetes_apis):
512
+ """Test that kind_delete_cluster_tool is blocked in non-destructive mode."""
513
+ from kubectl_mcp_tool.tools.kind import register_kind_tools
514
+
515
+ try:
516
+ from fastmcp import FastMCP
517
+ except ImportError:
518
+ from mcp.server.fastmcp import FastMCP
519
+
520
+ mcp = FastMCP(name="test")
521
+ register_kind_tools(mcp, non_destructive=True)
522
+
523
+ tool = await mcp.get_tool("kind_delete_cluster_tool")
524
+ result = tool.fn(name="test")
525
+ result_dict = json.loads(result)
526
+ assert result_dict["success"] is False
527
+ assert "non-destructive" in result_dict["error"].lower()
528
+
529
+ @pytest.mark.unit
530
+ @pytest.mark.asyncio
531
+ async def test_delete_all_blocked_in_non_destructive(self, mock_all_kubernetes_apis):
532
+ """Test that kind_delete_all_clusters_tool is blocked in non-destructive mode."""
533
+ from kubectl_mcp_tool.tools.kind import register_kind_tools
534
+
535
+ try:
536
+ from fastmcp import FastMCP
537
+ except ImportError:
538
+ from mcp.server.fastmcp import FastMCP
539
+
540
+ mcp = FastMCP(name="test")
541
+ register_kind_tools(mcp, non_destructive=True)
542
+
543
+ tool = await mcp.get_tool("kind_delete_all_clusters_tool")
544
+ result = tool.fn()
545
+ result_dict = json.loads(result)
546
+ assert result_dict["success"] is False
547
+ assert "non-destructive" in result_dict["error"].lower()
548
+
549
+ @pytest.mark.unit
550
+ @pytest.mark.asyncio
551
+ async def test_load_image_blocked_in_non_destructive(self, mock_all_kubernetes_apis):
552
+ """Test that kind_load_image_tool is blocked in non-destructive mode."""
553
+ from kubectl_mcp_tool.tools.kind import register_kind_tools
554
+
555
+ try:
556
+ from fastmcp import FastMCP
557
+ except ImportError:
558
+ from mcp.server.fastmcp import FastMCP
559
+
560
+ mcp = FastMCP(name="test")
561
+ register_kind_tools(mcp, non_destructive=True)
562
+
563
+ tool = await mcp.get_tool("kind_load_image_tool")
564
+ result = tool.fn(images="myapp:latest", name="test")
565
+ result_dict = json.loads(result)
566
+ assert result_dict["success"] is False
567
+ assert "non-destructive" in result_dict["error"].lower()
568
+
569
+ @pytest.mark.unit
570
+ @pytest.mark.asyncio
571
+ async def test_read_operations_allowed_in_non_destructive(self, mock_all_kubernetes_apis):
572
+ """Test that read operations work in non-destructive mode."""
573
+ from kubectl_mcp_tool.tools.kind import register_kind_tools
574
+
575
+ try:
576
+ from fastmcp import FastMCP
577
+ except ImportError:
578
+ from mcp.server.fastmcp import FastMCP
579
+
580
+ mcp = FastMCP(name="test")
581
+ register_kind_tools(mcp, non_destructive=True)
582
+
583
+ tool = await mcp.get_tool("kind_detect_tool")
584
+ with patch("subprocess.run") as mock_run:
585
+ mock_run.side_effect = FileNotFoundError()
586
+ result = tool.fn()
587
+ result_dict = json.loads(result)
588
+ assert "installed" in result_dict
589
+
590
+
591
+ class TestKindClusterInfo:
592
+ """Tests for kind_cluster_info function."""
593
+
594
+ @pytest.mark.unit
595
+ def test_kind_cluster_info_cluster_not_found(self):
596
+ """Test kind_cluster_info when cluster not found."""
597
+ from kubectl_mcp_tool.tools.kind import kind_cluster_info
598
+
599
+ with patch("kubectl_mcp_tool.tools.kind.kind_list_clusters") as mock_list:
600
+ mock_list.return_value = {
601
+ "success": True,
602
+ "clusters": ["other-cluster"]
603
+ }
604
+ result = kind_cluster_info(name="nonexistent")
605
+ assert result["success"] is False
606
+ assert "not found" in result["error"].lower()
607
+
608
+ @pytest.mark.unit
609
+ def test_kind_cluster_info_success(self):
610
+ """Test kind_cluster_info returns cluster info."""
611
+ from kubectl_mcp_tool.tools.kind import kind_cluster_info
612
+
613
+ with patch("kubectl_mcp_tool.tools.kind.kind_list_clusters") as mock_list:
614
+ mock_list.return_value = {
615
+ "success": True,
616
+ "clusters": ["test-cluster"]
617
+ }
618
+ with patch("kubectl_mcp_tool.tools.kind.kind_get_nodes") as mock_nodes:
619
+ mock_nodes.return_value = {
620
+ "success": True,
621
+ "nodes": ["test-cluster-control-plane"],
622
+ "total": 1
623
+ }
624
+ with patch("kubectl_mcp_tool.tools.kind.kind_get_kubeconfig") as mock_kubeconfig:
625
+ mock_kubeconfig.return_value = {
626
+ "success": True,
627
+ "kubeconfig": "apiVersion: v1\n..."
628
+ }
629
+ result = kind_cluster_info(name="test-cluster")
630
+ assert result["success"] is True
631
+ assert result["cluster"] == "test-cluster"
632
+ assert result["node_count"] == 1
633
+
634
+
635
+ class TestKindConfigValidate:
636
+ """Tests for kind_config_validate function."""
637
+
638
+ @pytest.mark.unit
639
+ def test_kind_config_validate_file_not_found(self):
640
+ """Test kind_config_validate with missing file."""
641
+ from kubectl_mcp_tool.tools.kind import kind_config_validate
642
+
643
+ result = kind_config_validate("/nonexistent/config.yaml")
644
+ assert result["success"] is False
645
+ assert "not found" in result["error"].lower()
646
+
647
+ @pytest.mark.unit
648
+ def test_kind_config_validate_valid_config(self, tmp_path):
649
+ """Test kind_config_validate with valid config."""
650
+ from kubectl_mcp_tool.tools.kind import kind_config_validate
651
+
652
+ config_file = tmp_path / "kind.yaml"
653
+ config_file.write_text("""
654
+ kind: Cluster
655
+ apiVersion: kind.x-k8s.io/v1alpha4
656
+ nodes:
657
+ - role: control-plane
658
+ - role: worker
659
+ """)
660
+ result = kind_config_validate(str(config_file))
661
+ assert result["success"] is True
662
+ assert result["valid"] is True
663
+ assert result["config_summary"]["control_planes"] == 1
664
+ assert result["config_summary"]["workers"] == 1
665
+
666
+ @pytest.mark.unit
667
+ def test_kind_config_validate_invalid_kind(self, tmp_path):
668
+ """Test kind_config_validate with invalid kind field."""
669
+ from kubectl_mcp_tool.tools.kind import kind_config_validate
670
+
671
+ config_file = tmp_path / "kind.yaml"
672
+ config_file.write_text("""
673
+ kind: Invalid
674
+ apiVersion: kind.x-k8s.io/v1alpha4
675
+ """)
676
+ result = kind_config_validate(str(config_file))
677
+ assert result["success"] is False
678
+ assert len(result["errors"]) > 0
679
+
680
+
681
+ class TestKindConfigGenerate:
682
+ """Tests for kind_config_generate function."""
683
+
684
+ @pytest.mark.unit
685
+ def test_kind_config_generate_default(self):
686
+ """Test kind_config_generate with default options."""
687
+ from kubectl_mcp_tool.tools.kind import kind_config_generate
688
+
689
+ result = kind_config_generate()
690
+ assert result["success"] is True
691
+ assert "config" in result
692
+ assert "kind: Cluster" in result["config"]
693
+ assert result["summary"]["control_planes"] == 1
694
+ assert result["summary"]["workers"] == 0
695
+
696
+ @pytest.mark.unit
697
+ def test_kind_config_generate_multi_node(self):
698
+ """Test kind_config_generate with multiple nodes."""
699
+ from kubectl_mcp_tool.tools.kind import kind_config_generate
700
+
701
+ result = kind_config_generate(workers=2, control_planes=1)
702
+ assert result["success"] is True
703
+ assert result["summary"]["total_nodes"] == 3
704
+ assert result["summary"]["workers"] == 2
705
+
706
+ @pytest.mark.unit
707
+ def test_kind_config_generate_with_ingress(self):
708
+ """Test kind_config_generate with ingress enabled."""
709
+ from kubectl_mcp_tool.tools.kind import kind_config_generate
710
+
711
+ result = kind_config_generate(ingress=True)
712
+ assert result["success"] is True
713
+ assert result["summary"]["features"]["ingress"] is True
714
+ assert "extraPortMappings" in result["config"]
715
+
716
+ @pytest.mark.unit
717
+ def test_kind_config_generate_with_registry(self):
718
+ """Test kind_config_generate with registry enabled."""
719
+ from kubectl_mcp_tool.tools.kind import kind_config_generate
720
+
721
+ result = kind_config_generate(registry=True)
722
+ assert result["success"] is True
723
+ assert result["summary"]["features"]["registry"] is True
724
+ assert "containerdConfigPatches" in result["config"]
725
+
726
+
727
+ class TestKindAvailableImages:
728
+ """Tests for kind_available_images function."""
729
+
730
+ @pytest.mark.unit
731
+ def test_kind_available_images(self):
732
+ """Test kind_available_images returns image list."""
733
+ from kubectl_mcp_tool.tools.kind import kind_available_images
734
+
735
+ result = kind_available_images()
736
+ assert result["success"] is True
737
+ assert "images" in result
738
+ assert len(result["images"]) > 0
739
+ assert result["latest"] is not None
740
+ assert "kindest/node" in result["latest"]
741
+
742
+
743
+ class TestKindRegistryCreate:
744
+ """Tests for kind_registry_create function."""
745
+
746
+ @pytest.mark.unit
747
+ def test_kind_registry_create_already_exists(self):
748
+ """Test kind_registry_create when registry exists."""
749
+ from kubectl_mcp_tool.tools.kind import kind_registry_create
750
+
751
+ with patch("kubectl_mcp_tool.tools.kind._run_docker") as mock_docker:
752
+ mock_docker.return_value = {"success": True, "output": "container_id"}
753
+ result = kind_registry_create()
754
+ assert result["success"] is True
755
+ assert "already exists" in result["message"]
756
+
757
+ @pytest.mark.unit
758
+ def test_kind_registry_create_new(self):
759
+ """Test kind_registry_create creates new registry."""
760
+ from kubectl_mcp_tool.tools.kind import kind_registry_create
761
+
762
+ with patch("kubectl_mcp_tool.tools.kind._run_docker") as mock_docker:
763
+ mock_docker.side_effect = [
764
+ {"success": True, "output": ""},
765
+ {"success": True, "output": ""},
766
+ {"success": True, "output": ""},
767
+ {"success": True, "output": ""}
768
+ ]
769
+ result = kind_registry_create()
770
+ assert result["success"] is True
771
+ assert result["port"] == 5001
772
+
773
+
774
+ class TestKindRegistryStatus:
775
+ """Tests for kind_registry_status function."""
776
+
777
+ @pytest.mark.unit
778
+ def test_kind_registry_status_not_found(self):
779
+ """Test kind_registry_status when registry not found."""
780
+ from kubectl_mcp_tool.tools.kind import kind_registry_status
781
+
782
+ with patch("kubectl_mcp_tool.tools.kind._run_docker") as mock_docker:
783
+ mock_docker.return_value = {"success": False, "error": "not found"}
784
+ result = kind_registry_status()
785
+ assert result["success"] is False
786
+ assert result["installed"] is False
787
+
788
+ @pytest.mark.unit
789
+ def test_kind_registry_status_running(self):
790
+ """Test kind_registry_status when registry is running."""
791
+ from kubectl_mcp_tool.tools.kind import kind_registry_status
792
+
793
+ with patch("kubectl_mcp_tool.tools.kind._run_docker") as mock_docker:
794
+ mock_docker.return_value = {
795
+ "success": True,
796
+ "output": json.dumps({
797
+ "State": {"Running": True, "Status": "running"},
798
+ "NetworkSettings": {
799
+ "Ports": {"5000/tcp": [{"HostPort": "5001"}]},
800
+ "Networks": {"kind": {}, "bridge": {}}
801
+ }
802
+ })
803
+ }
804
+ result = kind_registry_status()
805
+ assert result["success"] is True
806
+ assert result["running"] is True
807
+ assert result["connected_to_kind"] is True
808
+
809
+
810
+ class TestKindNodeExec:
811
+ """Tests for kind_node_exec function."""
812
+
813
+ @pytest.mark.unit
814
+ def test_kind_node_exec_missing_node(self):
815
+ """Test kind_node_exec with missing node."""
816
+ from kubectl_mcp_tool.tools.kind import kind_node_exec
817
+
818
+ result = kind_node_exec(node="", command="ls")
819
+ assert result["success"] is False
820
+ assert "required" in result["error"].lower()
821
+
822
+ @pytest.mark.unit
823
+ def test_kind_node_exec_missing_command(self):
824
+ """Test kind_node_exec with missing command."""
825
+ from kubectl_mcp_tool.tools.kind import kind_node_exec
826
+
827
+ result = kind_node_exec(node="kind-control-plane", command="")
828
+ assert result["success"] is False
829
+ assert "required" in result["error"].lower()
830
+
831
+ @pytest.mark.unit
832
+ def test_kind_node_exec_success(self):
833
+ """Test kind_node_exec succeeds."""
834
+ from kubectl_mcp_tool.tools.kind import kind_node_exec
835
+
836
+ with patch("kubectl_mcp_tool.tools.kind.kind_get_nodes") as mock_nodes:
837
+ mock_nodes.return_value = {
838
+ "success": True,
839
+ "nodes": ["kind-control-plane"]
840
+ }
841
+ with patch("kubectl_mcp_tool.tools.kind._run_docker") as mock_docker:
842
+ mock_docker.return_value = {"success": True, "output": "output"}
843
+ result = kind_node_exec(node="kind-control-plane", command="ls")
844
+ assert result["success"] is True
845
+ assert result["output"] == "output"
846
+
847
+
848
+ class TestKindNodeLogs:
849
+ """Tests for kind_node_logs function."""
850
+
851
+ @pytest.mark.unit
852
+ def test_kind_node_logs_missing_node(self):
853
+ """Test kind_node_logs with missing node."""
854
+ from kubectl_mcp_tool.tools.kind import kind_node_logs
855
+
856
+ result = kind_node_logs(node="")
857
+ assert result["success"] is False
858
+ assert "required" in result["error"].lower()
859
+
860
+ @pytest.mark.unit
861
+ def test_kind_node_logs_success(self):
862
+ """Test kind_node_logs succeeds."""
863
+ from kubectl_mcp_tool.tools.kind import kind_node_logs
864
+
865
+ with patch("kubectl_mcp_tool.tools.kind._run_docker") as mock_docker:
866
+ mock_docker.return_value = {"success": True, "output": "log output"}
867
+ result = kind_node_logs(node="kind-control-plane")
868
+ assert result["success"] is True
869
+ assert "logs" in result
870
+
871
+
872
+ class TestKindNodeInspect:
873
+ """Tests for kind_node_inspect function."""
874
+
875
+ @pytest.mark.unit
876
+ def test_kind_node_inspect_missing_node(self):
877
+ """Test kind_node_inspect with missing node."""
878
+ from kubectl_mcp_tool.tools.kind import kind_node_inspect
879
+
880
+ result = kind_node_inspect(node="")
881
+ assert result["success"] is False
882
+ assert "required" in result["error"].lower()
883
+
884
+ @pytest.mark.unit
885
+ def test_kind_node_inspect_success(self):
886
+ """Test kind_node_inspect succeeds."""
887
+ from kubectl_mcp_tool.tools.kind import kind_node_inspect
888
+
889
+ with patch("kubectl_mcp_tool.tools.kind._run_docker") as mock_docker:
890
+ mock_docker.return_value = {
891
+ "success": True,
892
+ "output": json.dumps({
893
+ "State": {"Running": True, "Status": "running", "StartedAt": "2024-01-01", "Pid": 1234},
894
+ "Config": {"Image": "kindest/node:v1.29.0", "Labels": {}},
895
+ "NetworkSettings": {"IPAddress": "172.18.0.2", "Networks": {"kind": {}}},
896
+ "HostConfig": {"PortBindings": {}},
897
+ "Mounts": []
898
+ })
899
+ }
900
+ result = kind_node_inspect(node="kind-control-plane")
901
+ assert result["success"] is True
902
+ assert result["state"]["running"] is True
903
+
904
+
905
+ class TestKindNodeRestart:
906
+ """Tests for kind_node_restart function."""
907
+
908
+ @pytest.mark.unit
909
+ def test_kind_node_restart_missing_node(self):
910
+ """Test kind_node_restart with missing node."""
911
+ from kubectl_mcp_tool.tools.kind import kind_node_restart
912
+
913
+ result = kind_node_restart(node="")
914
+ assert result["success"] is False
915
+ assert "required" in result["error"].lower()
916
+
917
+ @pytest.mark.unit
918
+ def test_kind_node_restart_success(self):
919
+ """Test kind_node_restart succeeds."""
920
+ from kubectl_mcp_tool.tools.kind import kind_node_restart
921
+
922
+ with patch("kubectl_mcp_tool.tools.kind._run_docker") as mock_docker:
923
+ mock_docker.return_value = {"success": True, "output": ""}
924
+ result = kind_node_restart(node="kind-control-plane")
925
+ assert result["success"] is True
926
+ assert "restarted" in result["message"].lower()
927
+
928
+
929
+ class TestKindNetworkInspect:
930
+ """Tests for kind_network_inspect function."""
931
+
932
+ @pytest.mark.unit
933
+ def test_kind_network_inspect_not_found(self):
934
+ """Test kind_network_inspect when network not found."""
935
+ from kubectl_mcp_tool.tools.kind import kind_network_inspect
936
+
937
+ with patch("kubectl_mcp_tool.tools.kind._run_docker") as mock_docker:
938
+ mock_docker.return_value = {"success": False, "error": "not found"}
939
+ result = kind_network_inspect()
940
+ assert result["success"] is False
941
+
942
+ @pytest.mark.unit
943
+ def test_kind_network_inspect_success(self):
944
+ """Test kind_network_inspect succeeds."""
945
+ from kubectl_mcp_tool.tools.kind import kind_network_inspect
946
+
947
+ with patch("kubectl_mcp_tool.tools.kind._run_docker") as mock_docker:
948
+ mock_docker.return_value = {
949
+ "success": True,
950
+ "output": json.dumps([{
951
+ "Name": "kind",
952
+ "Driver": "bridge",
953
+ "Scope": "local",
954
+ "IPAM": {"Config": [{"Subnet": "172.18.0.0/16", "Gateway": "172.18.0.1"}]},
955
+ "Containers": {
956
+ "abc123": {"Name": "kind-control-plane", "IPv4Address": "172.18.0.2/16", "MacAddress": "aa:bb:cc"}
957
+ }
958
+ }])
959
+ }
960
+ result = kind_network_inspect()
961
+ assert result["success"] is True
962
+ assert result["subnet"] == "172.18.0.0/16"
963
+
964
+
965
+ class TestKindPortMappings:
966
+ """Tests for kind_port_mappings function."""
967
+
968
+ @pytest.mark.unit
969
+ def test_kind_port_mappings_success(self):
970
+ """Test kind_port_mappings returns mappings."""
971
+ from kubectl_mcp_tool.tools.kind import kind_port_mappings
972
+
973
+ with patch("kubectl_mcp_tool.tools.kind.kind_get_nodes") as mock_nodes:
974
+ mock_nodes.return_value = {
975
+ "success": True,
976
+ "nodes": ["kind-control-plane"]
977
+ }
978
+ with patch("kubectl_mcp_tool.tools.kind._run_docker") as mock_docker:
979
+ mock_docker.return_value = {
980
+ "success": True,
981
+ "output": json.dumps({
982
+ "80/tcp": [{"HostIp": "0.0.0.0", "HostPort": "80"}]
983
+ })
984
+ }
985
+ result = kind_port_mappings()
986
+ assert result["success"] is True
987
+ assert result["has_mappings"] is True
988
+
989
+
990
+ class TestKindIngressSetup:
991
+ """Tests for kind_ingress_setup function."""
992
+
993
+ @pytest.mark.unit
994
+ def test_kind_ingress_setup_invalid_type(self):
995
+ """Test kind_ingress_setup with invalid type."""
996
+ from kubectl_mcp_tool.tools.kind import kind_ingress_setup
997
+
998
+ result = kind_ingress_setup(ingress_type="invalid")
999
+ assert result["success"] is False
1000
+ assert "unsupported" in result["error"].lower()
1001
+
1002
+ @pytest.mark.unit
1003
+ def test_kind_ingress_setup_cluster_not_found(self):
1004
+ """Test kind_ingress_setup when cluster not found."""
1005
+ from kubectl_mcp_tool.tools.kind import kind_ingress_setup
1006
+
1007
+ with patch("kubectl_mcp_tool.tools.kind.kind_list_clusters") as mock_list:
1008
+ mock_list.return_value = {"success": True, "clusters": []}
1009
+ result = kind_ingress_setup(cluster="nonexistent")
1010
+ assert result["success"] is False
1011
+
1012
+
1013
+ class TestKindClusterStatus:
1014
+ """Tests for kind_cluster_status function."""
1015
+
1016
+ @pytest.mark.unit
1017
+ def test_kind_cluster_status_not_found(self):
1018
+ """Test kind_cluster_status when cluster not found."""
1019
+ from kubectl_mcp_tool.tools.kind import kind_cluster_status
1020
+
1021
+ with patch("kubectl_mcp_tool.tools.kind.kind_list_clusters") as mock_list:
1022
+ mock_list.return_value = {"success": True, "clusters": []}
1023
+ result = kind_cluster_status(name="nonexistent")
1024
+ assert result["success"] is False
1025
+ assert "not found" in result["error"].lower()
1026
+
1027
+
1028
+ class TestKindImagesList:
1029
+ """Tests for kind_images_list function."""
1030
+
1031
+ @pytest.mark.unit
1032
+ def test_kind_images_list_no_nodes(self):
1033
+ """Test kind_images_list when no nodes."""
1034
+ from kubectl_mcp_tool.tools.kind import kind_images_list
1035
+
1036
+ with patch("kubectl_mcp_tool.tools.kind.kind_get_nodes") as mock_nodes:
1037
+ mock_nodes.return_value = {"success": True, "nodes": []}
1038
+ result = kind_images_list()
1039
+ assert result["success"] is False
1040
+
1041
+
1042
+ class TestKindProviderInfo:
1043
+ """Tests for kind_provider_info function."""
1044
+
1045
+ @pytest.mark.unit
1046
+ def test_kind_provider_info_success(self):
1047
+ """Test kind_provider_info returns provider details."""
1048
+ from kubectl_mcp_tool.tools.kind import kind_provider_info
1049
+
1050
+ with patch("kubectl_mcp_tool.tools.kind._run_docker") as mock_docker:
1051
+ mock_docker.return_value = {
1052
+ "success": True,
1053
+ "output": json.dumps({
1054
+ "Client": {"Version": "24.0.0", "ApiVersion": "1.43", "Os": "darwin", "Arch": "arm64"},
1055
+ "Server": {"Version": "24.0.0"}
1056
+ })
1057
+ }
1058
+ result = kind_provider_info()
1059
+ assert result["success"] is True
1060
+ assert result["provider"] == "docker"
1061
+ assert result["client_version"] == "24.0.0"
1062
+
1063
+
1064
+ class TestRunDocker:
1065
+ """Tests for _run_docker helper function."""
1066
+
1067
+ @pytest.mark.unit
1068
+ def test_run_docker_success(self):
1069
+ """Test _run_docker returns success."""
1070
+ from kubectl_mcp_tool.tools.kind import _run_docker
1071
+
1072
+ with patch("subprocess.run") as mock_run:
1073
+ mock_run.return_value = MagicMock(
1074
+ returncode=0,
1075
+ stdout="output",
1076
+ stderr=""
1077
+ )
1078
+ result = _run_docker(["ps"])
1079
+ assert result["success"] is True
1080
+ assert result["output"] == "output"
1081
+
1082
+ @pytest.mark.unit
1083
+ def test_run_docker_not_available(self):
1084
+ """Test _run_docker when Docker not available."""
1085
+ from kubectl_mcp_tool.tools.kind import _run_docker
1086
+
1087
+ with patch("subprocess.run") as mock_run:
1088
+ mock_run.side_effect = FileNotFoundError()
1089
+ result = _run_docker(["ps"])
1090
+ assert result["success"] is False
1091
+ assert "not available" in result["error"].lower()
1092
+
1093
+ @pytest.mark.unit
1094
+ def test_run_docker_timeout(self):
1095
+ """Test _run_docker handles timeout."""
1096
+ from kubectl_mcp_tool.tools.kind import _run_docker
1097
+
1098
+ with patch("subprocess.run") as mock_run:
1099
+ mock_run.side_effect = subprocess.TimeoutExpired(cmd="docker", timeout=60)
1100
+ result = _run_docker(["ps"])
1101
+ assert result["success"] is False
1102
+ assert "timed out" in result["error"]
1103
+
1104
+
1105
+ class TestNewKindNonDestructiveBlocking:
1106
+ """Tests for non-destructive mode blocking of new kind write operations."""
1107
+
1108
+ @pytest.mark.unit
1109
+ @pytest.mark.asyncio
1110
+ async def test_registry_create_blocked_in_non_destructive(self, mock_all_kubernetes_apis):
1111
+ """Test that kind_registry_create_tool is blocked in non-destructive mode."""
1112
+ from kubectl_mcp_tool.tools.kind import register_kind_tools
1113
+
1114
+ try:
1115
+ from fastmcp import FastMCP
1116
+ except ImportError:
1117
+ from mcp.server.fastmcp import FastMCP
1118
+
1119
+ mcp = FastMCP(name="test")
1120
+ register_kind_tools(mcp, non_destructive=True)
1121
+
1122
+ tool = await mcp.get_tool("kind_registry_create_tool")
1123
+ result = tool.fn()
1124
+ result_dict = json.loads(result)
1125
+ assert result_dict["success"] is False
1126
+ assert "non-destructive" in result_dict["error"].lower()
1127
+
1128
+ @pytest.mark.unit
1129
+ @pytest.mark.asyncio
1130
+ async def test_node_exec_blocked_in_non_destructive(self, mock_all_kubernetes_apis):
1131
+ """Test that kind_node_exec_tool is blocked in non-destructive mode."""
1132
+ from kubectl_mcp_tool.tools.kind import register_kind_tools
1133
+
1134
+ try:
1135
+ from fastmcp import FastMCP
1136
+ except ImportError:
1137
+ from mcp.server.fastmcp import FastMCP
1138
+
1139
+ mcp = FastMCP(name="test")
1140
+ register_kind_tools(mcp, non_destructive=True)
1141
+
1142
+ tool = await mcp.get_tool("kind_node_exec_tool")
1143
+ result = tool.fn(node="test", command="ls")
1144
+ result_dict = json.loads(result)
1145
+ assert result_dict["success"] is False
1146
+ assert "non-destructive" in result_dict["error"].lower()
1147
+
1148
+ @pytest.mark.unit
1149
+ @pytest.mark.asyncio
1150
+ async def test_node_restart_blocked_in_non_destructive(self, mock_all_kubernetes_apis):
1151
+ """Test that kind_node_restart_tool is blocked in non-destructive mode."""
1152
+ from kubectl_mcp_tool.tools.kind import register_kind_tools
1153
+
1154
+ try:
1155
+ from fastmcp import FastMCP
1156
+ except ImportError:
1157
+ from mcp.server.fastmcp import FastMCP
1158
+
1159
+ mcp = FastMCP(name="test")
1160
+ register_kind_tools(mcp, non_destructive=True)
1161
+
1162
+ tool = await mcp.get_tool("kind_node_restart_tool")
1163
+ result = tool.fn(node="test")
1164
+ result_dict = json.loads(result)
1165
+ assert result_dict["success"] is False
1166
+ assert "non-destructive" in result_dict["error"].lower()
1167
+
1168
+ @pytest.mark.unit
1169
+ @pytest.mark.asyncio
1170
+ async def test_ingress_setup_blocked_in_non_destructive(self, mock_all_kubernetes_apis):
1171
+ """Test that kind_ingress_setup_tool is blocked in non-destructive mode."""
1172
+ from kubectl_mcp_tool.tools.kind import register_kind_tools
1173
+
1174
+ try:
1175
+ from fastmcp import FastMCP
1176
+ except ImportError:
1177
+ from mcp.server.fastmcp import FastMCP
1178
+
1179
+ mcp = FastMCP(name="test")
1180
+ register_kind_tools(mcp, non_destructive=True)
1181
+
1182
+ tool = await mcp.get_tool("kind_ingress_setup_tool")
1183
+ result = tool.fn()
1184
+ result_dict = json.loads(result)
1185
+ assert result_dict["success"] is False
1186
+ assert "non-destructive" in result_dict["error"].lower()
1187
+
1188
+ @pytest.mark.unit
1189
+ @pytest.mark.asyncio
1190
+ async def test_read_operations_allowed_in_non_destructive(self, mock_all_kubernetes_apis):
1191
+ """Test that new read operations work in non-destructive mode."""
1192
+ from kubectl_mcp_tool.tools.kind import register_kind_tools
1193
+
1194
+ try:
1195
+ from fastmcp import FastMCP
1196
+ except ImportError:
1197
+ from mcp.server.fastmcp import FastMCP
1198
+
1199
+ mcp = FastMCP(name="test")
1200
+ register_kind_tools(mcp, non_destructive=True)
1201
+
1202
+ tool = await mcp.get_tool("kind_available_images_tool")
1203
+ result = tool.fn()
1204
+ result_dict = json.loads(result)
1205
+ assert result_dict["success"] is True
1206
+ assert "images" in result_dict