Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pkg/mcp/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ func TestMain(m *testing.M) {
CRD("instancetype.kubevirt.io", "v1beta1", "virtualmachineinstancetypes", "VirtualMachineInstancetype", "virtualmachineinstancetype", true),
CRD("instancetype.kubevirt.io", "v1beta1", "virtualmachineclusterpreferences", "VirtualMachineClusterPreference", "virtualmachineclusterpreference", false),
CRD("instancetype.kubevirt.io", "v1beta1", "virtualmachinepreferences", "VirtualMachinePreference", "virtualmachinepreference", true),
// OADP / Velero
CRD("velero.io", "v1", "backups", "Backup", "backup", true),
CRD("velero.io", "v1", "restores", "Restore", "restore", true),
CRD("velero.io", "v1", "backupstoragelocations", "BackupStorageLocation", "backupstoragelocation", true),
CRD("oadp.openshift.io", "v1alpha1", "dataprotectionapplications", "DataProtectionApplication", "dataprotectionapplication", true),
},
}
// Configure API server for faster CRD establishment and test performance
Expand Down
185 changes: 185 additions & 0 deletions pkg/mcp/oadp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package mcp

import (
"testing"

"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/stretchr/testify/suite"
"golang.org/x/sync/errgroup"

oadpToolset "github.com/containers/kubernetes-mcp-server/pkg/toolsets/oadp"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
)

var oadpApis = []schema.GroupVersionResource{
{Group: "velero.io", Version: "v1", Resource: "backups"},
{Group: "velero.io", Version: "v1", Resource: "restores"},
{Group: "velero.io", Version: "v1", Resource: "backupstoragelocations"},
{Group: "oadp.openshift.io", Version: "v1alpha1", Resource: "dataprotectionapplications"},
}

type OADPSuite struct {
BaseMcpSuite
}

func (s *OADPSuite) SetupSuite() {
ctx := s.T().Context()
tasks, _ := errgroup.WithContext(ctx)
for _, api := range oadpApis {
gvr := api
tasks.Go(func() error { return EnvTestEnableCRD(ctx, gvr.Group, gvr.Version, gvr.Resource) })
}
s.Require().NoError(tasks.Wait())

_, err := kubernetes.NewForConfigOrDie(envTestRestConfig).CoreV1().Namespaces().
Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "openshift-adp"}}, metav1.CreateOptions{})
s.Require().NoError(err)
}

func (s *OADPSuite) TearDownSuite() {
tasks, _ := errgroup.WithContext(s.T().Context())
for _, api := range oadpApis {
gvr := api
tasks.Go(func() error { return EnvTestDisableCRD(s.T().Context(), gvr.Group, gvr.Version, gvr.Resource) })
}
s.Require().NoError(tasks.Wait())
}

func (s *OADPSuite) SetupTest() {
s.BaseMcpSuite.SetupTest()
s.Cfg.Toolsets = append(s.Cfg.Toolsets, (&oadpToolset.Toolset{}).GetName())
s.InitMcpClient()
}

func (s *OADPSuite) TestToolsetRegistration() {
s.Run("toolset has no tools", func() {
tools, err := s.ListTools()
s.Require().NoError(err)
for _, tool := range tools.Tools {
s.Falsef(tool.Name == "oadp_backup" || tool.Name == "oadp_restore",
"OADP toolset should not expose dedicated tools, found: %s", tool.Name)
}
})

s.Run("toolset exposes oadp-troubleshoot prompt", func() {
prompts, err := s.ListPrompts()
s.Require().NoError(err)
var found bool
for _, prompt := range prompts.Prompts {
if prompt.Name == "oadp-troubleshoot" {
found = true
s.Equal("Generate a step-by-step troubleshooting guide for diagnosing OADP backup and restore issues", prompt.Description)
s.Len(prompt.Arguments, 3)
break
}
}
s.True(found, "expected oadp-troubleshoot prompt to be listed")
})
}

func (s *OADPSuite) TestTroubleshootPromptDefaultNamespace() {
result, err := s.GetPrompt("oadp-troubleshoot", map[string]string{})

s.Run("returns successfully", func() {
s.Require().NoError(err)
s.Require().NotNil(result)
})

s.Run("contains diagnostic sections", func() {
s.Require().NotNil(result)
s.Require().NotEmpty(result.Messages)
text := result.Messages[0].Content.(*mcp.TextContent).Text
s.Contains(text, "Namespace: openshift-adp")
s.Contains(text, "DataProtectionApplication")
s.Contains(text, "BackupStorageLocations")
s.Contains(text, "Recent Backups")
s.Contains(text, "Recent Restores")
s.Contains(text, "Velero Pods")
s.Contains(text, "Events")
})

s.Run("includes assistant analysis message", func() {
s.Require().NotNil(result)
s.Require().Len(result.Messages, 2)
s.Equal(mcp.Role("assistant"), result.Messages[1].Role)
})
}

