mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-30 12:30:17 +00:00
UI/UX Improvements for AI-skeptical users: - Only show 'Ideas' button if AI is enabled AND configured - Renamed 'Suggest Profile' to 'Ideas' with lightbulb icon - Moved 'New Profile' button to primary position - Changed AI button styling from prominent purple to subtle gray - Updated modal title to 'Profile Ideas' with neutral language Multi-tenant bug fix: - ProfileSuggestionHandler now uses MultiTenantPersistence - Properly resolves tenant-specific persistence from request context - Fixes potential nil pointer panic in multi-tenant deployments - Existing profiles are now correctly loaded per-tenant for AI context Tests updated to use MultiTenantPersistence with org context injection.
127 lines
4.6 KiB
Go
127 lines
4.6 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{legacyService: 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)
|
|
}
|
|
}
|
|
|
|
func TestProfileSuggestionHandler_InvalidRequest(t *testing.T) {
|
|
mockSvc := new(MockAIService)
|
|
mockSvc.On("IsRunning").Return(true)
|
|
aiHandler := &AIHandler{legacyService: 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{legacyService: 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)
|