Files
ai-agent/internal/services/conversation_vision_asset_test.go
T
t 18c9354095 refactor: 将客服后端重构为宿主可嵌入模块
- 注入数据库、运行时配置、统一响应、文件存储和平台 AI 能力,补充业务读写工具与客户快捷操作契约。

- 移除模块内重复的组织、客户、工单、标签、技能、旧工作流、MCP 和迁移实现,将身份权限与业务主体交由宿主管理。

- 使用 libSQL 重构向量存储,并完善图片消息、访客身份、排队调度、企业微信和支持聊天页面。

- 统一 HTTP、DTO 与 WebSocket 的 snake_case 协议,补齐模块初始化、业务动作和公共载荷等回归测试。
2026-08-28 22:23:13 +08:00

114 lines
4.5 KiB
Go

package services
import (
"encoding/base64"
"fmt"
"strings"
"testing"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/config"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/services/storage"
"github.com/glebarez/sqlite"
"github.com/mlogclub/simple/sqls"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
func setupConversationVisionAssetTest(t *testing.T) *gorm.DB {
t.Helper()
database, err := gorm.Open(sqlite.Open("file:"+strings.ReplaceAll(t.Name(), "/", "_")+"?mode=memory&cache=shared"), &gorm.Config{
NamingStrategy: schema.NamingStrategy{TablePrefix: "t_", SingularTable: true},
})
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
if err := database.AutoMigrate(&models.Asset{}); err != nil {
t.Fatalf("migrate asset: %v", err)
}
sqls.SetDB(database)
config.SetCurrent(&config.Config{Storage: config.StorageConfig{
Default: enums.AssetProviderLocal, MaxUploadSizeMB: 20,
Local: config.LocalStorageConfig{Root: t.TempDir(), BaseURL: "/storage"},
}})
storage.SetHostStorage(nil)
t.Cleanup(func() { storage.SetHostStorage(nil) })
return database
}
func TestConversationVisionImagesAreInlineAndConversationScoped(t *testing.T) {
setupConversationVisionAssetTest(t)
png := append([]byte("\x89PNG\r\n\x1a\n"), make([]byte, 520)...)
asset, err := AssetService.UploadConversationBytes(png, "images", "device.png", 11, nil)
if err != nil {
t.Fatalf("upload conversation image: %v", err)
}
if asset.ConversationID != 11 || asset.MimeType != "image/png" {
t.Fatalf("unexpected stored asset: %#v", asset)
}
payload := fmt.Sprintf(`{"asset_id":%q,"url":"https://attacker.invalid/ssrf.png","provider":"oss","storage_key":"other/customer.png"}`, asset.AssetID)
message := models.Message{
ID: 9, ConversationID: 11, SenderType: enums.IMSenderTypeCustomer,
MessageType: enums.IMMessageTypeImage, Payload: payload,
SendStatus: enums.IMMessageStatusSent,
}
images := AssetService.LoadConversationVisionImages(11, []models.Message{message}, 3)
if len(images) != 1 {
t.Fatalf("images = %#v, want one trusted image", images)
}
decoded, err := base64.StdEncoding.DecodeString(images[0].Base64Data)
if err != nil || string(decoded) != string(png) {
t.Fatalf("inline image mismatch: len=%d err=%v", len(decoded), err)
}
if images[0].MIMEType != "image/png" {
t.Fatalf("mime type = %q", images[0].MIMEType)
}
if got := AssetService.LoadConversationVisionImages(12, []models.Message{{
ID: 10, ConversationID: 12, SenderType: enums.IMSenderTypeCustomer,
MessageType: enums.IMMessageTypeImage, Payload: payload, SendStatus: enums.IMMessageStatusSent,
}}, 3); len(got) != 0 {
t.Fatalf("cross-conversation asset leaked into model input: %#v", got)
}
}
func TestConversationVisionImagesLoadsCompositeMessageInPayloadOrder(t *testing.T) {
setupConversationVisionAssetTest(t)
firstData := append([]byte("\x89PNG\r\n\x1a\n"), make([]byte, 520)...)
secondData := append([]byte("\x89PNG\r\n\x1a\n"), make([]byte, 640)...)
first, err := AssetService.UploadConversationBytes(firstData, "images", "front.png", 21, nil)
if err != nil {
t.Fatalf("upload first image: %v", err)
}
second, err := AssetService.UploadConversationBytes(secondData, "images", "label.png", 21, nil)
if err != nil {
t.Fatalf("upload second image: %v", err)
}
payload, err := buildIMMessageAssetBatchPayload([]*models.Asset{first, second})
if err != nil {
t.Fatalf("build batch payload: %v", err)
}
images := AssetService.LoadConversationVisionImages(21, []models.Message{{
ID: 22, ConversationID: 21, SenderType: enums.IMSenderTypeCustomer,
MessageType: enums.IMMessageTypeImage, Payload: payload, SendStatus: enums.IMMessageStatusSent,
}}, 6)
if len(images) != 2 || images[0].AssetID != first.AssetID || images[1].AssetID != second.AssetID {
t.Fatalf("composite image order mismatch: %#v", images)
}
}
func TestValidateConversationAssetRejectsCrossConversationAndFakeImage(t *testing.T) {
asset := &models.Asset{ConversationID: 7, Status: enums.AssetStatusSuccess, MimeType: "image/png"}
if err := validateConversationAsset(asset, 7, enums.IMMessageTypeImage); err != nil {
t.Fatalf("valid scoped image rejected: %v", err)
}
if err := validateConversationAsset(asset, 8, enums.IMMessageTypeImage); err == nil {
t.Fatal("cross-conversation asset must be rejected")
}
asset.MimeType = "text/html"
if err := validateConversationAsset(asset, 7, enums.IMMessageTypeImage); err == nil {
t.Fatal("non-image asset must not be sent as an image")
}
}