test(ai): prove recovery storage tool fallbacks through service

This commit is contained in:
rcourtman
2026-03-26 23:17:22 +00:00
parent 0f152660ae
commit 5e158d144c
+157
View File
@@ -2,14 +2,17 @@ package chat
import (
"context"
"encoding/json"
"strings"
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/agentexec"
"github.com/rcourtman/pulse-go-rewrite/internal/ai/providers"
"github.com/rcourtman/pulse-go-rewrite/internal/ai/tools"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
"github.com/rcourtman/pulse-go-rewrite/internal/recovery"
ur "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
)
@@ -48,6 +51,41 @@ func (f *fakeCanonicalReadState) K8sDeployments() []*ur.K8sDeploymentView {
func (f *fakeCanonicalReadState) Workloads() []*ur.WorkloadView { return nil }
func (f *fakeCanonicalReadState) Infrastructure() []*ur.InfrastructureView { return nil }
type fakeRecoveryPointsProvider struct {
points []recovery.RecoveryPoint
}
func (f *fakeRecoveryPointsProvider) ListPoints(_ context.Context, opts recovery.ListPointsOptions) ([]recovery.RecoveryPoint, int, error) {
filtered := make([]recovery.RecoveryPoint, 0, len(f.points))
for _, point := range f.points {
if opts.Provider != "" && point.Provider != opts.Provider {
continue
}
if opts.Kind != "" && point.Kind != opts.Kind {
continue
}
filtered = append(filtered, point)
}
total := len(filtered)
if opts.Limit <= 0 {
return filtered, total, nil
}
page := opts.Page
if page <= 0 {
page = 1
}
start := (page - 1) * opts.Limit
if start >= total {
return []recovery.RecoveryPoint{}, total, nil
}
end := start + opts.Limit
if end > total {
end = total
}
return filtered[start:end], total, nil
}
func toolNameSet(list []providers.Tool) map[string]bool {
set := make(map[string]bool, len(list))
for _, tool := range list {
@@ -227,6 +265,125 @@ func TestExecuteMCPTool_ErrorsAndSuccess(t *testing.T) {
}
}
func TestExecuteMCPTool_PulseStorageSnapshotsToleratesMalformedRecoveryMetadata(t *testing.T) {
completedAt := time.Date(2026, 2, 24, 10, 30, 0, 0, time.UTC)
svc := &Service{
executor: tools.NewPulseToolExecutor(tools.ExecutorConfig{
StateProvider: fakeStateProvider{},
ReadState: &fakeCanonicalReadState{},
RecoveryPointsProvider: &fakeRecoveryPointsProvider{points: []recovery.RecoveryPoint{
{
ID: "pve-snapshot:snap-100-before-upgrade",
Provider: recovery.ProviderProxmoxPVE,
Kind: recovery.KindSnapshot,
Mode: recovery.ModeLocal,
Outcome: recovery.OutcomeSuccess,
SubjectRef: &recovery.ExternalRef{
Type: "vm",
Name: "100",
ID: "100",
Namespace: "pve1",
Class: "node1",
},
Display: &recovery.RecoveryPointDisplay{
SubjectLabel: "100",
ItemType: "vm",
ClusterLabel: "pve1",
NodeHostLabel: "node1",
EntityIDLabel: "100",
DetailsSummary: "before-upgrade",
},
CompletedAt: &completedAt,
},
}},
}),
}
output, err := svc.ExecuteMCPTool(context.Background(), "pulse_storage", map[string]interface{}{
"type": "snapshots",
"guest_id": "100",
"instance": "pve1",
})
if err != nil {
t.Fatalf("ExecuteMCPTool(pulse_storage snapshots): %v", err)
}
var resp tools.SnapshotsResponse
if err := json.Unmarshal([]byte(output), &resp); err != nil {
t.Fatalf("unmarshal snapshots response: %v\noutput=%s", err, output)
}
if resp.Total != 1 || resp.Filtered != 1 || len(resp.Snapshots) != 1 {
t.Fatalf("unexpected snapshots counts: total=%d filtered=%d len=%d", resp.Total, resp.Filtered, len(resp.Snapshots))
}
if resp.Snapshots[0].VMID != 100 || resp.Snapshots[0].Instance != "pve1" || resp.Snapshots[0].Node != "node1" {
t.Fatalf("unexpected snapshot identity: %#v", resp.Snapshots[0])
}
if resp.Snapshots[0].Type != "vm" || resp.Snapshots[0].SnapshotName != "before-upgrade" {
t.Fatalf("unexpected snapshot canonical fields: %#v", resp.Snapshots[0])
}
}
func TestExecuteMCPTool_PulseStorageBackupTasksToleratesMalformedRecoveryMetadata(t *testing.T) {
startedAt := time.Date(2026, 2, 24, 11, 0, 0, 0, time.UTC)
completedAt := time.Date(2026, 2, 24, 11, 15, 0, 0, time.UTC)
svc := &Service{
executor: tools.NewPulseToolExecutor(tools.ExecutorConfig{
StateProvider: fakeStateProvider{},
ReadState: &fakeCanonicalReadState{},
RecoveryPointsProvider: &fakeRecoveryPointsProvider{points: []recovery.RecoveryPoint{
{
ID: "pve-task:task-101-backup",
Provider: recovery.ProviderProxmoxPVE,
Kind: recovery.KindBackup,
Mode: recovery.ModeLocal,
Outcome: recovery.OutcomeSuccess,
SubjectRef: &recovery.ExternalRef{
Type: "vm",
Name: "101",
ID: "101",
Namespace: "pve1",
Class: "node1",
},
Display: &recovery.RecoveryPointDisplay{
SubjectLabel: "101",
ItemType: "vm",
ClusterLabel: "pve1",
NodeHostLabel: "node1",
EntityIDLabel: "101",
DetailsSummary: "completed successfully",
},
StartedAt: &startedAt,
CompletedAt: &completedAt,
},
}},
}),
}
output, err := svc.ExecuteMCPTool(context.Background(), "pulse_storage", map[string]interface{}{
"type": "backup_tasks",
"guest_id": "101",
"instance": "pve1",
"status": "OK",
})
if err != nil {
t.Fatalf("ExecuteMCPTool(pulse_storage backup_tasks): %v", err)
}
var resp tools.BackupTasksListResponse
if err := json.Unmarshal([]byte(output), &resp); err != nil {
t.Fatalf("unmarshal backup tasks response: %v\noutput=%s", err, output)
}
if resp.Total != 1 || resp.Filtered != 1 || len(resp.Tasks) != 1 {
t.Fatalf("unexpected backup task counts: total=%d filtered=%d len=%d", resp.Total, resp.Filtered, len(resp.Tasks))
}
if resp.Tasks[0].VMID != 101 || resp.Tasks[0].Instance != "pve1" || resp.Tasks[0].Node != "node1" {
t.Fatalf("unexpected backup task identity: %#v", resp.Tasks[0])
}
if resp.Tasks[0].Type != "vm" || resp.Tasks[0].Status != "OK" {
t.Fatalf("unexpected backup task canonical fields: %#v", resp.Tasks[0])
}
}
func TestService_StartInitializesActionAuditStore(t *testing.T) {
svc := NewService(Config{
AIConfig: &config.AIConfig{