112 lines
4.4 KiB
Go
112 lines
4.4 KiB
Go
package service
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"errors"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"cmautobuy/admin/model"
|
||
|
|
"cmautobuy/admin/repository"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestAISpecMatchIdentity_相同业务上下文去重且数据变化后失效(t *testing.T) {
|
||
|
|
base := repository.SybOrderContext{
|
||
|
|
Order: model.SybOrder{SybID: "SYB-1", ShopeeGoodsID: "SP-1", SpecKey: "黑色,M", ProductSpec: "黑色,M"},
|
||
|
|
PddGoodsID: "737116531267", PddUpdatedAt: "2026-08-14T01:00:00Z", PddSkusJSON: collectedAIChoices,
|
||
|
|
}
|
||
|
|
same := base
|
||
|
|
same.Order.SybID = "SYB-2"
|
||
|
|
same.Order.OrderNo = "ORDER-2"
|
||
|
|
if aiSpecMatchIdentity(base) != aiSpecMatchIdentity(same) {
|
||
|
|
t.Fatal("同一蝦皮商品、来源规格和 PDD 上下文应归为同一个批次调用")
|
||
|
|
}
|
||
|
|
changed := same
|
||
|
|
changed.PddSkusJSON = collectedRuleChoices
|
||
|
|
if aiSpecMatchIdentity(base) == aiSpecMatchIdentity(changed) {
|
||
|
|
t.Fatal("PDD 候选变化后不得复用旧批次结果")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestBatchItemFromMatch_结果分类和置信度零值(t *testing.T) {
|
||
|
|
item := model.AIMatchBatchItem{ItemID: "AII-1"}
|
||
|
|
got := batchItemFromMatch(item, AISpecMatchResult{Outcome: "rejected", Message: "需要人工", ConfidenceBPS: 0}, nil)
|
||
|
|
if got.Status != "manual" || got.ConfidenceSet {
|
||
|
|
t.Fatalf("拒绝结果应进入人工队列且未提供置信度: %+v", got)
|
||
|
|
}
|
||
|
|
got = batchItemFromMatch(item, AISpecMatchResult{Outcome: "ai_saved", Source: "ai", ConfidenceBPS: 0, ConfidenceSet: true}, nil)
|
||
|
|
if got.Status != "succeeded" || !got.ConfidenceSet || got.ConfidenceBPS != 0 {
|
||
|
|
t.Fatalf("模型明确返回的 0%% 置信度也应被准确记录: %+v", got)
|
||
|
|
}
|
||
|
|
got = batchItemFromMatch(item, AISpecMatchResult{}, errors.New("provider failed"))
|
||
|
|
if got.Status != "failed" || got.Outcome != "failed" {
|
||
|
|
t.Fatalf("调用错误应记为失败: %+v", got)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAIMatchBatch_同规格只调用一次并记录逐条结果(t *testing.T) {
|
||
|
|
db := newTestDB(t)
|
||
|
|
actor := seedAIMatchContext(t, db, "SYB-BATCH-1")
|
||
|
|
seedWorkflowOrder(t, db, "SYB-BATCH-2", "SP-AI", "黑色,M")
|
||
|
|
|
||
|
|
fake := &fakeAIModelClient{response: AIModelMatchResponse{
|
||
|
|
Conclusion: "match", CandidateID: "C01", ConfidenceBPS: 9200, Reason: "颜色和尺码一致",
|
||
|
|
}}
|
||
|
|
snapshot := testAIMatchSnapshot(fake)
|
||
|
|
snapshot.Provider.TimeoutSeconds = 30
|
||
|
|
snapshot.Provider.MaxConcurrency = 2
|
||
|
|
batch, err := CreateAIMatchBatch(db, &actor, []string{"SYB-BATCH-1", "SYB-BATCH-2"}, snapshot, time.Now())
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if err := RunAIMatchBatch(context.Background(), db, actor, batch.BatchID, snapshot); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if fake.calls != 1 {
|
||
|
|
t.Fatalf("相同业务上下文应只调用模型一次,实际 %d 次", fake.calls)
|
||
|
|
}
|
||
|
|
view, err := GetAIMatchBatchView(db, &actor, batch.BatchID)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if view.Status != "succeeded" || view.Processed != 2 || view.Success != 1 || view.Reused != 1 {
|
||
|
|
t.Fatalf("批次汇总错误: %+v", view)
|
||
|
|
}
|
||
|
|
if len(view.Items) != 2 || view.Items[0].SourceText != "AI匹配" || view.Items[1].Status != "reused" {
|
||
|
|
t.Fatalf("逐条结果或来源错误: %+v", view.Items)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAIMatchBatch_重启中断和查看权限(t *testing.T) {
|
||
|
|
db := newTestDB(t)
|
||
|
|
actor := seedAIMatchContext(t, db, "SYB-BATCH-INTERRUPT")
|
||
|
|
snapshot := testAIMatchSnapshot(&fakeAIModelClient{})
|
||
|
|
snapshot.Provider.TimeoutSeconds = 30
|
||
|
|
snapshot.Provider.MaxConcurrency = 1
|
||
|
|
batch, err := CreateAIMatchBatch(db, &actor, []string{"SYB-BATCH-INTERRUPT"}, snapshot, time.Now())
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if count, err := InterruptRunningAIMatchBatches(db, time.Now()); err != nil || count != 1 {
|
||
|
|
t.Fatalf("中断未完成批次失败: count=%d err=%v", count, err)
|
||
|
|
}
|
||
|
|
view, err := GetAIMatchBatchView(db, &actor, batch.BatchID)
|
||
|
|
if err != nil || view.Status != "interrupted" || view.Failed != 1 || view.Items[0].Status != "interrupted" {
|
||
|
|
t.Fatalf("中断状态错误: view=%+v err=%v", view, err)
|
||
|
|
}
|
||
|
|
|
||
|
|
other := model.User{UserID: "USR-OTHER", Username: "other", PasswordHash: "test-hash", Role: model.RolePurchaser,
|
||
|
|
Status: model.UserActive, PasswordChangedAt: model.NowISO(), CreatedAt: model.NowISO(), UpdatedAt: model.NowISO()}
|
||
|
|
if err := repository.CreateUser(db, other); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if _, err := GetAIMatchBatchView(db, &other, batch.BatchID); !errors.Is(err, ErrForbidden) {
|
||
|
|
t.Fatalf("其他采购员不应查看本批次: %v", err)
|
||
|
|
}
|
||
|
|
admin := other
|
||
|
|
admin.Role = model.RoleAdmin
|
||
|
|
if _, err := GetAIMatchBatchView(db, &admin, batch.BatchID); err != nil {
|
||
|
|
t.Fatalf("管理员应可查看任意批次: %v", err)
|
||
|
|
}
|
||
|
|
}
|