func (s *OADPSuite) TestTroubleshootPromptCustomNamespace() {
result, err := s.GetPrompt("oadp-troubleshoot", map[string]string{
"namespace": "custom-ns",
})

s.Run("uses custom namespace", func() {
s.Require().NoError(err)
s.Require().NotNil(result)
s.Require().NotEmpty(result.Messages)
text := result.Messages[0].Content.(*mcp.TextContent).Text
s.Contains(text, "Namespace: custom-ns")
})
}

func (s *OADPSuite) TestTroubleshootPromptWithBackup() {
s.Run("with existing backup", func() {
dynamicClient := dynamic.NewForConfigOrDie(envTestRestConfig)
backup := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "velero.io/v1",
"kind": "Backup",
"metadata": map[string]any{
"name": "test-backup",
"namespace": "openshift-adp",
},
"spec": map[string]any{
"includedNamespaces": []any{"default"},
},
"status": map[string]any{
"phase": "Completed",
},
},
}
_, err := dynamicClient.Resource(schema.GroupVersionResource{
Group: "velero.io", Version: "v1", Resource: "backups",
}).Namespace("openshift-adp").Create(s.T().Context(), backup, metav1.CreateOptions{})
s.Require().NoError(err)

result, err := s.GetPrompt("oadp-troubleshoot", map[string]string{
"backup": "test-backup",
})
s.Require().NoError(err)
s.Require().NotNil(result)
text := result.Messages[0].Content.(*mcp.TextContent).Text
s.Contains(text, "Backup: test-backup")
})

s.Run("with non-existent backup", func() {
result, err := s.GetPrompt("oadp-troubleshoot", map[string]string{
"backup": "non-existent",
})
s.Require().NoError(err)
s.Require().NotNil(result)
text := result.Messages[0].Content.(*mcp.TextContent).Text
s.Contains(text, "Error fetching backup")
})
}

func (s *OADPSuite) TestTroubleshootPromptWithRestore() {
s.Run("with non-existent restore", func() {
result, err := s.GetPrompt("oadp-troubleshoot", map[string]string{
"restore": "non-existent",
})
s.Require().NoError(err)
s.Require().NotNil(result)
text := result.Messages[0].Content.(*mcp.TextContent).Text
s.Contains(text, "Error fetching restore")
})
}

func TestOADPSuite(t *testing.T) {
suite.Run(t, new(OADPSuite))
}
88 changes: 88 additions & 0 deletions pkg/toolsets/oadp/toolset_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package oadp

import (
"testing"

"github.com/stretchr/testify/suite"
)

type ToolsetSuite struct {
suite.Suite
}

func (s *ToolsetSuite) TestGetName() {
s.Run("returns oadp", func() {
t := &Toolset{}
s.Equal("oadp", t.GetName())
})
}

func (s *ToolsetSuite) TestGetDescription() {
s.Run("returns non-empty description", func() {
t := &Toolset{}
s.NotEmpty(t.GetDescription())
})
}

func (s *ToolsetSuite) TestGetTools() {
s.Run("returns nil", func() {
t := &Toolset{}
s.Nil(t.GetTools(nil))
})
}

func (s *ToolsetSuite) TestGetPrompts() {
s.Run("returns one prompt", func() {
t := &Toolset{}
prompts := t.GetPrompts()
s.Len(prompts, 1)
})

s.Run("prompt is named oadp-troubleshoot", func() {
t := &Toolset{}
prompts := t.GetPrompts()
s.Require().Len(prompts, 1)
s.Equal("oadp-troubleshoot", prompts[0].Prompt.Name)
})

s.Run("prompt has three arguments", func() {
t := &Toolset{}
prompts := t.GetPrompts()
s.Require().Len(prompts, 1)
s.Len(prompts[0].Prompt.Arguments, 3)
})

s.Run("all prompt arguments are optional", func() {
t := &Toolset{}
prompts := t.GetPrompts()
s.Require().Len(prompts, 1)
for _, arg := range prompts[0].Prompt.Arguments {
s.False(arg.Required)
}
})

s.Run("prompt has a handler", func() {
t := &Toolset{}
prompts := t.GetPrompts()
s.Require().Len(prompts, 1)
s.NotNil(prompts[0].Handler)
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

func (s *ToolsetSuite) TestGetResources() {
s.Run("returns nil", func() {
t := &Toolset{}
s.Nil(t.GetResources())
})
}

func (s *ToolsetSuite) TestGetResourceTemplates() {
s.Run("returns nil", func() {
t := &Toolset{}
s.Nil(t.GetResourceTemplates())
})
}

func TestToolsetSuite(t *testing.T) {
suite.Run(t, new(ToolsetSuite))
}