Skip to content

Commit c912484

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

File tree

3 files changed

+111
-48
lines changed

3 files changed

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

0 commit comments

Comments
 (0)