Files
gochat/backend/internal/handler/api/v1/copilot_suggestion_handler_test.go
T
Rogeeandrogee 2b182f9956 H-300: wire Captain Skills into Web runtime (#48)
* H-300: wire Captain Skills into Web runtime

* H-300: enforce effective model and conservative skill budget

* H-300: fix CI gosec step

* ci: extend golangci-lint timeout

* fix lint findings across backend

* fix(push): resolve delivery protocol blockers

* test(repository): close SQLite test databases

* test(repository): reuse SQLite schema per package

* H-307: restore backend Go cache in CI

* H-307: prefetch modules before cold lint

* H-307: resolve govulncheck security gate

* H-307: build lint with patched Go toolchain

* H-307: clear remaining security scan findings

---------

Co-authored-by: Rogee <rogee@ipao.vip>
2026-08-19 07:08:14 +08:00

259 lines
8.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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{}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
panic(err)
}
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{}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
panic(err)
}
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{}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
panic(err)
}
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{}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
panic(err)
}
data := resp["data"].([]interface{})
assert.Equal(s.T(), 0, len(data))
}
func TestCopilotSuggestionHandlerSuite(t *testing.T) {
suite.Run(t, new(CopilotSuggestionHandlerTestSuite))
}