mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
Manifest-backed MCP tools, prompts, and resources with surface affordance contracts; agent capability manifest and governance projection; API contract tests and capability route projection; operations-loop and intelligence-funnel telemetry; release-control subsystem documentation, registry, and tooling; licensing and configuration.
130 lines
4.8 KiB
Go
130 lines
4.8 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai/chat"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
"github.com/stretchr/testify/mock"
|
|
)
|
|
|
|
// withOrgContext adds the default org ID to the request context
|
|
func withOrgContext(req *http.Request) *http.Request {
|
|
ctx := context.WithValue(req.Context(), OrgIDContextKey, "default")
|
|
return req.WithContext(ctx)
|
|
}
|
|
|
|
func TestProfileSuggestionHandler_MethodNotAllowed(t *testing.T) {
|
|
handler := NewProfileSuggestionHandler(config.NewMultiTenantPersistence(t.TempDir()), &AIHandler{})
|
|
req := withOrgContext(httptest.NewRequest(http.MethodGet, "/api/admin/profiles/suggestions", nil))
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler.HandleSuggestProfile(rr, req)
|
|
|
|
if rr.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rr.Code)
|
|
}
|
|
}
|
|
|
|
func TestProfileSuggestionHandler_ServiceUnavailable(t *testing.T) {
|
|
mockSvc := new(MockAIService)
|
|
mockSvc.On("IsRunning").Return(false)
|
|
aiHandler := &AIHandler{defaultService: mockSvc}
|
|
|
|
handler := NewProfileSuggestionHandler(config.NewMultiTenantPersistence(t.TempDir()), aiHandler)
|
|
req := withOrgContext(httptest.NewRequest(http.MethodPost, "/api/admin/profiles/suggestions", bytes.NewReader([]byte(`{"prompt":"test"}`))))
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler.HandleSuggestProfile(rr, req)
|
|
|
|
if rr.Code != http.StatusServiceUnavailable {
|
|
t.Fatalf("expected status %d, got %d", http.StatusServiceUnavailable, rr.Code)
|
|
}
|
|
if rr.Body.String() != "Pulse Intelligence is not available\n" {
|
|
t.Fatalf("unexpected service-unavailable body: %q", rr.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestProfileSuggestionHandler_InvalidRequest(t *testing.T) {
|
|
mockSvc := new(MockAIService)
|
|
mockSvc.On("IsRunning").Return(true)
|
|
aiHandler := &AIHandler{defaultService: mockSvc}
|
|
handler := NewProfileSuggestionHandler(config.NewMultiTenantPersistence(t.TempDir()), aiHandler)
|
|
|
|
req := withOrgContext(httptest.NewRequest(http.MethodPost, "/api/admin/profiles/suggestions", bytes.NewReader([]byte("{bad"))))
|
|
rr := httptest.NewRecorder()
|
|
handler.HandleSuggestProfile(rr, req)
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rr.Code)
|
|
}
|
|
|
|
req = withOrgContext(httptest.NewRequest(http.MethodPost, "/api/admin/profiles/suggestions", bytes.NewReader([]byte(`{"prompt":" "}`))))
|
|
rr = httptest.NewRecorder()
|
|
handler.HandleSuggestProfile(rr, req)
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rr.Code)
|
|
}
|
|
}
|
|
|
|
func TestProfileSuggestionHandler_SuccessAndParseFailure(t *testing.T) {
|
|
mtPersistence := config.NewMultiTenantPersistence(t.TempDir())
|
|
// Get the default tenant's persistence to save test data
|
|
persistence, err := mtPersistence.GetPersistence("default")
|
|
if err != nil {
|
|
t.Fatalf("get persistence: %v", err)
|
|
}
|
|
if err := persistence.SaveAgentProfiles([]models.AgentProfile{{Name: "Existing"}}); err != nil {
|
|
t.Fatalf("save profiles: %v", err)
|
|
}
|
|
|
|
mockSvc := new(MockAIService)
|
|
mockSvc.On("IsRunning").Return(true)
|
|
mockSvc.On("Execute", mock.Anything, mock.Anything).Return(map[string]interface{}{
|
|
"content": `{"name":"Suggested","description":"desc","config":{"interval":"30s"},"rationale":["reason"]}`,
|
|
}, nil).Once()
|
|
|
|
aiHandler := &AIHandler{defaultService: mockSvc}
|
|
handler := NewProfileSuggestionHandler(mtPersistence, aiHandler)
|
|
|
|
req := withOrgContext(httptest.NewRequest(http.MethodPost, "/api/admin/profiles/suggestions", bytes.NewReader([]byte(`{"prompt":"build a profile"}`))))
|
|
rr := httptest.NewRecorder()
|
|
handler.HandleSuggestProfile(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d", http.StatusOK, rr.Code)
|
|
}
|
|
|
|
var resp ProfileSuggestion
|
|
if err := json.NewDecoder(rr.Body).Decode(&resp); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if resp.Name != "Suggested" {
|
|
t.Fatalf("unexpected name: %q", resp.Name)
|
|
}
|
|
if resp.Config["interval"] == "" {
|
|
t.Fatalf("expected config in response")
|
|
}
|
|
|
|
mockSvc.On("Execute", mock.Anything, mock.Anything).Return(map[string]interface{}{
|
|
"content": "not json",
|
|
}, nil).Once()
|
|
|
|
req = withOrgContext(httptest.NewRequest(http.MethodPost, "/api/admin/profiles/suggestions", bytes.NewReader([]byte(`{"prompt":"bad response"}`))))
|
|
rr = httptest.NewRecorder()
|
|
handler.HandleSuggestProfile(rr, req)
|
|
if rr.Code != http.StatusInternalServerError {
|
|
t.Fatalf("expected status %d, got %d", http.StatusInternalServerError, rr.Code)
|
|
}
|
|
}
|
|
|
|
// Ensure MockAIService implements the interface methods used by the handler.
|
|
var _ interface {
|
|
IsRunning() bool
|
|
Execute(ctx context.Context, req chat.ExecuteRequest) (map[string]interface{}, error)
|
|
} = (*MockAIService)(nil)
|