Files
gochat/backend/internal/handler/api/v1/copilot_suggestion_handler_test.go
T
rogee aeddedf2a3 Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories:
- backend/: Go module root (cmd, internal, pkg, configs, migrations,
  docs/swagger, scripts, tests, go.mod, Makefile, .air.toml)
- deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd
- docs/: project documentation + reports/ (moved from repo root)
- AGENTS.md: new AI coding-agent guide at repo root

Update all references to the new layout:
- Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root)
- docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile,
  env_file ../../.env, volume mounts ../../backend:/app
- deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile
- CI: working-directory: backend for go commands, file deploy/docker/Dockerfile,
  coverage path backend/coverage.out, health_check backend/scripts/
- backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../
- README: architecture tree, quickstart, config paths updated

Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh,
gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to
preserve history. Build, vet, SQLite tests, and docker compose config verified.
2026-07-07 14:44:12 +08:00

250 lines
8.3 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{}
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))
}