Skip to content

Commit fcb6ff2

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> Signed-off-by: Marc Nuri <marc@marcnuri.com>
1 parent f612d99 commit fcb6ff2

3 files changed

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

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: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,51 @@ func (s *OutputSuite) TestTableToStructured() {
188188
_, hasNs := result[0]["Namespace"]
189189
s.False(hasNs, "expected no Namespace key for cluster-scoped resource")
190190
})
191+
s.Run("returns nil for table with no rows", func() {
192+
t := &metav1.Table{
193+
ColumnDefinitions: []metav1.TableColumnDefinition{{Name: "Name"}},
194+
}
195+
result := tableToStructured(t)
196+
s.Nil(result)
197+
})
198+
s.Run("does not duplicate Namespace from embedded object when column exists", func() {
199+
t := &metav1.Table{
200+
ColumnDefinitions: []metav1.TableColumnDefinition{
201+
{Name: "Name"},
202+
{Name: "Namespace"},
203+
},
204+
Rows: []metav1.TableRow{{
205+
Cells: []any{"my-pod", "from-column"},
206+
Object: runtime.RawExtension{
207+
Object: &unstructured.Unstructured{Object: map[string]any{
208+
"metadata": map[string]any{"namespace": "from-object"},
209+
}},
210+
},
211+
}},
212+
}
213+
result := tableToStructured(t)
214+
s.Require().Len(result, 1)
215+
s.Equal("from-column", result[0]["Namespace"], "column value should take precedence over embedded object")
216+
})
217+
s.Run("adds Namespace from embedded object when no column exists", func() {
218+
t := &metav1.Table{
219+
ColumnDefinitions: []metav1.TableColumnDefinition{
220+
{Name: "Name"},
221+
},
222+
Rows: []metav1.TableRow{{
223+
Cells: []any{"my-pod"},
224+
Object: runtime.RawExtension{
225+
Object: &unstructured.Unstructured{Object: map[string]any{
226+
"metadata": map[string]any{"namespace": "default"},
227+
}},
228+
},
229+
}},
230+
}
231+
result := tableToStructured(t)
232+
s.Require().Len(result, 1)
233+
s.Equal("my-pod", result[0]["Name"])
234+
s.Equal("default", result[0]["Namespace"])
235+
})
191236
}
192237

193238
func TestOutput(t *testing.T) {

0 commit comments

Comments
 (0)