Files
ai-customer-service/test/integration/dialog_service_test.go
Your Name cf46b27610 fix: P0-1 RateLimiter并发写安全 + P0-2工单操作错误码区分 + P1 rows.Close修复
P0-1 (limits.go): Allow()方法改为全程使用写锁保护counters map读写,避免RLock写入时的data race
P0-2 (ticket_workflow.go+ticket_handler.go): Assign/Resolve/Close操作先查询ticket存在性和状态,返回明确的CS_TICKET_4001/CS_TKT_4002/CS_TICKET_4092/CS_TICKET_4093错误码,handler根据错误前缀路由HTTP状态码
P1-1 (ticket_store.go): 移除GetStats中3处手动rows.Close(),只保留defer Close()
2026-05-01 20:56:25 +08:00

155 lines
4.7 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 integration
import (
"context"
"testing"
"github.com/bridge/ai-customer-service/internal/domain/message"
"github.com/bridge/ai-customer-service/internal/service/dialog"
"github.com/bridge/ai-customer-service/internal/service/handoff"
intentservice "github.com/bridge/ai-customer-service/internal/service/intent"
"github.com/bridge/ai-customer-service/internal/service/reply"
"github.com/bridge/ai-customer-service/internal/store/memory"
)
// TestDialogService_AC02_IntentMatrix covers the AC-02 intent recognition test matrix:
// - 退款意图 → P1 handoff
// - 数据泄露意图 → P1 handoff
// - 人工意图 → handoff
// - 正常查询 → bot 回复(无 handoff
func TestDialogService_AC02_IntentMatrix(t *testing.T) {
sessions := memory.NewSessionStore()
audits := memory.NewAuditStore()
tickets := memory.NewTicketStore()
dedup := memory.NewDedupStore()
knowledge := memory.NewKnowledgeStore()
svc := dialog.NewService(sessions, audits, tickets, dedup, intentservice.NewService(), reply.NewService(knowledge), handoff.NewService())
tests := []struct {
name string
content string
wantIntent string
wantHandoff bool
wantPriority string // empty if no handoff expected
wantReply bool // whether to check reply is non-empty
}{
{
name: "AC-02: 退款意图 → P1 handoff",
content: "我要申请退款",
wantIntent: "refund",
wantHandoff: true,
wantPriority: "P1",
wantReply: true,
},
{
name: "AC-02: 数据泄露意图 → P1 handoff",
content: "我的账户数据泄露了",
wantIntent: "security",
wantHandoff: true,
wantPriority: "P1",
wantReply: true,
},
{
name: "AC-02: 人工意图 → handoff",
content: "转人工客服",
wantIntent: "handoff",
wantHandoff: true,
wantPriority: "P1", // NeedsHuman=true → P1
wantReply: true,
},
{
name: "AC-02: 正常查询 → bot 回复无 handoff",
content: "查询额度",
wantIntent: "quota",
wantHandoff: false,
wantReply: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result, err := svc.Process(context.Background(), &message.UnifiedMessage{
MessageID: "m_" + tc.name,
Channel: "widget",
OpenID: "u_" + tc.name,
Content: tc.content,
})
if err != nil {
t.Fatalf("Process() error = %v", err)
}
// Verify intent recognition
if result.Intent.Intent != tc.wantIntent {
t.Fatalf("intent = %s, want %s", result.Intent.Intent, tc.wantIntent)
}
// Verify handoff decision
if result.Handoff.ShouldHandoff != tc.wantHandoff {
t.Fatalf("handoff.ShouldHandoff = %v, want %v", result.Handoff.ShouldHandoff, tc.wantHandoff)
}
// Verify priority for handoff cases
if tc.wantHandoff {
if result.Handoff.Priority != tc.wantPriority {
t.Fatalf("handoff.Priority = %s, want %s", result.Handoff.Priority, tc.wantPriority)
}
// ticket must be created
if result.TicketID == "" {
t.Fatalf("TicketID empty, want non-empty for handoff case")
}
// Verify ticket was actually stored
stored := tickets.List()
found := false
for _, tk := range stored {
if tk.ID == result.TicketID {
found = true
if string(tk.Priority) != tc.wantPriority {
t.Fatalf("stored ticket priority = %s, want %s", tk.Priority, tc.wantPriority)
}
if tk.SessionID == "" {
t.Fatalf("stored ticket session_id is empty")
}
break
}
}
if !found {
t.Fatalf("ticket %s not found in store", result.TicketID)
}
} else {
// No handoff: ticket must NOT be created
if result.TicketID != "" {
t.Fatalf("TicketID = %s, want empty for non-handoff case", result.TicketID)
}
}
// Verify reply
if tc.wantReply && result.Reply == "" {
t.Fatalf("Reply empty, want non-empty reply")
}
})
}
}
func TestDialogService_Process(t *testing.T) {
sessions := memory.NewSessionStore()
audits := memory.NewAuditStore()
tickets := memory.NewTicketStore()
dedup := memory.NewDedupStore()
knowledge := memory.NewKnowledgeStore()
svc := dialog.NewService(sessions, audits, tickets, dedup, intentservice.NewService(), reply.NewService(knowledge), handoff.NewService())
result, err := svc.Process(context.Background(), &message.UnifiedMessage{MessageID: "m1", Channel: "widget", OpenID: "u1", Content: "查询额度"})
if err != nil {
t.Fatalf("Process() error = %v", err)
}
if result.Intent.Intent != "quota" {
t.Fatalf("intent = %s, want quota", result.Intent.Intent)
}
if result.Handoff.ShouldHandoff {
t.Fatalf("expected no handoff")
}
if len(audits.List()) != 1 {
t.Fatalf("audit events = %d, want 1", len(audits.List()))
}
}