Skip to content

Commit 7da0b28

Browse files
manusaclaude
andcommitted
test: add tests for review fix behaviors (resource lifecycle, meta merge, namespace dedup)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 30b7420 commit 7da0b28

3 files changed

Lines changed: 174 additions & 0 deletions

File tree

pkg/mcp/text_result_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"errors"
55
"testing"
66

7+
"github.com/containers/kubernetes-mcp-server/pkg/mcpapps"
78
"github.com/modelcontextprotocol/go-sdk/mcp"
89
"github.com/stretchr/testify/suite"
910
)
@@ -114,3 +115,70 @@ func (s *TextResultSuite) TestNewStructuredResult() {
114115
func TestTextResult(t *testing.T) {
115116
suite.Run(t, new(TextResultSuite))
116117
}
118+
119+
type RegisterMCPAppResourcesSuite struct {
120+
suite.Suite
121+
}
122+
123+
func (s *RegisterMCPAppResourcesSuite) newServer() *Server {
124+
return &Server{
125+
server: mcp.NewServer(
126+
&mcp.Implementation{Name: "test"},
127+
&mcp.ServerOptions{
128+
Capabilities: &mcp.ServerCapabilities{
129+
Resources: &mcp.ResourceCapabilities{},
130+
},
131+
},
132+
),
133+
}
134+
}
135+
136+
func (s *RegisterMCPAppResourcesSuite) TestTracksRegisteredURIs() {
137+
srv := s.newServer()
138+
srv.registerMCPAppResources([]string{"pods_list", "nodes_top"})
139+
s.ElementsMatch(
140+
[]string{
141+
mcpapps.ToolResourceURI("pods_list"),
142+
mcpapps.ToolResourceURI("nodes_top"),
143+
},
144+
srv.registeredAppURIs,
145+
)
146+
}
147+
148+
func (s *RegisterMCPAppResourcesSuite) TestRemovesStaleResources() {
149+
srv := s.newServer()
150+
// First registration: two tools
151+
srv.registerMCPAppResources([]string{"pods_list", "nodes_top"})
152+
s.Len(srv.registeredAppURIs, 2)
153+
// Second registration: only one tool remains — stale nodes_top should be cleaned up
154+
srv.registerMCPAppResources([]string{"pods_list"})
155+
s.Equal(
156+
[]string{mcpapps.ToolResourceURI("pods_list")},
157+
srv.registeredAppURIs,
158+
)
159+
}
160+
161+
func (s *RegisterMCPAppResourcesSuite) TestUpdatesTrackingOnReRegister() {
162+
srv := s.newServer()
163+
srv.registerMCPAppResources([]string{"pods_list"})
164+
srv.registerMCPAppResources([]string{"pods_list", "namespaces_list"})
165+
s.ElementsMatch(
166+
[]string{
167+
mcpapps.ToolResourceURI("pods_list"),
168+
mcpapps.ToolResourceURI("namespaces_list"),
169+
},
170+
srv.registeredAppURIs,
171+
)
172+
}
173+
174+
func (s *RegisterMCPAppResourcesSuite) TestEmptyListClearsAll() {
175+
srv := s.newServer()
176+
srv.registerMCPAppResources([]string{"pods_list"})
177+
s.Len(srv.registeredAppURIs, 1)
178+
srv.registerMCPAppResources([]string{})
179+
s.Empty(srv.registeredAppURIs)
180+
}
181+
182+
func TestRegisterMCPAppResources(t *testing.T) {
183+
suite.Run(t, new(RegisterMCPAppResourcesSuite))
184+
}

pkg/mcp/tool_mutator_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,3 +307,62 @@ func (s *TargetListToolMutatorSuite) TestHandlerWithGetTargetsError() {
307307
func TestTargetListToolMutator(t *testing.T) {
308308
suite.Run(t, new(TargetListToolMutatorSuite))
309309
}
310+
311+
type AppsMetaMutatorSuite struct {
312+
suite.Suite
313+
}
314+
315+
func (s *AppsMetaMutatorSuite) TestInjectsMetaWhenNil() {
316+
tool := createTestTool("pods_list")
317+
s.Nil(tool.Tool.Meta)
318+
mutator := WithAppsMeta()
319+
result := mutator(tool)
320+
s.Run("sets Meta", func() {
321+
s.NotNil(result.Tool.Meta)
322+
})
323+
s.Run("contains ui key", func() {
324+
_, hasUI := result.Tool.Meta["ui"]
325+
s.True(hasUI)
326+
})
327+
s.Run("contains legacy flat key", func() {
328+
_, hasLegacy := result.Tool.Meta["ui/resourceUri"]
329+
s.True(hasLegacy)
330+
})
331+
}
332+
333+
func (s *AppsMetaMutatorSuite) TestMergesIntoExistingMeta() {
334+
tool := createTestTool("pods_list")
335+
tool.Tool.Meta = map[string]any{"custom": "value"}
336+
mutator := WithAppsMeta()
337+
result := mutator(tool)
338+
s.Run("preserves existing keys", func() {
339+
s.Equal("value", result.Tool.Meta["custom"])
340+
})
341+
s.Run("adds ui key", func() {
342+
_, hasUI := result.Tool.Meta["ui"]
343+
s.True(hasUI)
344+
})
345+
s.Run("adds legacy flat key", func() {
346+
_, hasLegacy := result.Tool.Meta["ui/resourceUri"]
347+
s.True(hasLegacy)
348+
})
349+
}
350+
351+
func (s *AppsMetaMutatorSuite) TestSkipsWhenUIKeyExists() {
352+
existingUI := map[string]any{"resourceUri": "ui://custom/resource"}
353+
tool := createTestTool("pods_list")
354+
tool.Tool.Meta = map[string]any{"ui": existingUI}
355+
mutator := WithAppsMeta()
356+
result := mutator(tool)
357+
s.Run("does not overwrite existing ui key", func() {
358+
s.Equal(existingUI, result.Tool.Meta["ui"])
359+
})
360+
s.Run("does not add legacy flat key", func() {
361+
_, hasLegacy := result.Tool.Meta["ui/resourceUri"]
362+
s.False(hasLegacy)
363+
})
364+
}
365+
366+
func TestAppsMetaMutator(t *testing.T) {
367+
suite.Run(t, new(AppsMetaMutatorSuite))
368+
}

pkg/output/output_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import (
66
"testing"
77

88
"github.com/stretchr/testify/suite"
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
910
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
11+
"k8s.io/apimachinery/pkg/runtime"
1012
)
1113

1214
var podListJSON = `{
@@ -111,6 +113,51 @@ func (s *OutputSuite) TestTableToStructured() {
111113
result := tableToStructured(nil)
112114
s.Nil(result)
113115
})
116+
s.Run("returns nil for table with no rows", func() {
117+
t := &metav1.Table{
118+
ColumnDefinitions: []metav1.TableColumnDefinition{{Name: "Name"}},
119+
}
120+
result := tableToStructured(t)
121+
s.Nil(result)
122+
})
123+
s.Run("does not duplicate Namespace from embedded object when column exists", func() {
124+
t := &metav1.Table{
125+
ColumnDefinitions: []metav1.TableColumnDefinition{
126+
{Name: "Name"},
127+
{Name: "Namespace"},
128+
},
129+
Rows: []metav1.TableRow{{
130+
Cells: []any{"my-pod", "from-column"},
131+
Object: runtime.RawExtension{
132+
Object: &unstructured.Unstructured{Object: map[string]any{
133+
"metadata": map[string]any{"namespace": "from-object"},
134+
}},
135+
},
136+
}},
137+
}
138+
result := tableToStructured(t)
139+
s.Require().Len(result, 1)
140+
s.Equal("from-column", result[0]["Namespace"], "column value should take precedence over embedded object")
141+
})
142+
s.Run("adds Namespace from embedded object when no column exists", func() {
143+
t := &metav1.Table{
144+
ColumnDefinitions: []metav1.TableColumnDefinition{
145+
{Name: "Name"},
146+
},
147+
Rows: []metav1.TableRow{{
148+
Cells: []any{"my-pod"},
149+
Object: runtime.RawExtension{
150+
Object: &unstructured.Unstructured{Object: map[string]any{
151+
"metadata": map[string]any{"namespace": "default"},
152+
}},
153+
},
154+
}},
155+
}
156+
result := tableToStructured(t)
157+
s.Require().Len(result, 1)
158+
s.Equal("my-pod", result[0]["Name"])
159+
s.Equal("default", result[0]["Namespace"])
160+
})
114161
}
115162

116163
func TestOutput(t *testing.T) {

0 commit comments

Comments
 (0)