package v1 import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "strconv" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" "github.com/gochat/gochat/internal/llm" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" "github.com/gochat/gochat/internal/service" ) // --- Copilot Suggestion Message Handler Test Suite --- // 测试 CopilotSuggestionMessage 的 GET/POST 接口 // 使用真实 SQLite 内存数据库 + 真实 repo + 真实 service // mockSuggestionLLMProvider 用于测试中模拟 LLM 调用(CopilotSuggestion 不需要 LLM) type mockSuggestionLLMProvider struct{} func (m *mockSuggestionLLMProvider) ChatCompletion(ctx context.Context, req llm.ChatRequest) (*llm.ChatResponse, error) { return &llm.ChatResponse{ Choices: []llm.ChatChoice{{Message: llm.ChatMessage{Role: "assistant", Content: "mock response"}}}, }, nil } func (m *mockSuggestionLLMProvider) CreateEmbedding(ctx context.Context, req llm.EmbeddingRequest) (*llm.EmbeddingResponse, error) { return &llm.EmbeddingResponse{}, nil } func (m *mockSuggestionLLMProvider) ChatCompletionStream(ctx context.Context, req llm.ChatRequest, onChunk func(llm.StreamChunk) error) error { return nil } type CopilotSuggestionHandlerTestSuite struct { suite.Suite router *gin.Engine handler *CopilotHandler db *gorm.DB account *model.Account } func (s *CopilotSuggestionHandlerTestSuite) SetupSuite() { gin.SetMode(gin.TestMode) db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) s.Require().NoError(err) s.db = db // 自动迁移所需模型 err = db.AutoMigrate( &model.Account{}, &model.CopilotThread{}, &model.CopilotMessage{}, &model.CopilotSuggestionMessage{}, ) s.Require().NoError(err) // 创建测试账户 account := &model.Account{Name: "CopilotSuggestionTestOrg", Locale: "en", Active: true} s.Require().NoError(db.Create(account).Error) s.account = account // 创建 repo + service + handler threadRepo := repository.NewCopilotThreadRepo(db) messageRepo := repository.NewCopilotMessageRepo(db) suggestionRepo := repository.NewCopilotSuggestionRepo(db) mockProvider := &mockSuggestionLLMProvider{} svc := service.NewCopilotService(threadRepo, messageRepo, suggestionRepo, mockProvider) s.handler = NewCopilotHandler(svc) // 设置路由 s.router = gin.New() accountsGroup := s.router.Group("/api/v1/accounts/:id") { captainGroup := accountsGroup.Group("/captain") { copilotMessages := captainGroup.Group("/copilot_messages") { copilotMessages.GET("/", s.handler.ListSuggestionMessages) copilotMessages.POST("/", s.handler.CreateSuggestionMessage) } } } } func (s *CopilotSuggestionHandlerTestSuite) TearDownSuite() { sqlDB, err := s.db.DB() s.Require().NoError(err) sqlDB.Close() } // ========== 创建建议消息测试 ========== func (s *CopilotSuggestionHandlerTestSuite) TestCreateSuggestionMessage_成功创建回复建议() { body := map[string]interface{}{ "conversation_id": float64(1), "content": "这是一个回复建议", "suggestion_type": "reply", } jsonBody, _ := json.Marshal(body) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/"+strconv.FormatUint(uint64(s.account.ID), 10)+"/captain/copilot_messages/", bytes.NewReader(jsonBody)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusCreated, w.Code) var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) assert.True(s.T(), resp["success"].(bool)) data := resp["data"].(map[string]interface{}) assert.Equal(s.T(), "这是一个回复建议", data["content"]) assert.Equal(s.T(), "reply", data["suggestion_type"]) assert.Equal(s.T(), "pending", data["status"]) } func (s *CopilotSuggestionHandlerTestSuite) TestCreateSuggestionMessage_默认类型为suggestion() { body := map[string]interface{}{ "conversation_id": float64(2), "content": "这是一个默认建议", } jsonBody, _ := json.Marshal(body) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/"+strconv.FormatUint(uint64(s.account.ID), 10)+"/captain/copilot_messages/", bytes.NewReader(jsonBody)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusCreated, w.Code) var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) data := resp["data"].(map[string]interface{}) assert.Equal(s.T(), "suggestion", data["suggestion_type"]) } func (s *CopilotSuggestionHandlerTestSuite) TestCreateSuggestionMessage_缺少content返回错误() { body := map[string]interface{}{ "conversation_id": float64(1), } jsonBody, _ := json.Marshal(body) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/"+strconv.FormatUint(uint64(s.account.ID), 10)+"/captain/copilot_messages/", bytes.NewReader(jsonBody)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) // 空content应该返回400(验证失败) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *CopilotSuggestionHandlerTestSuite) TestCreateSuggestionMessage_无效accountID() { body := map[string]interface{}{ "conversation_id": float64(1), "content": "测试内容", } jsonBody, _ := json.Marshal(body) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/invalid/captain/copilot_messages/", bytes.NewReader(jsonBody)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } // ========== 列出建议消息测试 ========== func (s *CopilotSuggestionHandlerTestSuite) TestListSuggestionMessages_成功列出() { // 先创建几条建议消息 for i := 0; i < 3; i++ { body := map[string]interface{}{ "conversation_id": float64(10), "content": "建议消息 " + strconv.Itoa(i+1), "suggestion_type": "suggestion", } jsonBody, _ := json.Marshal(body) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/"+strconv.FormatUint(uint64(s.account.ID), 10)+"/captain/copilot_messages/", bytes.NewReader(jsonBody)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusCreated, w.Code) } // 列出该会话的建议消息 w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/"+strconv.FormatUint(uint64(s.account.ID), 10)+"/captain/copilot_messages/?conversation_id=10", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) assert.True(s.T(), resp["success"].(bool)) data := resp["data"].([]interface{}) assert.Equal(s.T(), 3, len(data)) meta := resp["meta"].(map[string]interface{}) assert.Equal(s.T(), float64(3), meta["total_count"]) } func (s *CopilotSuggestionHandlerTestSuite) TestListSuggestionMessages_缺少conversation_id() { w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/"+strconv.FormatUint(uint64(s.account.ID), 10)+"/captain/copilot_messages/", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *CopilotSuggestionHandlerTestSuite) TestListSuggestionMessages_无效conversation_id() { w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/"+strconv.FormatUint(uint64(s.account.ID), 10)+"/captain/copilot_messages/?conversation_id=invalid", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *CopilotSuggestionHandlerTestSuite) TestListSuggestionMessages_空结果() { w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/"+strconv.FormatUint(uint64(s.account.ID), 10)+"/captain/copilot_messages/?conversation_id=999", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) data := resp["data"].([]interface{}) assert.Equal(s.T(), 0, len(data)) } func TestCopilotSuggestionHandlerSuite(t *testing.T) { suite.Run(t, new(CopilotSuggestionHandlerTestSuite)) }