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

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

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

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

174 lines
6.2 KiB
Go

package runtime
import (
"context"
"testing"
"code.tczkiot.com/wlw/ai-agent/identity"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
svc "code.tczkiot.com/wlw/ai-agent/internal/services"
)
func TestResolveGuestBusinessIdentityFromCardNumber(t *testing.T) {
svc.SetQuerySubjects(func(_ context.Context, query identity.Query) ([]identity.Subject, error) {
if len(query.Types) == 1 && query.Types[0] == identity.SubjectCard && query.Keyword == "50506783" {
return []identity.Subject{{
Type: identity.SubjectCard,
Category: identity.CategoryUser,
ID: 17443,
Username: "50506783",
Name: "卡号 50506783",
Identifier: "898608691025D4186783",
Enabled: true,
}}, nil
}
return nil, nil
})
t.Cleanup(func() { svc.SetQuerySubjects(nil) })
resolution, err := resolveGuestBusinessIdentity(context.Background(), models.Conversation{
ID: 9,
CustomerType: string(enums.ExternalSourceGuest),
CustomerExternalID: "guest-1",
CustomerName: "访客",
CurrentAssigneeID: 0,
CustomerUnreadCount: 0,
AgentUnreadCount: 0,
}, models.Message{
ID: 20,
MessageType: enums.IMMessageTypeText,
Content: "卡号 50506783,帮我查流量",
})
if err != nil {
t.Fatalf("resolveGuestBusinessIdentity() error = %v", err)
}
if resolution.NeedsPrompt {
t.Fatal("resolved card identity must not request another identity prompt")
}
if resolution.Conversation.CustomerType != string(identity.SubjectCard) ||
resolution.Conversation.CustomerID != 17443 ||
resolution.Conversation.CustomerExternalID != "50506783" {
t.Fatalf("unexpected resolved conversation: %#v", resolution.Conversation)
}
}
func TestResolveGuestBusinessIdentityFromDeviceNumber(t *testing.T) {
svc.SetQuerySubjects(func(_ context.Context, query identity.Query) ([]identity.Subject, error) {
if len(query.Types) == 1 && query.Types[0] == identity.SubjectDevice && query.Keyword == "37012627000987" {
return []identity.Subject{{
Type: identity.SubjectDevice,
Category: identity.CategoryUser,
ID: 27,
Username: "37012627000987",
Name: "设备号 37012627000987",
Identifier: "37012627000987",
Enabled: true,
}}, nil
}
return nil, nil
})
t.Cleanup(func() { svc.SetQuerySubjects(nil) })
resolution, err := resolveGuestBusinessIdentity(context.Background(), models.Conversation{
ID: 10,
CustomerType: string(enums.ExternalSourceGuest),
CustomerExternalID: "guest-2",
}, models.Message{
ID: 21,
MessageType: enums.IMMessageTypeText,
Content: "设备号 37012627000987",
})
if err != nil {
t.Fatalf("resolveGuestBusinessIdentity() error = %v", err)
}
if resolution.NeedsPrompt || resolution.Conversation.CustomerType != string(identity.SubjectDevice) || resolution.Conversation.CustomerID != 27 {
t.Fatalf("unexpected resolved conversation: %#v", resolution.Conversation)
}
}
func TestBusinessIdentityCandidatesRequireExplicitIdentifier(t *testing.T) {
candidates, explicit := businessIdentityCandidates("请帮我查流量")
if len(candidates) != 0 || explicit {
t.Fatalf("unexpected candidates=%v explicit=%v", candidates, explicit)
}
candidates, explicit = businessIdentityCandidates("37012627000987")
if len(candidates) != 1 || candidates[0] != "37012627000987" || !explicit {
t.Fatalf("unexpected candidates=%v explicit=%v", candidates, explicit)
}
}
func TestBusinessIdentityMessageContentSupportsHTMLInput(t *testing.T) {
content := businessIdentityMessageContent(models.Message{
MessageType: enums.IMMessageTypeHTML,
Content: "<p>卡板50506783</p>",
})
if content != "卡板50506783" {
t.Fatalf("unexpected html message content: %q", content)
}
candidates, explicit := businessIdentityCandidates(content)
if len(candidates) != 1 || candidates[0] != "50506783" || !explicit {
t.Fatalf("html card identifier was not extracted: candidates=%#v explicit=%v", candidates, explicit)
}
}
func TestBusinessIdentityHTMLMenuInput(t *testing.T) {
if !isBusinessIdentityOnlyMessage(models.Message{
MessageType: enums.IMMessageTypeHTML,
Content: "<p>卡板50506783</p>",
}) {
t.Fatal("card-only html message must open the deterministic service menu")
}
if isBusinessIdentityOnlyMessage(models.Message{
MessageType: enums.IMMessageTypeHTML,
Content: "<p>卡号 50506783,请查询流量</p>",
}) {
t.Fatal("card query must execute the requested service instead of opening the menu")
}
selection, ok := businessIdentityMenuSelection(models.Message{
MessageType: enums.IMMessageTypeHTML,
Content: "<p>1</p>",
})
if !ok || selection != 1 {
t.Fatalf("unexpected html menu selection: selection=%d ok=%v", selection, ok)
}
}
func TestLegacyCardMenuActionCode(t *testing.T) {
statusMenu := &models.Message{MessageType: enums.IMMessageTypeText, Content: `已确认您查询的是卡板 50506783 的卡片状态。
1. 卡片提示停机/被暂停使用
2. 无法上网(有信号但连不上)
3. 无信号/无服务
4. 已充值但未恢复`}
for selection := 1; selection <= 4; selection++ {
code, ok := legacyCardMenuActionCode(statusMenu, selection)
if !ok || code != "card/network_diagnosis" {
t.Fatalf("status menu selection %d returned code=%q ok=%v", selection, code, ok)
}
}
helpMenu := &models.Message{MessageType: enums.IMMessageTypeText, Content: `请问您遇到的是哪种情况?
1. 卡片状态异常 / 停机
2. 无法上网 / 网络连接问题
3. 套餐或流量相关
4. 其他问题`}
wants := map[int]string{1: "card/status", 2: "card/network_diagnosis", 3: "card/package"}
for selection, want := range wants {
code, ok := legacyCardMenuActionCode(helpMenu, selection)
if !ok || code != want {
t.Fatalf("help menu selection %d returned code=%q ok=%v, want %q", selection, code, ok, want)
}
}
if _, ok := legacyCardMenuActionCode(helpMenu, 4); ok {
t.Fatal("free-form other problem must remain available to the AI")
}
}
func TestGuestBusinessIdentityPromptForInvalidIdentifier(t *testing.T) {
got := guestBusinessIdentityPrompt(guestBusinessIdentityResolution{CandidateProvided: true}, nil)
if got != invalidBusinessIdentityReply {
t.Fatalf("unexpected prompt: %q", got)
}
}