mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-07 00:37:36 +00:00
169 lines
5.4 KiB
Go
169 lines
5.4 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
|
)
|
|
|
|
type stubAppContainerActionProvider struct {
|
|
calls []AppContainerActionRequest
|
|
result *AppContainerActionResult
|
|
err error
|
|
}
|
|
|
|
func (s *stubAppContainerActionProvider) ExecuteAction(_ context.Context, req AppContainerActionRequest) (*AppContainerActionResult, error) {
|
|
s.calls = append(s.calls, req)
|
|
if s.err != nil {
|
|
return nil, s.err
|
|
}
|
|
if s.result == nil {
|
|
return &AppContainerActionResult{
|
|
ResourceID: req.ResourceID,
|
|
ProviderUID: req.ProviderUID,
|
|
Name: req.Name,
|
|
Host: req.Host,
|
|
Platform: req.Platform,
|
|
Action: req.Action,
|
|
Status: "running",
|
|
Output: "ok",
|
|
}, nil
|
|
}
|
|
result := *s.result
|
|
return &result, nil
|
|
}
|
|
|
|
func TestPulseToolExecutor_ListTools_IncludesPulseControlForNativeAppProvider(t *testing.T) {
|
|
provider := newTrueNASUnifiedQueryProvider(t)
|
|
executor := NewPulseToolExecutor(ExecutorConfig{
|
|
UnifiedResourceProvider: provider,
|
|
ReadState: provider.ResourceRegistry,
|
|
AppContainerActionProvider: &stubAppContainerActionProvider{},
|
|
ControlLevel: ControlLevelControlled,
|
|
})
|
|
|
|
tools := executor.ListTools()
|
|
found := false
|
|
for _, tool := range tools {
|
|
if tool.Name == "pulse_control" {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected pulse_control to be available with native app action provider, got %+v", tools)
|
|
}
|
|
}
|
|
|
|
func TestPulseToolExecutor_ListTools_PulseControlDescriptionStaysCapabilityBounded(t *testing.T) {
|
|
executor := NewPulseToolExecutor(ExecutorConfig{
|
|
StateProvider: &mockStateProvider{},
|
|
AgentServer: &mockAgentServer{},
|
|
ControlLevel: ControlLevelControlled,
|
|
})
|
|
|
|
tools := executor.ListTools()
|
|
for _, tool := range tools {
|
|
if tool.Name != "pulse_control" {
|
|
continue
|
|
}
|
|
if !strings.Contains(tool.Description, "explicitly advertise shared Pulse actions") {
|
|
t.Fatalf("expected pulse_control description to stay capability-bounded, got %q", tool.Description)
|
|
}
|
|
if !strings.Contains(tool.Description, "read-only") {
|
|
t.Fatalf("expected pulse_control description to warn about read-only resources, got %q", tool.Description)
|
|
}
|
|
if action := tool.InputSchema.Properties["action"].Description; !strings.Contains(action, "resolved resource's shared action set") {
|
|
t.Fatalf("expected pulse_control action schema to describe shared action gating, got %q", action)
|
|
}
|
|
return
|
|
}
|
|
t.Fatalf("expected pulse_control to be available, got %+v", tools)
|
|
}
|
|
|
|
func TestExecuteControlResource_TrueNASAppUsesNativeActionProvider(t *testing.T) {
|
|
provider := newTrueNASUnifiedQueryProvider(t)
|
|
resolved := &mockResolvedContext{
|
|
resources: make(map[string]ResolvedResourceInfo),
|
|
aliases: make(map[string]ResolvedResourceInfo),
|
|
}
|
|
actionProvider := &stubAppContainerActionProvider{
|
|
result: &AppContainerActionResult{
|
|
ResourceID: "app-container:truenas-main:nextcloud",
|
|
ProviderUID: "nextcloud",
|
|
Name: "Nextcloud",
|
|
Host: "truenas-main",
|
|
Platform: "truenas",
|
|
Action: "restart",
|
|
Status: "running",
|
|
Output: "restart app Nextcloud on truenas-main; current state=running",
|
|
},
|
|
}
|
|
store := unifiedresources.NewMemoryStore()
|
|
executor := NewPulseToolExecutor(ExecutorConfig{
|
|
UnifiedResourceProvider: provider,
|
|
ReadState: provider.ResourceRegistry,
|
|
AppContainerActionProvider: actionProvider,
|
|
ActionAuditStore: store,
|
|
})
|
|
executor.SetResolvedContext(resolved)
|
|
|
|
if _, err := executor.executeGetResource(context.Background(), map[string]interface{}{
|
|
"resource_type": "app-container",
|
|
"resource_id": "nextcloud",
|
|
}); err != nil {
|
|
t.Fatalf("seed resolved context: unexpected error: %v", err)
|
|
}
|
|
|
|
result, err := executor.executeControl(context.Background(), map[string]interface{}{
|
|
"type": "resource",
|
|
"resource_id": "Nextcloud",
|
|
"action": "restart",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("executeControl(type=resource): unexpected error: %v", err)
|
|
}
|
|
if result.IsError {
|
|
t.Fatalf("expected success result, got %+v", result)
|
|
}
|
|
|
|
var payload map[string]any
|
|
if err := json.Unmarshal([]byte(result.Content[0].Text), &payload); err != nil {
|
|
t.Fatalf("decode result payload: %v", err)
|
|
}
|
|
if payload["platform"] != "truenas" {
|
|
t.Fatalf("expected truenas platform, got %+v", payload)
|
|
}
|
|
if payload["action"] != "restart" || payload["status"] != "running" {
|
|
t.Fatalf("unexpected action payload: %+v", payload)
|
|
}
|
|
|
|
if len(actionProvider.calls) != 1 {
|
|
t.Fatalf("expected one native app action call, got %+v", actionProvider.calls)
|
|
}
|
|
call := actionProvider.calls[0]
|
|
if call.OrgID != "default" || call.ProviderUID != "nextcloud" || call.Host != "truenas-main" || call.Action != "restart" {
|
|
t.Fatalf("unexpected native app action request: %+v", call)
|
|
}
|
|
|
|
audits, err := store.GetActionAudits("", time.Time{}, 10)
|
|
if err != nil {
|
|
t.Fatalf("GetActionAudits() error = %v", err)
|
|
}
|
|
if len(audits) != 1 {
|
|
t.Fatalf("expected one action audit, got %+v", audits)
|
|
}
|
|
if audits[0].Request.CapabilityName != "pulse_control" {
|
|
t.Fatalf("unexpected capability name: %+v", audits[0].Request)
|
|
}
|
|
if got := audits[0].Request.Params["action"]; got != "restart" {
|
|
t.Fatalf("expected audited action restart, got %+v", audits[0].Request.Params)
|
|
}
|
|
if got := audits[0].Request.Params["platform"]; got != "truenas" {
|
|
t.Fatalf("expected audited platform truenas, got %+v", audits[0].Request.Params)
|
|
}
|
|
}
|