Skip to content

Commit ae6eced

Browse files
authored
test(mcp): add tests for WatchKubeConfig tools reload (#449)
Verifies that tools are added and **removed** when kubeconfig file changes. Signed-off-by: Marc Nuri <marc@marcnuri.com>
1 parent 24fd9bc commit ae6eced

File tree

3 files changed

+107
-48
lines changed

3 files changed

+107
-48
lines changed

internal/test/mock_server.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ func (m *MockServer) Handle(handler http.Handler) {
5959
m.restHandlers = append(m.restHandlers, handler.ServeHTTP)
6060
}
6161

62+
func (m *MockServer) ResetHandlers() {
63+
m.restHandlers = make([]http.HandlerFunc, 0)
64+
}
65+
6266
func (m *MockServer) Config() *rest.Config {
6367
return m.config
6468
}

pkg/mcp/mcp_test.go

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,14 @@
11
package mcp
22

33
import (
4-
"context"
54
"net/http"
6-
"os"
7-
"runtime"
85
"testing"
9-
"time"
106

117
"github.com/containers/kubernetes-mcp-server/internal/test"
128
"github.com/mark3labs/mcp-go/client/transport"
13-
"github.com/mark3labs/mcp-go/mcp"
149
"github.com/stretchr/testify/suite"
1510
)
1611

17-
type WatchKubeConfigSuite struct {
18-
BaseMcpSuite
19-
}
20-
21-
func (s *WatchKubeConfigSuite) SetupTest() {
22-
s.BaseMcpSuite.SetupTest()
23-
kubeconfig := test.KubeConfigFake()
24-
s.Cfg.KubeConfig = test.KubeconfigFile(s.T(), kubeconfig)
25-
}
26-
27-
func (s *WatchKubeConfigSuite) TestNotifiesToolsChange() {
28-
if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
29-
s.T().Skip("Skipping test on non-Unix-like platforms")
30-
}
31-
// Given
32-
s.InitMcpClient()
33-
withTimeout, cancel := context.WithTimeout(s.T().Context(), 5*time.Second)
34-
defer cancel()
35-
var notification *mcp.JSONRPCNotification
36-
s.OnNotification(func(n mcp.JSONRPCNotification) {
37-
notification = &n
38-
})
39-
// When
40-
f, _ := os.OpenFile(s.Cfg.KubeConfig, os.O_APPEND|os.O_WRONLY, 0644)
41-
_, _ = f.WriteString("\n")
42-
_ = f.Close()
43-
for notification == nil {
44-
select {
45-
case <-withTimeout.Done():
46-
s.FailNow("timeout waiting for WatchKubeConfig notification")
47-
default:
48-
time.Sleep(100 * time.Millisecond)
49-
}
50-
}
51-
// Then
52-
s.NotNil(notification, "WatchKubeConfig did not notify")
53-
s.Equal("notifications/tools/list_changed", notification.Method, "WatchKubeConfig did not notify tools change")
54-
}
55-
56-
func TestWatchKubeConfig(t *testing.T) {
57-
suite.Run(t, new(WatchKubeConfigSuite))
58-
}
59-
6012
type McpHeadersSuite struct {
6113
BaseMcpSuite
6214
mockServer *test.MockServer

pkg/mcp/mcp_watch_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package mcp
2+
3+
import (
4+
"context"
5+
"os"
6+
"testing"
7+
"time"
8+
9+
"github.com/containers/kubernetes-mcp-server/internal/test"
10+
"github.com/mark3labs/mcp-go/mcp"
11+
"github.com/stretchr/testify/suite"
12+
)
13+
14+
type WatchKubeConfigSuite struct {
15+
BaseMcpSuite
16+
mockServer *test.MockServer
17+
}
18+
19+
func (s *WatchKubeConfigSuite) SetupTest() {
20+
s.BaseMcpSuite.SetupTest()
21+
s.mockServer = test.NewMockServer()
22+
s.Cfg.KubeConfig = s.mockServer.KubeconfigFile(s.T())
23+
}
24+
25+
func (s *WatchKubeConfigSuite) TearDownTest() {
26+
s.BaseMcpSuite.TearDownTest()
27+
if s.mockServer != nil {
28+
s.mockServer.Close()
29+
}
30+
}
31+
32+
func (s *WatchKubeConfigSuite) WriteKubeconfig() {
33+
f, _ := os.OpenFile(s.Cfg.KubeConfig, os.O_APPEND|os.O_WRONLY, 0644)
34+
_, _ = f.WriteString("\n")
35+
_ = f.Close()
36+
}
37+
38+
// WaitForNotification waits for an MCP server notification or fails the test after a timeout
39+
func (s *WatchKubeConfigSuite) WaitForNotification() *mcp.JSONRPCNotification {
40+
withTimeout, cancel := context.WithTimeout(s.T().Context(), 5*time.Second)
41+
defer cancel()
42+
var notification *mcp.JSONRPCNotification
43+
s.OnNotification(func(n mcp.JSONRPCNotification) {
44+
notification = &n
45+
})
46+
for notification == nil {
47+
select {
48+
case <-withTimeout.Done():
49+
s.FailNow("timeout waiting for WatchKubeConfig notification")
50+
default:
51+
time.Sleep(100 * time.Millisecond)
52+
}
53+
}
54+
return notification
55+
}
56+
57+
func (s *WatchKubeConfigSuite) TestNotifiesToolsChange() {
58+
// Given
59+
s.InitMcpClient()
60+
// When
61+
s.WriteKubeconfig()
62+
notification := s.WaitForNotification()
63+
// Then
64+
s.NotNil(notification, "WatchKubeConfig did not notify")
65+
s.Equal("notifications/tools/list_changed", notification.Method, "WatchKubeConfig did not notify tools change")
66+
}
67+
68+
func (s *WatchKubeConfigSuite) TestClearsNoLongerAvailableTools() {
69+
s.mockServer.Handle(&test.InOpenShiftHandler{})
70+
s.InitMcpClient()
71+
72+
s.Run("OpenShift tool is available", func() {
73+
tools, err := s.ListTools(s.T().Context(), mcp.ListToolsRequest{})
74+
s.Require().NoError(err, "call ListTools failed")
75+
s.Require().NotNil(tools, "list tools failed")
76+
var found bool
77+
for _, tool := range tools.Tools {
78+
if tool.Name == "projects_list" {
79+
found = true
80+
break
81+
}
82+
}
83+
s.Truef(found, "expected OpenShift tool to be available")
84+
})
85+
86+
s.Run("OpenShift tool is removed after kubeconfig change", func() {
87+
// Reload Config without OpenShift
88+
s.mockServer.ResetHandlers()
89+
s.WriteKubeconfig()
90+
s.WaitForNotification()
91+
92+
tools, err := s.ListTools(s.T().Context(), mcp.ListToolsRequest{})
93+
s.Require().NoError(err, "call ListTools failed")
94+
s.Require().NotNil(tools, "list tools failed")
95+
for _, tool := range tools.Tools {
96+
s.Require().Falsef(tool.Name == "projects_list", "expected OpenShift tool to be removed")
97+
}
98+
})
99+
}
100+
101+
func TestWatchKubeConfig(t *testing.T) {
102+
suite.Run(t, new(WatchKubeConfigSuite))
103+
}

0 commit comments

Comments
 (0)