Skip to content

Commit a648547

Browse files
authored
test: raise pkg coverage above 80% and bump tool versions (#65)
Add unit tests across pkg providers so every package in ./pkg exceeds 80% statement coverage: - utils: 6.5% -> 100% (shell tool, kubeconfig, RegisterTools) - cilium: 46.7% -> 85.5% (cilium-dbg and CLI handlers) - argo: 49.7% -> 88.3% (gateway plugin, list/check logs, RegisterTools) - istio: 61.6% -> 92.7% (waypoint apply/delete/status, ztunnel) - prometheus: 72.4% -> 81.6% (RegisterTools, validation/error paths) - k8s: 73.9% -> 82.2% (RegisterTools, NewK8sToolWithConfig) Extract the shell tool closure into a named handleShellTool for testability. Bump tool versions to latest releases (make check-releases): - cilium 0.19.2 -> 0.19.4 - istio 1.29.1 -> 1.30.1 - helm 4.1.3 -> 4.2.2 - kubectl 1.35.3 -> 1.36.2 Signed-off-by: Dmytro Rashko <dmitriy.rashko@amdocs.com>
1 parent d819500 commit a648547

9 files changed

Lines changed: 716 additions & 19 deletions

File tree

Makefile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,11 @@ DOCKER_BUILDER ?= docker buildx
136136
DOCKER_BUILD_ARGS ?= --pull --load --platform linux/$(LOCALARCH) --builder $(BUILDX_BUILDER_NAME)
137137

138138
# tools image build args
139-
TOOLS_ISTIO_VERSION ?= 1.29.1
139+
TOOLS_ISTIO_VERSION ?= 1.30.1
140140
TOOLS_ARGO_ROLLOUTS_VERSION ?= 1.9.0
141-
TOOLS_KUBECTL_VERSION ?= 1.35.3
142-
TOOLS_HELM_VERSION ?= 4.1.3
143-
TOOLS_CILIUM_VERSION ?= 0.19.2
141+
TOOLS_KUBECTL_VERSION ?= 1.36.2
142+
TOOLS_HELM_VERSION ?= 4.2.2
143+
TOOLS_CILIUM_VERSION ?= 0.19.4
144144

145145
# build args
146146
TOOLS_IMAGE_BUILD_ARGS = --build-arg VERSION=$(VERSION)

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/kagent-dev/tools
22

3-
go 1.26.1
3+
go 1.26.4
44

55
require (
66
github.com/joho/godotenv v1.5.1

pkg/argo/argo_test.go

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,166 @@ import (
77

88
"github.com/kagent-dev/tools/internal/cmd"
99
"github.com/mark3labs/mcp-go/mcp"
10+
"github.com/mark3labs/mcp-go/server"
1011
"github.com/stretchr/testify/assert"
1112
"github.com/stretchr/testify/require"
1213
)
1314

15+
func TestRegisterTools(t *testing.T) {
16+
t.Run("read-write", func(t *testing.T) {
17+
s := server.NewMCPServer("test", "v0.0.1")
18+
RegisterTools(s, false)
19+
})
20+
t.Run("read-only", func(t *testing.T) {
21+
s := server.NewMCPServer("test", "v0.0.1")
22+
RegisterTools(s, true)
23+
})
24+
}
25+
26+
func TestHandleListRollouts(t *testing.T) {
27+
t.Run("default namespace and type", func(t *testing.T) {
28+
mock := cmd.NewMockShellExecutor()
29+
mock.AddCommandString("kubectl", []string{"argo", "rollouts", "list", "rollouts", "-n", "argo-rollouts"}, "NAME STATUS\nmyapp Healthy", nil)
30+
ctx := cmd.WithShellExecutor(context.Background(), mock)
31+
32+
result, err := handleListRollouts(ctx, mcp.CallToolRequest{})
33+
assert.NoError(t, err)
34+
assert.False(t, result.IsError)
35+
assert.Contains(t, getResultText(result), "myapp")
36+
})
37+
38+
t.Run("experiments type custom namespace", func(t *testing.T) {
39+
mock := cmd.NewMockShellExecutor()
40+
mock.AddCommandString("kubectl", []string{"argo", "rollouts", "list", "experiments", "-n", "prod"}, "NAME", nil)
41+
ctx := cmd.WithShellExecutor(context.Background(), mock)
42+
43+
req := mcp.CallToolRequest{}
44+
req.Params.Arguments = map[string]interface{}{"type": "experiments", "namespace": "prod"}
45+
result, err := handleListRollouts(ctx, req)
46+
assert.NoError(t, err)
47+
assert.False(t, result.IsError)
48+
})
49+
50+
t.Run("command failure", func(t *testing.T) {
51+
mock := cmd.NewMockShellExecutor()
52+
mock.AddCommandString("kubectl", []string{"argo", "rollouts", "list", "rollouts", "-n", "argo-rollouts"}, "", assert.AnError)
53+
ctx := cmd.WithShellExecutor(context.Background(), mock)
54+
55+
result, err := handleListRollouts(ctx, mcp.CallToolRequest{})
56+
assert.NoError(t, err)
57+
assert.True(t, result.IsError)
58+
assert.Contains(t, getResultText(result), "Error listing rollouts")
59+
})
60+
}
61+
62+
func TestHandleCheckPluginLogs(t *testing.T) {
63+
t.Run("plugin install found in logs", func(t *testing.T) {
64+
mock := cmd.NewMockShellExecutor()
65+
logs := `Downloading plugin argoproj-labs/gatewayAPI from: https://github.com/x/releases/download/v0.5.0/gatewayapi-plugin-linux-amd64"
66+
Download complete, it took 1.5s`
67+
mock.AddCommandString("kubectl", []string{"logs", "-n", "argo-rollouts", "-l", "app.kubernetes.io/name=argo-rollouts", "--tail", "100"}, logs, nil)
68+
ctx := cmd.WithShellExecutor(context.Background(), mock)
69+
70+
result, err := handleCheckPluginLogs(ctx, mcp.CallToolRequest{})
71+
assert.NoError(t, err)
72+
assert.Contains(t, getResultText(result), "0.5.0")
73+
assert.Contains(t, getResultText(result), `"installed": true`)
74+
})
75+
76+
t.Run("plugin install not found", func(t *testing.T) {
77+
mock := cmd.NewMockShellExecutor()
78+
mock.AddCommandString("kubectl", []string{"logs", "-n", "argo-rollouts", "-l", "app.kubernetes.io/name=argo-rollouts", "--tail", "100"}, "no plugin here", nil)
79+
ctx := cmd.WithShellExecutor(context.Background(), mock)
80+
81+
result, err := handleCheckPluginLogs(ctx, mcp.CallToolRequest{})
82+
assert.NoError(t, err)
83+
assert.Contains(t, getResultText(result), "Plugin installation not found")
84+
})
85+
86+
t.Run("command failure", func(t *testing.T) {
87+
mock := cmd.NewMockShellExecutor()
88+
mock.AddCommandString("kubectl", []string{"logs", "-n", "argo-rollouts", "-l", "app.kubernetes.io/name=argo-rollouts", "--tail", "100"}, "", assert.AnError)
89+
ctx := cmd.WithShellExecutor(context.Background(), mock)
90+
91+
result, err := handleCheckPluginLogs(ctx, mcp.CallToolRequest{})
92+
assert.NoError(t, err)
93+
assert.Contains(t, getResultText(result), `"installed": false`)
94+
})
95+
}
96+
97+
func TestConfigureGatewayPlugin(t *testing.T) {
98+
t.Run("applies configmap successfully", func(t *testing.T) {
99+
mock := cmd.NewMockShellExecutor()
100+
mock.AddPartialMatcherString("kubectl", []string{"apply", "-f"}, "configmap/argo-rollouts-config created", nil)
101+
ctx := cmd.WithShellExecutor(context.Background(), mock)
102+
103+
status := configureGatewayPlugin(ctx, "0.5.0", "argo-rollouts")
104+
assert.True(t, status.Installed)
105+
assert.Equal(t, "0.5.0", status.Version)
106+
assert.NotEmpty(t, status.Architecture)
107+
})
108+
109+
t.Run("apply failure", func(t *testing.T) {
110+
mock := cmd.NewMockShellExecutor()
111+
mock.AddPartialMatcherString("kubectl", []string{"apply", "-f"}, "", assert.AnError)
112+
ctx := cmd.WithShellExecutor(context.Background(), mock)
113+
114+
status := configureGatewayPlugin(ctx, "0.5.0", "argo-rollouts")
115+
assert.False(t, status.Installed)
116+
assert.Contains(t, status.ErrorMessage, "Error applying Gateway API plugin config")
117+
})
118+
}
119+
120+
func TestHandleVerifyGatewayPluginAlreadyConfigured(t *testing.T) {
121+
mock := cmd.NewMockShellExecutor()
122+
mock.AddCommandString("kubectl", []string{"get", "configmap", "argo-rollouts-config", "-n", "argo-rollouts", "-o", "yaml"}, "data:\n trafficRouterPlugins: argoproj-labs/gatewayAPI", nil)
123+
ctx := cmd.WithShellExecutor(context.Background(), mock)
124+
125+
result, err := handleVerifyGatewayPlugin(ctx, mcp.CallToolRequest{})
126+
assert.NoError(t, err)
127+
assert.Contains(t, getResultText(result), "already configured")
128+
}
129+
130+
func TestHandleVerifyArgoRolloutsControllerInstallStatuses(t *testing.T) {
131+
baseCmd := []string{"get", "pods", "-n", "argo-rollouts", "-l", "app.kubernetes.io/component=rollouts-controller", "-o", "jsonpath={.items[*].status.phase}"}
132+
133+
t.Run("all running", func(t *testing.T) {
134+
mock := cmd.NewMockShellExecutor()
135+
mock.AddCommandString("kubectl", baseCmd, "Running Running", nil)
136+
ctx := cmd.WithShellExecutor(context.Background(), mock)
137+
result, err := handleVerifyArgoRolloutsControllerInstall(ctx, mcp.CallToolRequest{})
138+
assert.NoError(t, err)
139+
assert.Contains(t, getResultText(result), "All pods are running")
140+
})
141+
142+
t.Run("not all running", func(t *testing.T) {
143+
mock := cmd.NewMockShellExecutor()
144+
mock.AddCommandString("kubectl", baseCmd, "Running Pending", nil)
145+
ctx := cmd.WithShellExecutor(context.Background(), mock)
146+
result, err := handleVerifyArgoRolloutsControllerInstall(ctx, mcp.CallToolRequest{})
147+
assert.NoError(t, err)
148+
assert.Contains(t, getResultText(result), "Not all pods are running")
149+
})
150+
151+
t.Run("no pods", func(t *testing.T) {
152+
mock := cmd.NewMockShellExecutor()
153+
mock.AddCommandString("kubectl", baseCmd, "", nil)
154+
ctx := cmd.WithShellExecutor(context.Background(), mock)
155+
result, err := handleVerifyArgoRolloutsControllerInstall(ctx, mcp.CallToolRequest{})
156+
assert.NoError(t, err)
157+
assert.Contains(t, getResultText(result), "No pods found")
158+
})
159+
160+
t.Run("command error", func(t *testing.T) {
161+
mock := cmd.NewMockShellExecutor()
162+
mock.AddCommandString("kubectl", baseCmd, "", assert.AnError)
163+
ctx := cmd.WithShellExecutor(context.Background(), mock)
164+
result, err := handleVerifyArgoRolloutsControllerInstall(ctx, mcp.CallToolRequest{})
165+
assert.NoError(t, err)
166+
assert.True(t, result.IsError)
167+
})
168+
}
169+
14170
// Helper function to extract text content from MCP result
15171
func getResultText(result *mcp.CallToolResult) string {
16172
if result == nil || len(result.Content) == 0 {

pkg/cilium/cilium_test.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,3 +619,147 @@ func getResultText(r *mcp.CallToolResult) string {
619619
}
620620
return ""
621621
}
622+
623+
type ciliumHandler func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error)
624+
625+
// TestCiliumDbgHandlers exercises the success path of every cilium-dbg based handler.
626+
func TestCiliumDbgHandlers(t *testing.T) {
627+
cases := []struct {
628+
name string
629+
handler ciliumHandler
630+
args map[string]any
631+
dbgArgs []string
632+
expect string
633+
}{
634+
{"manage_endpoint_labels", handleManageEndpointLabels, map[string]any{"endpoint_id": "34", "labels": "key=val"}, []string{"endpoint", "labels", "34", "--add", "key=val"}, "ok"},
635+
{"manage_endpoint_configuration", handleManageEndpointConfiguration, map[string]any{"endpoint_id": "34", "config": "Debug=true"}, []string{"endpoint", "config", "34", "Debug=true"}, "ok"},
636+
{"disconnect_endpoint", handleDisconnectEndpoint, map[string]any{"endpoint_id": "34"}, []string{"endpoint", "disconnect", "34"}, "ok"},
637+
{"get_identity_details", handleGetIdentityDetails, map[string]any{"identity_id": "123"}, []string{"identity", "get", "123"}, "ok"},
638+
{"flush_ipsec_state", handleFlushIPsecState, map[string]any{}, []string{"encrypt", "flush", "-f"}, "ok"},
639+
{"list_envoy_config", handleListEnvoyConfig, map[string]any{"resource_name": "clusters"}, []string{"envoy", "admin", "clusters"}, "ok"},
640+
{"show_ipcache_cidr", handleShowIPCacheInformation, map[string]any{"cidr": "10.0.0.0/24"}, []string{"ip", "get", "10.0.0.0/24"}, "ok"},
641+
{"show_ipcache_labels", handleShowIPCacheInformation, map[string]any{"labels": "app=foo"}, []string{"ip", "get", "--labels", "app=foo"}, "ok"},
642+
{"delete_kvstore_key", handleDeleteKeyFromKVStore, map[string]any{"key": "foo"}, []string{"kvstore", "delete", "foo"}, "ok"},
643+
{"get_kvstore_key", handleGetKVStoreKey, map[string]any{"key": "foo"}, []string{"kvstore", "get", "foo"}, "ok"},
644+
{"set_kvstore_key", handleSetKVStoreKey, map[string]any{"key": "foo", "value": "bar"}, []string{"kvstore", "set", "foo=bar"}, "ok"},
645+
{"show_load_information", handleShowLoadInformation, map[string]any{}, []string{"loadinfo"}, "ok"},
646+
{"display_policy_node_info", handleDisplayPolicyNodeInformation, map[string]any{}, []string{"policy", "get"}, "ok"},
647+
{"display_policy_node_info_labels", handleDisplayPolicyNodeInformation, map[string]any{"labels": "k=v"}, []string{"policy", "get", "k=v"}, "ok"},
648+
{"delete_policy_rules_all", handleDeletePolicyRules, map[string]any{"all": "true"}, []string{"policy", "delete", "--all"}, "ok"},
649+
{"delete_policy_rules_labels", handleDeletePolicyRules, map[string]any{"labels": "k=v"}, []string{"policy", "delete", "k=v"}, "ok"},
650+
{"update_xdp_cidr", handleUpdateXDPCIDRFilters, map[string]any{"cidr_prefixes": "10.0.0.0/8"}, []string{"prefilter", "update", "--cidr", "10.0.0.0/8"}, "ok"},
651+
{"update_xdp_cidr_rev", handleUpdateXDPCIDRFilters, map[string]any{"cidr_prefixes": "10.0.0.0/8", "revision": "2"}, []string{"prefilter", "update", "--cidr", "10.0.0.0/8", "--revision", "2"}, "ok"},
652+
{"delete_xdp_cidr", handleDeleteXDPCIDRFilters, map[string]any{"cidr_prefixes": "10.0.0.0/8"}, []string{"prefilter", "delete", "--cidr", "10.0.0.0/8"}, "ok"},
653+
{"delete_xdp_cidr_rev", handleDeleteXDPCIDRFilters, map[string]any{"cidr_prefixes": "10.0.0.0/8", "revision": "2"}, []string{"prefilter", "delete", "--cidr", "10.0.0.0/8", "--revision", "2"}, "ok"},
654+
{"validate_cnp", handleValidateCiliumNetworkPolicies, map[string]any{"enable_k8s": "true", "enable_k8s_api_discovery": "true"}, []string{"preflight", "validate-cnp", "--enable-k8s", "--enable-k8s-api-discovery"}, "ok"},
655+
{"list_pcap_recorders", handleListPCAPRecorders, map[string]any{}, []string{"recorder", "list"}, "ok"},
656+
{"get_pcap_recorder", handleGetPCAPRecorder, map[string]any{"recorder_id": "1"}, []string{"recorder", "get", "1"}, "ok"},
657+
{"delete_pcap_recorder", handleDeletePCAPRecorder, map[string]any{"recorder_id": "1"}, []string{"recorder", "delete", "1"}, "ok"},
658+
{"update_pcap_recorder", handleUpdatePCAPRecorder, map[string]any{"recorder_id": "1", "filters": "f"}, []string{"recorder", "update", "1", "--filters", "f", "--caplen", "0", "--id", "0"}, "ok"},
659+
{"get_service_information", handleGetServiceInformation, map[string]any{"service_id": "5"}, []string{"service", "get", "5"}, "ok"},
660+
{"delete_service_all", handleDeleteService, map[string]any{"all": "true"}, []string{"service", "delete", "--all"}, "ok"},
661+
{"delete_service_id", handleDeleteService, map[string]any{"service_id": "5"}, []string{"service", "delete", "5"}, "ok"},
662+
{"update_service", handleUpdateService, map[string]any{"backends": "b", "frontend": "f", "id": "1"}, []string{"service", "update", "1", "--backends", "b", "--frontend", "f", "--protocol", "TCP", "--states", "active"}, "ok"},
663+
}
664+
665+
for _, tc := range cases {
666+
t.Run(tc.name, func(t *testing.T) {
667+
mock := cmd.NewMockShellExecutor()
668+
mockCiliumDbgCommand(mock, tc.dbgArgs, tc.expect, nil)
669+
ctx := cmd.WithShellExecutor(context.Background(), mock)
670+
671+
tc.args["node_name"] = "test-node"
672+
result, err := tc.handler(ctx, newRequestWithArgs(tc.args))
673+
require.NoError(t, err)
674+
assert.False(t, result.IsError, "handler returned error result: %s", getResultText(result))
675+
assert.Contains(t, getResultText(result), tc.expect)
676+
})
677+
}
678+
}
679+
680+
// TestCiliumDbgHandlersMissingParams covers required-parameter validation branches.
681+
func TestCiliumDbgHandlersMissingParams(t *testing.T) {
682+
cases := []struct {
683+
name string
684+
handler ciliumHandler
685+
args map[string]any
686+
}{
687+
{"manage_endpoint_labels", handleManageEndpointLabels, map[string]any{}},
688+
{"manage_endpoint_configuration_no_id", handleManageEndpointConfiguration, map[string]any{}},
689+
{"manage_endpoint_configuration_no_config", handleManageEndpointConfiguration, map[string]any{"endpoint_id": "34"}},
690+
{"disconnect_endpoint", handleDisconnectEndpoint, map[string]any{}},
691+
{"get_identity_details", handleGetIdentityDetails, map[string]any{}},
692+
{"list_envoy_config", handleListEnvoyConfig, map[string]any{}},
693+
{"show_ipcache_none", handleShowIPCacheInformation, map[string]any{}},
694+
{"delete_kvstore_key", handleDeleteKeyFromKVStore, map[string]any{}},
695+
{"get_kvstore_key", handleGetKVStoreKey, map[string]any{}},
696+
{"set_kvstore_key", handleSetKVStoreKey, map[string]any{"key": "foo"}},
697+
{"delete_policy_rules_none", handleDeletePolicyRules, map[string]any{}},
698+
{"update_xdp_cidr", handleUpdateXDPCIDRFilters, map[string]any{}},
699+
{"delete_xdp_cidr", handleDeleteXDPCIDRFilters, map[string]any{}},
700+
{"get_pcap_recorder", handleGetPCAPRecorder, map[string]any{}},
701+
{"delete_pcap_recorder", handleDeletePCAPRecorder, map[string]any{}},
702+
{"update_pcap_recorder", handleUpdatePCAPRecorder, map[string]any{"recorder_id": "1"}},
703+
{"get_service_information", handleGetServiceInformation, map[string]any{}},
704+
{"delete_service_none", handleDeleteService, map[string]any{}},
705+
{"update_service", handleUpdateService, map[string]any{"backends": "b"}},
706+
}
707+
708+
for _, tc := range cases {
709+
t.Run(tc.name, func(t *testing.T) {
710+
mock := cmd.NewMockShellExecutor()
711+
ctx := cmd.WithShellExecutor(context.Background(), mock)
712+
result, err := tc.handler(ctx, newRequestWithArgs(tc.args))
713+
require.NoError(t, err)
714+
assert.True(t, result.IsError)
715+
assert.Empty(t, mock.GetCallLog())
716+
})
717+
}
718+
}
719+
720+
// TestCiliumCliHandlers covers the cilium-CLI based handlers.
721+
func TestCiliumCliHandlers(t *testing.T) {
722+
cases := []struct {
723+
name string
724+
handler ciliumHandler
725+
args map[string]any
726+
cliArgs []string
727+
}{
728+
{"show_cluster_mesh_status", handleShowClusterMeshStatus, map[string]any{}, []string{"clustermesh", "status"}},
729+
{"show_features_status", handleShowFeaturesStatus, map[string]any{}, []string{"features", "status"}},
730+
{"toggle_cluster_mesh_enable", handleToggleClusterMesh, map[string]any{"enable": "true"}, []string{"clustermesh", "enable"}},
731+
{"toggle_cluster_mesh_disable", handleToggleClusterMesh, map[string]any{"enable": "false"}, []string{"clustermesh", "disable"}},
732+
}
733+
734+
for _, tc := range cases {
735+
t.Run(tc.name, func(t *testing.T) {
736+
mock := cmd.NewMockShellExecutor()
737+
mock.AddCommandString("cilium", tc.cliArgs, "cli-ok", nil)
738+
ctx := cmd.WithShellExecutor(context.Background(), mock)
739+
result, err := tc.handler(ctx, newRequestWithArgs(tc.args))
740+
require.NoError(t, err)
741+
assert.False(t, result.IsError)
742+
assert.Contains(t, getResultText(result), "cli-ok")
743+
})
744+
}
745+
}
746+
747+
func TestCiliumCliHandlersError(t *testing.T) {
748+
mock := cmd.NewMockShellExecutor()
749+
mock.AddCommandString("cilium", []string{"clustermesh", "status"}, "", assert.AnError)
750+
ctx := cmd.WithShellExecutor(context.Background(), mock)
751+
result, err := handleShowClusterMeshStatus(ctx, newRequestWithArgs(map[string]any{}))
752+
require.NoError(t, err)
753+
assert.True(t, result.IsError)
754+
assert.Contains(t, getResultText(result), "Error getting cluster mesh status")
755+
}
756+
757+
// TestCiliumDbgHandlerError covers the error path shared by dbg handlers.
758+
func TestCiliumDbgHandlerError(t *testing.T) {
759+
mock := cmd.NewMockShellExecutor()
760+
mockCiliumDbgCommand(mock, []string{"loadinfo"}, "", assert.AnError)
761+
ctx := cmd.WithShellExecutor(context.Background(), mock)
762+
result, err := handleShowLoadInformation(ctx, newRequestWithArgs(map[string]any{"node_name": "test-node"}))
763+
require.NoError(t, err)
764+
assert.True(t, result.IsError)
765+
}

0 commit comments

Comments
 (0)