Files

267 lines
10 KiB
Go

package tokenhub
import (
"fmt"
"strings"
"testing"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/mysqlmodel"
)
// clothesFixture 构造衣橱衣物测试数据(ID/UUID 由 id 派生,便于断言)。
func clothesFixture(id int64, category, style, color, impact string) mysqlmodel.Clothes {
return mysqlmodel.Clothes{
Model: mysqlmodel.Model{ID: id},
ClothesUUID: fmt.Sprintf("uuid-%d", id),
CategoryMain: category,
Style: style,
ColorMain: color,
VisualImpact: impact,
}
}
// itemFixture 构造推荐单品(Type 已按衣橱实际大类纠正,模拟 ValidateClothesIDs 之后的结果)。
func itemFixture(category string, id int64) RecommendLLMItem {
return RecommendLLMItem{
Type: category,
ClothesID: id,
ClothesUUID: fmt.Sprintf("uuid-%d", id),
Reason: "test",
}
}
// TestCompletenessIssues 校验完整性判定:内层必选、外套不可孤立、下装/鞋按衣橱存在与否强制、配饰不强制、连衣裙与下装冲突。
func TestCompletenessIssues(t *testing.T) {
wardrobe := []mysqlmodel.Clothes{
clothesFixture(1, "上装", "休闲", "黑", "3"),
clothesFixture(2, "下装", "休闲", "蓝", "2"),
clothesFixture(3, "外套", "休闲", "灰", "2"),
clothesFixture(4, "鞋", "休闲", "白", "1"),
clothesFixture(5, "连衣裙", "甜美", "粉", "3"),
clothesFixture(6, "配饰", "休闲", "金", "2"),
}
for _, tc := range []struct {
name string
picked []RecommendLLMItem
wantMissing []string
wantConflict []string
}{
{
name: "外套加下装缺上装与鞋",
picked: []RecommendLLMItem{itemFixture("外套", 3), itemFixture("下装", 2)},
wantMissing: []string{"上装", "鞋"},
},
{
name: "上装下装鞋齐全",
picked: []RecommendLLMItem{itemFixture("上装", 1), itemFixture("下装", 2), itemFixture("鞋", 4)},
},
{
name: "连衣裙加鞋完整",
picked: []RecommendLLMItem{itemFixture("连衣裙", 5), itemFixture("鞋", 4)},
},
{
name: "连衣裙与下装冲突",
picked: []RecommendLLMItem{itemFixture("连衣裙", 5), itemFixture("下装", 2), itemFixture("鞋", 4)},
wantConflict: []string{"下装"},
},
{
name: "缺下装",
picked: []RecommendLLMItem{itemFixture("上装", 1), itemFixture("鞋", 4)},
wantMissing: []string{"下装"},
},
{
name: "配饰不强制但缺鞋仍要补",
picked: []RecommendLLMItem{itemFixture("上装", 1), itemFixture("下装", 2)},
wantMissing: []string{"鞋"},
},
{
name: "只有配饰时按规则补齐主品类",
picked: []RecommendLLMItem{itemFixture("配饰", 6)},
wantMissing: []string{"上装", "下装", "鞋"},
},
} {
t.Run(tc.name, func(t *testing.T) {
r := &RecommendLLMResponse{Items: tc.picked}
missing, conflicts := r.CompletenessIssues(wardrobe)
if got, want := strings.Join(missing, ","), strings.Join(tc.wantMissing, ","); got != want {
t.Fatalf("missing = %q, want %q", got, want)
}
if got, want := strings.Join(conflicts, ","), strings.Join(tc.wantConflict, ","); got != want {
t.Fatalf("conflicts = %q, want %q", got, want)
}
})
}
}
// TestCompletenessIssuesWardrobeWithoutInner 衣橱没有内层时不强制内层(避免要求补齐衣橱中不存在的品类)。
func TestCompletenessIssuesWardrobeWithoutInner(t *testing.T) {
wardrobe := []mysqlmodel.Clothes{
clothesFixture(1, "外套", "休闲", "黑", "3"),
clothesFixture(2, "下装", "休闲", "黑", "2"),
}
r := &RecommendLLMResponse{Items: []RecommendLLMItem{itemFixture("外套", 1), itemFixture("下装", 2)}}
missing, conflicts := r.CompletenessIssues(wardrobe)
if len(missing) != 0 || len(conflicts) != 0 {
t.Fatalf("missing=%v conflicts=%v, want both empty", missing, conflicts)
}
// 衣橱只有连衣裙、结果只选了外套:应补连衣裙(外套不可孤立)。
dressOnly := []mysqlmodel.Clothes{clothesFixture(5, "连衣裙", "甜美", "粉", "3")}
coatOnly := &RecommendLLMResponse{Items: []RecommendLLMItem{itemFixture("外套", 9)}}
missing, _ = coatOnly.CompletenessIssues(dressOnly)
if strings.Join(missing, ",") != "连衣裙" {
t.Fatalf("missing = %v, want [连衣裙]", missing)
}
}
// TestNormalizeCategories 同一品类只保留首次出现的单品。
func TestNormalizeCategories(t *testing.T) {
r := &RecommendLLMResponse{Items: []RecommendLLMItem{
itemFixture("上装", 1), itemFixture("上装", 2), itemFixture("下装", 3),
}}
removed := r.NormalizeCategories()
if len(r.Items) != 2 || r.Items[0].ClothesID != 1 || r.Items[1].ClothesID != 3 {
t.Fatalf("items = %+v", r.Items)
}
if strings.Join(removed, ",") != "上装" {
t.Fatalf("removed = %v, want [上装]", removed)
}
}
// TestApplyAutoComplete 自动补齐:用户反馈的「只有外套+下装」场景补齐上装与鞋,且补齐单品按同风格/同主色优先级确定性挑选。
func TestApplyAutoComplete(t *testing.T) {
wardrobe := []mysqlmodel.Clothes{
clothesFixture(1, "上装", "休闲", "黑", "3"), // 与已选下装同风格同主色 -> 应被选中
clothesFixture(2, "上装", "商务", "白", "4"),
clothesFixture(3, "下装", "休闲", "黑", "2"),
clothesFixture(4, "鞋", "商务", "白", "1"),
}
r := &RecommendLLMResponse{Items: []RecommendLLMItem{itemFixture("外套", 9), itemFixture("下装", 3)}}
added, removed := r.ApplyAutoComplete(wardrobe)
if strings.Join(added, ",") != "上装,鞋" {
t.Fatalf("added = %v, want [上装 鞋]", added)
}
if len(removed) != 0 {
t.Fatalf("removed = %v, want empty", removed)
}
var top *RecommendLLMItem
for i := range r.Items {
if r.Items[i].Type == "上装" {
top = &r.Items[i]
}
}
if top == nil || top.ClothesID != 1 || top.ClothesUUID != "uuid-1" {
t.Fatalf("补齐的上装应取同风格同主色的 ID1,实际 = %+v", top)
}
if !strings.Contains(top.Reason, "系统按搭配完整性规则补齐") {
t.Fatalf("补齐理由未标注系统补齐: %q", top.Reason)
}
// 补齐后必须完整(补齐再校验)。
if missing, conflicts := r.CompletenessIssues(wardrobe); len(missing) != 0 || len(conflicts) != 0 {
t.Fatalf("still incomplete: missing=%v conflicts=%v", missing, conflicts)
}
}
// TestApplyAutoCompleteRemovesConflict 选连衣裙时剔除与之冲突的下装,并补上缺的鞋。
func TestApplyAutoCompleteRemovesConflict(t *testing.T) {
wardrobe := []mysqlmodel.Clothes{
clothesFixture(5, "连衣裙", "甜美", "粉", "3"),
clothesFixture(3, "下装", "休闲", "黑", "2"),
clothesFixture(4, "鞋", "甜美", "白", "1"),
}
r := &RecommendLLMResponse{Items: []RecommendLLMItem{itemFixture("连衣裙", 5), itemFixture("下装", 3)}}
added, removed := r.ApplyAutoComplete(wardrobe)
if strings.Join(removed, ",") != "下装" {
t.Fatalf("removed = %v, want [下装]", removed)
}
if strings.Join(added, ",") != "鞋" {
t.Fatalf("added = %v, want [鞋]", added)
}
for _, item := range r.Items {
if item.Type == "下装" {
t.Fatalf("下装 应被剔除: %+v", r.Items)
}
}
}
// TestApplyAutoCompleteDeterministic 同输入必须同输出(便于线上排查与回归)。
func TestApplyAutoCompleteDeterministic(t *testing.T) {
wardrobe := []mysqlmodel.Clothes{
clothesFixture(1, "上装", "休闲", "黑", "3"),
clothesFixture(2, "上装", "休闲", "白", "3"),
clothesFixture(3, "下装", "休闲", "黑", "2"),
clothesFixture(4, "鞋", "休闲", "黑", "1"),
clothesFixture(5, "鞋", "商务", "白", "1"),
}
first := &RecommendLLMResponse{Items: []RecommendLLMItem{itemFixture("外套", 9), itemFixture("下装", 3)}}
second := &RecommendLLMResponse{Items: []RecommendLLMItem{itemFixture("外套", 9), itemFixture("下装", 3)}}
first.ApplyAutoComplete(wardrobe)
second.ApplyAutoComplete(wardrobe)
if len(first.Items) != len(second.Items) {
t.Fatalf("length mismatch: %d vs %d", len(first.Items), len(second.Items))
}
for i := range first.Items {
if first.Items[i].ClothesID != second.Items[i].ClothesID {
t.Fatalf("run %d differs: %+v vs %+v", i, first.Items, second.Items)
}
}
}
// TestApplyAutoCompleteSkipsUnavailableCategory 衣橱没有该品类时无法补齐(不做无意义的失败)。
func TestApplyAutoCompleteSkipsUnavailableCategory(t *testing.T) {
wardrobe := []mysqlmodel.Clothes{clothesFixture(3, "下装", "休闲", "黑", "2")}
r := &RecommendLLMResponse{Items: []RecommendLLMItem{itemFixture("下装", 3)}}
added, removed := r.ApplyAutoComplete(wardrobe)
if len(added) != 0 || len(removed) != 0 {
t.Fatalf("added=%v removed=%v, want no change", added, removed)
}
if len(r.Items) != 1 {
t.Fatalf("items = %+v, want unchanged", r.Items)
}
}
// TestBuildCorrectionHint 纠偏提示:无问题返回空串;有问题时包含缺失/冲突品类。
func TestBuildCorrectionHint(t *testing.T) {
if got := BuildCorrectionHint(nil, nil); got != "" {
t.Fatalf("empty issues should return empty hint, got %q", got)
}
got := BuildCorrectionHint([]string{"上装", "鞋"}, []string{"下装"})
for _, want := range []string{"必须补充", "上装", "鞋", "必须移除", "下装"} {
if !strings.Contains(got, want) {
t.Fatalf("hint missing %q: %s", want, got)
}
}
}
// TestBuildRecommendPromptCompleteness 提示词包含完整性硬规则与去重约束;纠偏段仅在重试时出现。
func TestBuildRecommendPromptCompleteness(t *testing.T) {
wardrobe := []mysqlmodel.Clothes{clothesFixture(1, "上装", "休闲", "黑", "3")}
fortune := &FortuneItem{Overall: 4, Love: 3, Work: 2, Money: 5, Description: "状态不错"}
first := buildRecommendPrompt("白羊座", fortune, wardrobe, "", "")
for _, want := range []string{
"完整性要求",
"必须包含「上装」或「连衣裙」",
"外套是外层,不可孤立出现",
"二者互斥",
"每个存在的品类最多推荐 1 件",
} {
if !strings.Contains(first, want) {
t.Fatalf("prompt missing %q", want)
}
}
if strings.Contains(first, "上次输出问题") {
t.Fatal("首次调用不应包含纠偏段")
}
retry := buildRecommendPrompt("白羊座", fortune, wardrobe, "通勤", BuildCorrectionHint([]string{"上装"}, nil))
for _, want := range []string{"上次输出问题", "必须补充", "上装", "用户指定场景", "通勤"} {
if !strings.Contains(retry, want) {
t.Fatalf("retry prompt missing %q", want)
}
}
}