* 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>
259 lines
8.4 KiB
Go
259 lines
8.4 KiB
Go
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))
|
||
}
|