package services import ( "context" "testing" "code.tczkiot.com/wlw/ai-agent/contract" "code.tczkiot.com/wlw/ai-agent/identity" "code.tczkiot.com/wlw/ai-agent/internal/models" "code.tczkiot.com/wlw/ai-agent/internal/pkg/enums" ) func TestCustomerQuickActionsResolveKnownCardWithoutChangingGuestOwnership(t *testing.T) { db := setupMessageWelcomeTestDB(t) aiAgent := createWelcomeTestAIAgent(t, db, "") external := welcomeTestExternalUser("card:50506783") conversation, err := ConversationService.Create(external, 11, aiAgent.ID) if err != nil { t.Fatalf("create conversation: %v", err) } originalCustomerType := conversation.CustomerType 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", Identifier: "898608691025D4186783", Name: "卡号 50506783", Enabled: true, }}, nil } return nil, nil }) t.Cleanup(func() { SetQuerySubjects(nil) }) var executedContext contract.BusinessReadContext if err := SetCustomerQuickActions([]contract.CustomerQuickAction{ { Code: "card/traffic", Title: "查流量", Message: "请查询流量", CustomerTypes: []string{"card"}, Execute: func(_ context.Context, businessContext contract.BusinessReadContext) (string, error) { executedContext = businessContext return "剩余流量 30G", nil }, }, { Code: "device/status", Title: "查设备", Message: "请查询设备", CustomerTypes: []string{"device"}, Execute: func(context.Context, contract.BusinessReadContext) (string, error) { return "设备正常", nil }, }, }); err != nil { t.Fatalf("SetCustomerQuickActions() error = %v", err) } t.Cleanup(func() { _ = SetCustomerQuickActions(nil) }) actions, err := CustomerQuickActionService.ListForConversation(context.Background(), conversation) if err != nil || len(actions) != 1 || actions[0].Code != "card/traffic" { t.Fatalf("known card actions = %#v, err = %v", actions, err) } if conversation.CustomerType != originalCustomerType || !ConversationService.IsCustomerConversationOwner(conversation, external) { t.Fatalf("quick-action resolution changed guest ownership: %#v", conversation) } if _, _, err := CustomerQuickActionService.ExecuteAndRecord( context.Background(), conversation.ID, "card/traffic", "known-card-1", external, "known-card-request-1", ); err != nil { t.Fatalf("ExecuteAndRecord() error = %v", err) } if executedContext.CustomerType != "card" || executedContext.CustomerID != 17443 || executedContext.CustomerExternalID != "50506783" { t.Fatalf("unexpected business context: %#v", executedContext) } } func TestCustomerQuickActionsResolveDeviceFromConversationHistory(t *testing.T) { db := setupMessageWelcomeTestDB(t) aiAgent := createWelcomeTestAIAgent(t, db, "") external := welcomeTestExternalUser("quick-device-history") conversation, err := ConversationService.Create(external, 11, aiAgent.ID) if err != nil { t.Fatalf("create conversation: %v", err) } 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", Identifier: "37012627000987", Name: "设备号 37012627000987", Enabled: true, }}, nil } return nil, nil }) t.Cleanup(func() { SetQuerySubjects(nil) }) if _, err := MessageService.SendCustomerMessageWithoutAIReplyWithRequestID( conversation.ID, "known-device-message", enums.IMMessageTypeHTML, "

设备号 37012627000987

", "", external, "known-device-request", ); err != nil { t.Fatalf("send identity message: %v", err) } if err := SetCustomerQuickActions([]contract.CustomerQuickAction{{ Code: "device/wifi", Title: "WiFi 信息", Message: "查询 WiFi", CustomerTypes: []string{"device"}, Execute: func(context.Context, contract.BusinessReadContext) (string, error) { return "WiFi 正常", nil }, }}); err != nil { t.Fatalf("SetCustomerQuickActions() error = %v", err) } t.Cleanup(func() { _ = SetCustomerQuickActions(nil) }) actions, err := CustomerQuickActionService.ListForConversation(context.Background(), conversation) if err != nil || len(actions) != 1 || actions[0].Code != "device/wifi" { t.Fatalf("known device actions = %#v, err = %v", actions, err) } } func TestCustomerQuickActionRecordsReplyWithoutTriggeringAI(t *testing.T) { db := setupMessageWelcomeTestDB(t) aiAgent := createWelcomeTestAIAgent(t, db, "") external := welcomeTestExternalUser("quick-action-user") conversation, err := ConversationService.Create(external, 11, aiAgent.ID) if err != nil { t.Fatalf("create conversation: %v", err) } if err := SetCustomerQuickActions([]contract.CustomerQuickAction{{ Code: "test/status", Title: "查状态", Message: "请查询状态", Execute: func(context.Context, contract.BusinessReadContext) (string, error) { return "当前状态正常", nil }, }}); err != nil { t.Fatalf("SetCustomerQuickActions() error = %v", err) } t.Cleanup(func() { _ = SetCustomerQuickActions(nil) }) previousHook := TriggerAIReplyAsyncHook called := false TriggerAIReplyAsyncHook = func(context.Context, models.Conversation, models.Message) { called = true } t.Cleanup(func() { TriggerAIReplyAsyncHook = previousHook }) customerMessage, replyMessage, err := CustomerQuickActionService.ExecuteAndRecord( context.Background(), conversation.ID, "test/status", "quick-client-1", external, "quick-request-1", ) if err != nil { t.Fatalf("ExecuteAndRecord() error = %v", err) } if customerMessage.Content != "请查询状态" || customerMessage.SenderType != enums.IMSenderTypeCustomer { t.Fatalf("unexpected customer message: %#v", customerMessage) } if replyMessage.Content != "当前状态正常" || replyMessage.SenderType != enums.IMSenderTypeAI { t.Fatalf("unexpected automatic reply: %#v", replyMessage) } if called { t.Fatalf("quick action customer message must not trigger an AI reply") } var count int64 if err := db.Model(&models.Message{}).Where("conversation_id = ?", conversation.ID).Count(&count).Error; err != nil { t.Fatalf("count messages: %v", err) } if count != 2 { t.Fatalf("message count = %d, want 2", count) } } func TestCustomerQuickActionAvailabilityIsCheckedForListAndExecution(t *testing.T) { db := setupMessageWelcomeTestDB(t) aiAgent := createWelcomeTestAIAgent(t, db, "") external := welcomeTestExternalUser("quick-action-availability-user") conversation, err := ConversationService.Create(external, 11, aiAgent.ID) if err != nil { t.Fatalf("create conversation: %v", err) } available := false if err := SetCustomerQuickActions([]contract.CustomerQuickAction{{ Code: "test/dynamic", Title: "动态操作", Message: "执行动态操作", Available: func(context.Context, contract.BusinessReadContext) (bool, error) { return available, nil }, Execute: func(context.Context, contract.BusinessReadContext) (string, error) { return "执行成功", nil }, }}); err != nil { t.Fatalf("SetCustomerQuickActions() error = %v", err) } t.Cleanup(func() { _ = SetCustomerQuickActions(nil) }) actions, err := CustomerQuickActionService.ListForConversation(context.Background(), conversation) if err != nil || len(actions) != 0 { t.Fatalf("unavailable action leaked into list: actions=%#v err=%v", actions, err) } if _, _, err := CustomerQuickActionService.ExecuteAndRecord(context.Background(), conversation.ID, "test/dynamic", "quick-dynamic-1", external, "quick-dynamic-request-1"); err == nil { t.Fatal("unavailable action was executed") } available = true actions, err = CustomerQuickActionService.ListForConversation(context.Background(), conversation) if err != nil || len(actions) != 1 || actions[0].Code != "test/dynamic" { t.Fatalf("available action missing from list: actions=%#v err=%v", actions, err) } } func TestCustomerQuickActionExecutesMatchedRecordedMessage(t *testing.T) { db := setupMessageWelcomeTestDB(t) aiAgent := createWelcomeTestAIAgent(t, db, "") external := welcomeTestExternalUser("matched-action-user") conversation, err := ConversationService.Create(external, 11, aiAgent.ID) if err != nil { t.Fatalf("create conversation: %v", err) } if err := SetCustomerQuickActions([]contract.CustomerQuickAction{{ Code: "test/traffic", Title: "查流量", Message: "请查询流量", MatchIntent: func(message string) bool { return message == "卡号 50506783,请查询流量" }, Execute: func(context.Context, contract.BusinessReadContext) (string, error) { return "剩余流量:58.38G", nil }, }}); err != nil { t.Fatalf("SetCustomerQuickActions() error = %v", err) } t.Cleanup(func() { _ = SetCustomerQuickActions(nil) }) message, err := MessageService.SendCustomerMessageWithoutAIReplyWithRequestID( conversation.ID, "matched-customer-1", enums.IMMessageTypeText, "卡号 50506783,请查询流量", "", external, "matched-request-1", ) if err != nil { t.Fatalf("send customer message: %v", err) } matched, err := CustomerQuickActionService.ExecuteMatchedReply( context.Background(), conversation, message.Content, message.RequestID, message.ID, ) if err != nil || !matched { t.Fatalf("ExecuteMatchedReply() matched=%v err=%v", matched, err) } list, _, _ := MessageService.FindByConversationIDCursor(conversation.ID, 0, 20, "", "") if len(list) != 2 || list[1].Content != "剩余流量:58.38G" || list[1].SenderType != enums.IMSenderTypeAI { t.Fatalf("unexpected messages: %#v", list) } } func TestCustomerQuickActionExecutesSelectedMenuItem(t *testing.T) { db := setupMessageWelcomeTestDB(t) aiAgent := createWelcomeTestAIAgent(t, db, "") external := welcomeTestExternalUser("selected-action-user") conversation, err := ConversationService.Create(external, 11, aiAgent.ID) if err != nil { t.Fatalf("create conversation: %v", err) } if err := SetCustomerQuickActions([]contract.CustomerQuickAction{{ Code: "test/traffic", Title: "查流量", Message: "请查询流量", Sort: 10, Execute: func(context.Context, contract.BusinessReadContext) (string, error) { return "剩余流量:58.38G", nil }, }}); err != nil { t.Fatalf("SetCustomerQuickActions() error = %v", err) } t.Cleanup(func() { _ = SetCustomerQuickActions(nil) }) matched, aiMessage, err := CustomerQuickActionService.ExecuteSelectedReply( context.Background(), conversation, 1, "selected-request-1", 100, ) if err != nil || !matched || aiMessage != "" { t.Fatalf("ExecuteSelectedReply() matched=%v aiMessage=%q err=%v", matched, aiMessage, err) } list, _, _ := MessageService.FindByConversationIDCursor(conversation.ID, 0, 20, "", "") if len(list) != 1 || list[0].Content != "剩余流量:58.38G" { t.Fatalf("unexpected selected action messages: %#v", list) } } func TestCustomerQuickActionExecutesActionByCode(t *testing.T) { db := setupMessageWelcomeTestDB(t) aiAgent := createWelcomeTestAIAgent(t, db, "") external := welcomeTestExternalUser("coded-action-user") conversation, err := ConversationService.Create(external, 11, aiAgent.ID) if err != nil { t.Fatalf("create conversation: %v", err) } if err := SetCustomerQuickActions([]contract.CustomerQuickAction{ { Code: "test/status", Title: "查状态", Message: "请查询状态", Sort: 10, Execute: func(context.Context, contract.BusinessReadContext) (string, error) { return "当前状态正常", nil }, }, { Code: "test/diagnosis", Title: "智能检测", Message: "请智能检测", Sort: 20, Execute: func(context.Context, contract.BusinessReadContext) (string, error) { return "智能检测结果:网络异常", nil }, }, }); err != nil { t.Fatalf("SetCustomerQuickActions() error = %v", err) } t.Cleanup(func() { _ = SetCustomerQuickActions(nil) }) matched, aiMessage, err := CustomerQuickActionService.ExecuteActionReply( context.Background(), conversation, "test/diagnosis", "coded-request-1", 101, ) if err != nil || !matched || aiMessage != "" { t.Fatalf("ExecuteActionReply() matched=%v aiMessage=%q err=%v", matched, aiMessage, err) } list, _, _ := MessageService.FindByConversationIDCursor(conversation.ID, 0, 20, "", "") if len(list) != 1 || list[0].Content != "智能检测结果:网络异常" { t.Fatalf("unexpected coded action messages: %#v", list) } }