package api import ( "context" "encoding/json" "net/http" "net/http/httptest" "path/filepath" "strings" "testing" "github.com/rcourtman/pulse-go-rewrite/internal/agentexec" "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/rcourtman/pulse-go-rewrite/internal/monitoring" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) type MockAIService struct { mock.Mock } func (m *MockAIService) Start(ctx context.Context) error { args := m.Called(ctx) return args.Error(0) } func (m *MockAIService) Stop(ctx context.Context) error { args := m.Called(ctx) return args.Error(0) } func (m *MockAIService) Restart(ctx context.Context, newCfg *config.AIConfig) error { args := m.Called(ctx, newCfg) return args.Error(0) } func (m *MockAIService) IsRunning() bool { args := m.Called() return args.Bool(0) } func (m *MockAIService) Execute(ctx context.Context, req chat.ExecuteRequest) (map[string]interface{}, error) { args := m.Called(ctx, req) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(map[string]interface{}), args.Error(1) } func (m *MockAIService) ExecuteStream(ctx context.Context, req chat.ExecuteRequest, callback chat.StreamCallback) error { args := m.Called(ctx, req, callback) return args.Error(0) } func (m *MockAIService) ListSessions(ctx context.Context) ([]chat.Session, error) { args := m.Called(ctx) return args.Get(0).([]chat.Session), args.Error(1) } func (m *MockAIService) CreateSession(ctx context.Context) (*chat.Session, error) { args := m.Called(ctx) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*chat.Session), args.Error(1) } func (m *MockAIService) DeleteSession(ctx context.Context, sessionID string) error { args := m.Called(ctx, sessionID) return args.Error(0) } func (m *MockAIService) GetMessages(ctx context.Context, sessionID string) ([]chat.Message, error) { args := m.Called(ctx, sessionID) return args.Get(0).([]chat.Message), args.Error(1) } func (m *MockAIService) AbortSession(ctx context.Context, sessionID string) error { args := m.Called(ctx, sessionID) return args.Error(0) } func (m *MockAIService) SummarizeSession(ctx context.Context, sessionID string) (map[string]interface{}, error) { args := m.Called(ctx, sessionID) return args.Get(0).(map[string]interface{}), args.Error(1) } func (m *MockAIService) GetSessionDiff(ctx context.Context, sessionID string) (map[string]interface{}, error) { args := m.Called(ctx, sessionID) return args.Get(0).(map[string]interface{}), args.Error(1) } func (m *MockAIService) ForkSession(ctx context.Context, sessionID string) (*chat.Session, error) { args := m.Called(ctx, sessionID) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*chat.Session), args.Error(1) } func (m *MockAIService) RevertSession(ctx context.Context, sessionID string) (map[string]interface{}, error) { args := m.Called(ctx, sessionID) return args.Get(0).(map[string]interface{}), args.Error(1) } func (m *MockAIService) UnrevertSession(ctx context.Context, sessionID string) (map[string]interface{}, error) { args := m.Called(ctx, sessionID) return args.Get(0).(map[string]interface{}), args.Error(1) } func (m *MockAIService) AnswerQuestion(ctx context.Context, questionID string, answers []chat.QuestionAnswer) error { args := m.Called(ctx, questionID, answers) return args.Error(0) } func (m *MockAIService) SetAlertProvider(provider chat.MCPAlertProvider) { m.Called(provider) } func (m *MockAIService) SetFindingsProvider(provider chat.MCPFindingsProvider) { m.Called(provider) } func (m *MockAIService) SetBaselineProvider(provider chat.MCPBaselineProvider) { m.Called(provider) } func (m *MockAIService) SetPatternProvider(provider chat.MCPPatternProvider) { m.Called(provider) } func (m *MockAIService) SetMetricsHistory(provider chat.MCPMetricsHistoryProvider) { m.Called(provider) } func (m *MockAIService) SetBackupProvider(provider chat.MCPBackupProvider) { m.Called(provider) } func (m *MockAIService) SetStorageProvider(provider chat.MCPStorageProvider) { m.Called(provider) } func (m *MockAIService) SetGuestConfigProvider(provider chat.MCPGuestConfigProvider) { m.Called(provider) } func (m *MockAIService) SetDiskHealthProvider(provider chat.MCPDiskHealthProvider) { m.Called(provider) } func (m *MockAIService) SetUpdatesProvider(provider chat.MCPUpdatesProvider) { m.Called(provider) } func (m *MockAIService) SetAgentProfileManager(manager chat.AgentProfileManager) { m.Called(manager) } func (m *MockAIService) SetFindingsManager(manager chat.FindingsManager) { m.Called(manager) } func (m *MockAIService) SetMetadataUpdater(updater chat.MetadataUpdater) { m.Called(updater) } func (m *MockAIService) SetKnowledgeStoreProvider(provider chat.KnowledgeStoreProvider) { m.Called(provider) } func (m *MockAIService) SetIncidentRecorderProvider(provider chat.IncidentRecorderProvider) { m.Called(provider) } func (m *MockAIService) SetEventCorrelatorProvider(provider chat.EventCorrelatorProvider) { m.Called(provider) } func (m *MockAIService) SetTopologyProvider(provider chat.TopologyProvider) { m.Called(provider) } func (m *MockAIService) SetDiscoveryProvider(provider chat.MCPDiscoveryProvider) { m.Called(provider) } func (m *MockAIService) UpdateControlSettings(cfg *config.AIConfig) { m.Called(cfg) } func (m *MockAIService) GetBaseURL() string { args := m.Called() return args.String(0) } type MockAIPersistence struct { mock.Mock dataDir string } func (m *MockAIPersistence) LoadAIConfig() (*config.AIConfig, error) { args := m.Called() if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*config.AIConfig), args.Error(1) } func (m *MockAIPersistence) DataDir() string { return m.dataDir } type MockAIStateProvider struct { mock.Mock } func (m *MockAIStateProvider) GetState() models.StateSnapshot { args := m.Called() return args.Get(0).(models.StateSnapshot) } func newTestAIHandler(cfg *config.Config, persistence AIPersistence, _ *agentexec.Server) *AIHandler { handler := NewAIHandler(nil, nil, nil) handler.legacyConfig = cfg handler.legacyPersistence = persistence return handler } func TestStart(t *testing.T) { // Mock newChatService oldNewService := newChatService defer func() { newChatService = oldNewService }() mockSvc := new(MockAIService) var capturedCfg chat.Config newChatService = func(cfg chat.Config) AIService { capturedCfg = cfg return mockSvc } mockPersist := new(MockAIPersistence) h := newTestAIHandler(&config.Config{}, mockPersist, nil) // AI disabled in config mockPersist.On("LoadAIConfig").Return(&config.AIConfig{Enabled: false}, nil).Once() err := h.Start(context.Background(), nil) assert.NoError(t, err) assert.Nil(t, h.legacyService) // AI enabled aiCfg := &config.AIConfig{Enabled: true, Model: "test"} mockPersist.On("LoadAIConfig").Return(aiCfg, nil).Once() mockSvc.On("Start", mock.Anything).Return(nil).Once() err = h.Start(context.Background(), nil) assert.NoError(t, err) assert.Equal(t, mockSvc, h.legacyService) assert.Equal(t, "data", capturedCfg.DataDir) } func TestStop(t *testing.T) { mockSvc := new(MockAIService) h := newTestAIHandler(nil, nil, nil) h.legacyService = mockSvc mockSvc.On("Stop", mock.Anything).Return(nil) err := h.Stop(context.Background()) assert.NoError(t, err) // Nil service h.legacyService = nil err = h.Stop(context.Background()) assert.NoError(t, err) } func TestStart_Error(t *testing.T) { oldNewService := newChatService defer func() { newChatService = oldNewService }() mockSvc := new(MockAIService) newChatService = func(cfg chat.Config) AIService { return mockSvc } mockPersist := new(MockAIPersistence) h := newTestAIHandler(&config.Config{}, mockPersist, nil) aiCfg := &config.AIConfig{Enabled: true, Model: "test"} mockPersist.On("LoadAIConfig").Return(aiCfg, nil) mockSvc.On("Start", mock.Anything).Return(assert.AnError) err := h.Start(context.Background(), nil) assert.Error(t, err) } func TestRestart(t *testing.T) { mockPersist := new(MockAIPersistence) mockSvc := new(MockAIService) h := newTestAIHandler(nil, mockPersist, nil) h.legacyService = mockSvc aiCfg := &config.AIConfig{} mockPersist.On("LoadAIConfig").Return(aiCfg, nil) mockSvc.On("IsRunning").Return(true) mockSvc.On("Restart", mock.Anything, aiCfg).Return(nil) err := h.Restart(context.Background()) assert.NoError(t, err) // Service nil h.legacyService = nil err = h.Restart(context.Background()) assert.NoError(t, err) } func TestRestart_StartsStoppedServiceWithSavedStateProvider(t *testing.T) { oldNewService := newChatService defer func() { newChatService = oldNewService }() mockPersist := &MockAIPersistence{dataDir: "data"} stoppedSvc := new(MockAIService) startedSvc := new(MockAIService) mockState := new(MockAIStateProvider) var capturedCfg chat.Config newChatService = func(cfg chat.Config) AIService { capturedCfg = cfg return startedSvc } h := newTestAIHandler(nil, mockPersist, nil) h.legacyService = stoppedSvc h.SetStateProvider(mockState) aiCfg := &config.AIConfig{Enabled: true, Model: "test"} mockPersist.On("LoadAIConfig").Return(aiCfg, nil).Once() stoppedSvc.On("IsRunning").Return(false).Once() startedSvc.On("Start", mock.Anything).Return(nil).Once() err := h.Restart(context.Background()) assert.NoError(t, err) assert.Equal(t, startedSvc, h.legacyService) assert.Same(t, mockState, capturedCfg.StateProvider) assert.Equal(t, "data", capturedCfg.DataDir) } func TestStart_DefaultsEmptyPersistenceDataDir(t *testing.T) { oldNewService := newChatService defer func() { newChatService = oldNewService }() mockSvc := new(MockAIService) mockState := new(MockAIStateProvider) mockPersist := &MockAIPersistence{} var capturedCfg chat.Config newChatService = func(cfg chat.Config) AIService { capturedCfg = cfg return mockSvc } h := newTestAIHandler(&config.Config{}, mockPersist, nil) mockPersist.On("LoadAIConfig").Return(&config.AIConfig{Enabled: true, Model: "test"}, nil).Once() mockSvc.On("Start", mock.Anything).Return(nil).Once() err := h.Start(context.Background(), mockState) assert.NoError(t, err) assert.Equal(t, "data", capturedCfg.DataDir) assert.Same(t, mockState, capturedCfg.StateProvider) } func TestGetService(t *testing.T) { mockSvc := new(MockAIService) h := newTestAIHandler(nil, nil, nil) h.legacyService = mockSvc assert.Equal(t, mockSvc, h.GetService(context.Background())) } func TestGetAIConfig(t *testing.T) { mockPersist := new(MockAIPersistence) h := newTestAIHandler(nil, mockPersist, nil) aiCfg := &config.AIConfig{Model: "test"} mockPersist.On("LoadAIConfig").Return(aiCfg, nil) result := h.GetAIConfig(context.Background()) assert.Equal(t, aiCfg, result) } func TestLoadAIConfig_Error(t *testing.T) { mockPersist := new(MockAIPersistence) h := newTestAIHandler(nil, mockPersist, nil) mockPersist.On("LoadAIConfig").Return((*config.AIConfig)(nil), assert.AnError) result := h.loadAIConfig(context.Background()) assert.Nil(t, result) } func TestHandleStatus(t *testing.T) { cfg := &config.Config{ APIToken: "test-token", } h := newTestAIHandler(cfg, nil, nil) mockSvc := new(MockAIService) h.legacyService = mockSvc mockSvc.On("IsRunning").Return(true) req := httptest.NewRequest("GET", "/api/ai/status", nil) w := httptest.NewRecorder() h.HandleStatus(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]interface{} err := json.NewDecoder(w.Body).Decode(&resp) assert.NoError(t, err) assert.True(t, resp["running"].(bool)) assert.Equal(t, "direct", resp["engine"]) } func TestHandleSessions(t *testing.T) { cfg := &config.Config{} h := newTestAIHandler(cfg, nil, nil) mockSvc := new(MockAIService) h.legacyService = mockSvc mockSvc.On("IsRunning").Return(true) sessions := []chat.Session{{ID: "s1"}, {ID: "s2"}} mockSvc.On("ListSessions", mock.Anything).Return(sessions, nil) req := httptest.NewRequest("GET", "/api/ai/sessions", nil) w := httptest.NewRecorder() h.HandleSessions(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestHandleCreateSession(t *testing.T) { cfg := &config.Config{} h := newTestAIHandler(cfg, nil, nil) mockSvc := new(MockAIService) h.legacyService = mockSvc mockSvc.On("IsRunning").Return(true) session := &chat.Session{ID: "new-session"} mockSvc.On("CreateSession", mock.Anything).Return(session, nil) req := httptest.NewRequest("POST", "/api/ai/sessions", nil) w := httptest.NewRecorder() h.HandleCreateSession(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestHandleDeleteSession(t *testing.T) { cfg := &config.Config{} h := newTestAIHandler(cfg, nil, nil) mockSvc := new(MockAIService) h.legacyService = mockSvc mockSvc.On("IsRunning").Return(true) mockSvc.On("DeleteSession", mock.Anything, "s1").Return(nil) req := httptest.NewRequest("DELETE", "/api/ai/sessions/s1", nil) w := httptest.NewRecorder() h.HandleDeleteSession(w, req, "s1") assert.Equal(t, http.StatusNoContent, w.Code) } func TestHandleMessages(t *testing.T) { cfg := &config.Config{} h := newTestAIHandler(cfg, nil, nil) mockSvc := new(MockAIService) h.legacyService = mockSvc mockSvc.On("IsRunning").Return(true) messages := []chat.Message{{Role: "user", Content: "hello"}} mockSvc.On("GetMessages", mock.Anything, "s1").Return(messages, nil) req := httptest.NewRequest("GET", "/api/ai/sessions/s1/messages", nil) w := httptest.NewRecorder() h.HandleMessages(w, req, "s1") assert.Equal(t, http.StatusOK, w.Code) } func TestHandleChat_NotRunning(t *testing.T) { cfg := &config.Config{} h := newTestAIHandler(cfg, nil, nil) mockSvc := new(MockAIService) h.legacyService = mockSvc mockSvc.On("IsRunning").Return(false) req := httptest.NewRequest("POST", "/api/ai/chat", nil) w := httptest.NewRecorder() h.HandleChat(w, req) assert.Equal(t, http.StatusServiceUnavailable, w.Code) } func TestHandleChat_InvalidJSON(t *testing.T) { cfg := &config.Config{} h := newTestAIHandler(cfg, nil, nil) mockSvc := new(MockAIService) h.legacyService = mockSvc mockSvc.On("IsRunning").Return(true) req := httptest.NewRequest("POST", "/api/ai/chat", strings.NewReader("invalid")) w := httptest.NewRecorder() h.HandleChat(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestHandleChat_Success(t *testing.T) { cfg := &config.Config{} h := newTestAIHandler(cfg, nil, nil) mockSvc := new(MockAIService) h.legacyService = mockSvc mockSvc.On("IsRunning").Return(true) // Mock ExecuteStream to just return nil mockSvc.On("ExecuteStream", mock.Anything, mock.Anything, mock.Anything).Return(nil).Run(func(args mock.Arguments) { callback := args.Get(2).(chat.StreamCallback) data, _ := json.Marshal("hello") callback(chat.StreamEvent{Type: "content", Data: data}) }) body := `{"prompt": "hi"}` req := httptest.NewRequest("POST", "/api/ai/chat", strings.NewReader(body)) w := httptest.NewRecorder() h.HandleChat(w, req) assert.Equal(t, http.StatusOK, w.Code) assert.Contains(t, w.Header().Get("Content-Type"), "text/event-stream") } func TestHandleAnswerQuestion(t *testing.T) { cfg := &config.Config{} h := newTestAIHandler(cfg, nil, nil) mockSvc := new(MockAIService) h.legacyService = mockSvc mockSvc.On("IsRunning").Return(true) mockSvc.On("AnswerQuestion", mock.Anything, "q1", mock.Anything).Return(nil) body := `{"answers": [{"id": "a1", "value": "v1"}]}` req := httptest.NewRequest("POST", "/api/ai/question/q1/answer", strings.NewReader(body)) w := httptest.NewRecorder() h.HandleAnswerQuestion(w, req, "q1") assert.Equal(t, http.StatusOK, w.Code) } func TestHandleSessions_NotRunning(t *testing.T) { h := newTestAIHandler(&config.Config{}, nil, nil) mockSvc := new(MockAIService) h.legacyService = mockSvc mockSvc.On("IsRunning").Return(false) req := httptest.NewRequest("GET", "/api/ai/sessions", nil) w := httptest.NewRecorder() h.HandleSessions(w, req) assert.Equal(t, http.StatusServiceUnavailable, w.Code) } func TestHandleSessions_Error(t *testing.T) { h := newTestAIHandler(&config.Config{}, nil, nil) mockSvc := new(MockAIService) h.legacyService = mockSvc mockSvc.On("IsRunning").Return(true) mockSvc.On("ListSessions", mock.Anything).Return(([]chat.Session)(nil), assert.AnError) req := httptest.NewRequest("GET", "/api/ai/sessions", nil) w := httptest.NewRecorder() h.HandleSessions(w, req) assert.Equal(t, http.StatusInternalServerError, w.Code) } func TestHandleCreateSession_Error(t *testing.T) { h := newTestAIHandler(&config.Config{}, nil, nil) mockSvc := new(MockAIService) h.legacyService = mockSvc mockSvc.On("IsRunning").Return(true) mockSvc.On("CreateSession", mock.Anything).Return((*chat.Session)(nil), assert.AnError) req := httptest.NewRequest("POST", "/api/ai/sessions", nil) w := httptest.NewRecorder() h.HandleCreateSession(w, req) assert.Equal(t, http.StatusInternalServerError, w.Code) } func TestHandleDeleteSession_Error(t *testing.T) { h := newTestAIHandler(&config.Config{}, nil, nil) mockSvc := new(MockAIService) h.legacyService = mockSvc mockSvc.On("IsRunning").Return(true) mockSvc.On("DeleteSession", mock.Anything, "s1").Return(assert.AnError) req := httptest.NewRequest("DELETE", "/api/ai/sessions/s1", nil) w := httptest.NewRecorder() h.HandleDeleteSession(w, req, "s1") assert.Equal(t, http.StatusInternalServerError, w.Code) } func TestHandleMessages_Error(t *testing.T) { h := newTestAIHandler(&config.Config{}, nil, nil) mockSvc := new(MockAIService) h.legacyService = mockSvc mockSvc.On("IsRunning").Return(true) mockSvc.On("GetMessages", mock.Anything, "s1").Return(([]chat.Message)(nil), assert.AnError) req := httptest.NewRequest("GET", "/api/ai/sessions/s1/messages", nil) w := httptest.NewRecorder() h.HandleMessages(w, req, "s1") assert.Equal(t, http.StatusInternalServerError, w.Code) } func TestHandleAbort_Error(t *testing.T) { h := newTestAIHandler(&config.Config{}, nil, nil) mockSvc := new(MockAIService) h.legacyService = mockSvc mockSvc.On("IsRunning").Return(true) mockSvc.On("AbortSession", mock.Anything, "s1").Return(assert.AnError) req := httptest.NewRequest("POST", "/api/ai/sessions/s1/abort", nil) w := httptest.NewRecorder() h.HandleAbort(w, req, "s1") assert.Equal(t, http.StatusInternalServerError, w.Code) } func TestHandleSummarize_Error(t *testing.T) { h := newTestAIHandler(&config.Config{}, nil, nil) mockSvc := new(MockAIService) h.legacyService = mockSvc mockSvc.On("IsRunning").Return(true) mockSvc.On("SummarizeSession", mock.Anything, "s1").Return((map[string]interface{})(nil), assert.AnError) req := httptest.NewRequest("POST", "/api/ai/sessions/s1/summarize", nil) w := httptest.NewRecorder() h.HandleSummarize(w, req, "s1") assert.Equal(t, http.StatusInternalServerError, w.Code) } func TestHandleAnswerQuestion_InvalidJSON(t *testing.T) { h := newTestAIHandler(&config.Config{}, nil, nil) mockSvc := new(MockAIService) h.legacyService = mockSvc mockSvc.On("IsRunning").Return(true) req := httptest.NewRequest("POST", "/api/ai/question/q1/answer", strings.NewReader("invalid")) w := httptest.NewRecorder() h.HandleAnswerQuestion(w, req, "q1") assert.Equal(t, http.StatusBadRequest, w.Code) } func TestHandleAnswerQuestion_Error(t *testing.T) { h := newTestAIHandler(&config.Config{}, nil, nil) mockSvc := new(MockAIService) h.legacyService = mockSvc mockSvc.On("IsRunning").Return(true) mockSvc.On("AnswerQuestion", mock.Anything, "q1", mock.Anything).Return(assert.AnError) body := `{"answers": []}` req := httptest.NewRequest("POST", "/api/ai/question/q1/answer", strings.NewReader(body)) w := httptest.NewRecorder() h.HandleAnswerQuestion(w, req, "q1") assert.Equal(t, http.StatusInternalServerError, w.Code) } func TestHandleChat_Options(t *testing.T) { h := newTestAIHandler(nil, nil, nil) req := httptest.NewRequest("OPTIONS", "/api/ai/chat", nil) req.Header.Set("Origin", "http://example.com") w := httptest.NewRecorder() h.HandleChat(w, req) assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, "http://example.com", w.Header().Get("Access-Control-Allow-Origin")) } func TestHandleChat_MethodNotAllowed(t *testing.T) { h := newTestAIHandler(nil, nil, nil) req := httptest.NewRequest("GET", "/api/ai/chat", nil) w := httptest.NewRecorder() h.HandleChat(w, req) assert.Equal(t, http.StatusMethodNotAllowed, w.Code) } func TestHandleChat_Error(t *testing.T) { cfg := &config.Config{} h := newTestAIHandler(cfg, nil, nil) mockSvc := new(MockAIService) h.legacyService = mockSvc mockSvc.On("IsRunning").Return(true) mockSvc.On("ExecuteStream", mock.Anything, mock.Anything, mock.Anything).Return(assert.AnError) body := `{"prompt": "hi"}` req := httptest.NewRequest("POST", "/api/ai/chat", strings.NewReader(body)) w := httptest.NewRecorder() h.HandleChat(w, req) // ExecuteStream error happens after headers are sent, so w.Code might be 200 // but the error is returned. assert.Equal(t, http.StatusOK, w.Code) } func TestHandleDiff_Error(t *testing.T) { h := newTestAIHandler(&config.Config{}, nil, nil) mockSvc := new(MockAIService) h.legacyService = mockSvc mockSvc.On("IsRunning").Return(true) mockSvc.On("GetSessionDiff", mock.Anything, "s1").Return((map[string]interface{})(nil), assert.AnError) req := httptest.NewRequest("GET", "/api/ai/sessions/s1/diff", nil) w := httptest.NewRecorder() h.HandleDiff(w, req, "s1") assert.Equal(t, http.StatusInternalServerError, w.Code) } func TestHandleFork_Error(t *testing.T) { h := newTestAIHandler(&config.Config{}, nil, nil) mockSvc := new(MockAIService) h.legacyService = mockSvc mockSvc.On("IsRunning").Return(true) mockSvc.On("ForkSession", mock.Anything, "s1").Return((*chat.Session)(nil), assert.AnError) req := httptest.NewRequest("POST", "/api/ai/sessions/s1/fork", nil) w := httptest.NewRecorder() h.HandleFork(w, req, "s1") assert.Equal(t, http.StatusInternalServerError, w.Code) } func TestHandleRevert_Error(t *testing.T) { h := newTestAIHandler(&config.Config{}, nil, nil) mockSvc := new(MockAIService) h.legacyService = mockSvc mockSvc.On("IsRunning").Return(true) mockSvc.On("RevertSession", mock.Anything, "s1").Return((map[string]interface{})(nil), assert.AnError) req := httptest.NewRequest("POST", "/api/ai/sessions/s1/revert", nil) w := httptest.NewRecorder() h.HandleRevert(w, req, "s1") assert.Equal(t, http.StatusInternalServerError, w.Code) } func TestHandleUnrevert_Error(t *testing.T) { h := newTestAIHandler(&config.Config{}, nil, nil) mockSvc := new(MockAIService) h.legacyService = mockSvc mockSvc.On("IsRunning").Return(true) mockSvc.On("UnrevertSession", mock.Anything, "s1").Return((map[string]interface{})(nil), assert.AnError) req := httptest.NewRequest("POST", "/api/ai/sessions/s1/unrevert", nil) w := httptest.NewRecorder() h.HandleUnrevert(w, req, "s1") assert.Equal(t, http.StatusInternalServerError, w.Code) } func TestHandleStatus_NotRunning(t *testing.T) { h := newTestAIHandler(&config.Config{}, nil, nil) mockSvc := new(MockAIService) h.legacyService = mockSvc mockSvc.On("IsRunning").Return(false) req := httptest.NewRequest("GET", "/api/ai/status", nil) w := httptest.NewRecorder() h.HandleStatus(w, req) assert.Equal(t, http.StatusOK, w.Code) // HandleStatus returns 200 even if not running var resp map[string]interface{} json.NewDecoder(w.Body).Decode(&resp) assert.False(t, resp["running"].(bool)) } func TestMockUnimplemented(t *testing.T) { mockSvc := new(MockAIService) mockSvc.On("SetFindingsManager", mock.Anything).Return() mockSvc.On("SetMetadataUpdater", mock.Anything).Return() mockSvc.On("UpdateControlSettings", mock.Anything).Return() h := newTestAIHandler(nil, nil, nil) h.legacyService = mockSvc h.SetFindingsManager(nil) h.SetMetadataUpdater(nil) h.UpdateControlSettings(nil) mockSvc.AssertExpectations(t) } func TestProviders(t *testing.T) { h := newTestAIHandler(nil, nil, nil) mockSvc := new(MockAIService) h.legacyService = mockSvc mockSvc.On("SetAlertProvider", mock.Anything).Return() mockSvc.On("SetFindingsProvider", mock.Anything).Return() mockSvc.On("SetBaselineProvider", mock.Anything).Return() mockSvc.On("SetPatternProvider", mock.Anything).Return() mockSvc.On("SetMetricsHistory", mock.Anything).Return() mockSvc.On("SetAgentProfileManager", mock.Anything).Return() mockSvc.On("SetStorageProvider", mock.Anything).Return() mockSvc.On("SetBackupProvider", mock.Anything).Return() mockSvc.On("SetDiskHealthProvider", mock.Anything).Return() mockSvc.On("SetUpdatesProvider", mock.Anything).Return() h.SetAlertProvider(nil) h.SetFindingsProvider(nil) h.SetBaselineProvider(nil) h.SetPatternProvider(nil) h.SetMetricsHistory(nil) h.SetAgentProfileManager(nil) h.SetStorageProvider(nil) h.SetBackupProvider(nil) h.SetDiskHealthProvider(nil) h.SetUpdatesProvider(nil) mockSvc.AssertExpectations(t) } func TestHandleAbort_Success(t *testing.T) { h := newTestAIHandler(&config.Config{}, nil, nil) mockSvc := new(MockAIService) h.legacyService = mockSvc mockSvc.On("IsRunning").Return(true) mockSvc.On("AbortSession", mock.Anything, "s1").Return(nil) req := httptest.NewRequest("POST", "/api/ai/sessions/s1/abort", nil) w := httptest.NewRecorder() h.HandleAbort(w, req, "s1") assert.Equal(t, http.StatusOK, w.Code) } func TestHandleSummarize_Success(t *testing.T) { h := newTestAIHandler(&config.Config{}, nil, nil) mockSvc := new(MockAIService) h.legacyService = mockSvc mockSvc.On("IsRunning").Return(true) mockSvc.On("SummarizeSession", mock.Anything, "s1").Return(map[string]interface{}{"summary": "ok"}, nil) req := httptest.NewRequest("POST", "/api/ai/sessions/s1/summarize", nil) w := httptest.NewRecorder() h.HandleSummarize(w, req, "s1") assert.Equal(t, http.StatusOK, w.Code) } func TestHandleDiff_Success(t *testing.T) { h := newTestAIHandler(&config.Config{}, nil, nil) mockSvc := new(MockAIService) h.legacyService = mockSvc mockSvc.On("IsRunning").Return(true) mockSvc.On("GetSessionDiff", mock.Anything, "s1").Return(map[string]interface{}{"diff": "test"}, nil) req := httptest.NewRequest("GET", "/api/ai/sessions/s1/diff", nil) w := httptest.NewRecorder() h.HandleDiff(w, req, "s1") assert.Equal(t, http.StatusOK, w.Code) } func TestHandleFork_Success(t *testing.T) { h := newTestAIHandler(&config.Config{}, nil, nil) mockSvc := new(MockAIService) h.legacyService = mockSvc mockSvc.On("IsRunning").Return(true) mockSvc.On("ForkSession", mock.Anything, "s1").Return(&chat.Session{ID: "s2"}, nil) req := httptest.NewRequest("POST", "/api/ai/sessions/s1/fork", nil) w := httptest.NewRecorder() h.HandleFork(w, req, "s1") assert.Equal(t, http.StatusOK, w.Code) } func TestHandleRevert_Success(t *testing.T) { h := newTestAIHandler(&config.Config{}, nil, nil) mockSvc := new(MockAIService) h.legacyService = mockSvc mockSvc.On("IsRunning").Return(true) mockSvc.On("RevertSession", mock.Anything, "s1").Return(map[string]interface{}{"reverted": true}, nil) req := httptest.NewRequest("POST", "/api/ai/sessions/s1/revert", nil) w := httptest.NewRecorder() h.HandleRevert(w, req, "s1") assert.Equal(t, http.StatusOK, w.Code) } func TestHandleUnrevert_Success(t *testing.T) { h := newTestAIHandler(&config.Config{}, nil, nil) mockSvc := new(MockAIService) h.legacyService = mockSvc mockSvc.On("IsRunning").Return(true) mockSvc.On("UnrevertSession", mock.Anything, "s1").Return(map[string]interface{}{"unreverted": true}, nil) req := httptest.NewRequest("POST", "/api/ai/sessions/s1/unrevert", nil) w := httptest.NewRecorder() h.HandleUnrevert(w, req, "s1") assert.Equal(t, http.StatusOK, w.Code) } func TestHandleStatus_NoService(t *testing.T) { // HandleStatus with no service initialized should still return 200 with running=false cfg := &config.Config{} h := newTestAIHandler(cfg, nil, nil) req := httptest.NewRequest("GET", "/api/ai/status", nil) w := httptest.NewRecorder() h.HandleStatus(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]interface{} json.NewDecoder(w.Body).Decode(&resp) assert.False(t, resp["running"].(bool)) } func TestGetService_MultiTenantInitAndCache(t *testing.T) { oldNewService := newChatService defer func() { newChatService = oldNewService }() tempDir := t.TempDir() mtp := config.NewMultiTenantPersistence(tempDir) h := NewAIHandler(mtp, nil, nil) mockSvc := new(MockAIService) mockSvc.On("Start", mock.Anything).Return(nil).Once() var gotCfg chat.Config newChatService = func(cfg chat.Config) AIService { gotCfg = cfg return mockSvc } ctx := context.WithValue(context.Background(), OrgIDContextKey, "acme") svc := h.GetService(ctx) assert.Same(t, mockSvc, svc) expectedDir := filepath.Join(tempDir, "orgs", "acme") assert.Equal(t, expectedDir, gotCfg.DataDir) assert.NotNil(t, gotCfg.AIConfig) // Second call should return cached service without re-starting svc = h.GetService(ctx) assert.Same(t, mockSvc, svc) mockSvc.AssertExpectations(t) } func TestRemoveTenantService(t *testing.T) { h := NewAIHandler(nil, nil, nil) mockSvc := new(MockAIService) mockSvc.On("Stop", mock.Anything).Return(assert.AnError).Once() h.services["acme"] = mockSvc err := h.RemoveTenantService(context.Background(), "acme") assert.NoError(t, err) _, exists := h.services["acme"] assert.False(t, exists) mockSvc.AssertExpectations(t) } func TestRemoveTenantService_DefaultNoop(t *testing.T) { h := NewAIHandler(nil, nil, nil) mockSvc := new(MockAIService) h.services["default"] = mockSvc err := h.RemoveTenantService(context.Background(), "default") assert.NoError(t, err) _, exists := h.services["default"] assert.True(t, exists) } func TestGetConfig_DefaultFallback(t *testing.T) { cfg := &config.Config{APIToken: "token"} h := newTestAIHandler(cfg, nil, nil) ctx := context.WithValue(context.Background(), OrgIDContextKey, "acme") result := h.getConfig(ctx) assert.Same(t, cfg, result) } func TestGetDataDirDefault(t *testing.T) { h := newTestAIHandler(nil, nil, nil) assert.Equal(t, "data", h.getDataDir(nil, "")) assert.Equal(t, "custom", h.getDataDir(nil, "custom")) } func TestSetMultiTenantPointers(t *testing.T) { h := NewAIHandler(nil, nil, nil) mtp := config.NewMultiTenantPersistence(t.TempDir()) mtm := &monitoring.MultiTenantMonitor{} h.SetMultiTenantPersistence(mtp) h.SetMultiTenantMonitor(mtm) assert.Same(t, mtp, h.mtPersistence) assert.Same(t, mtm, h.mtMonitor) }