package domain import ( "strings" "testing" "time" "git.cin.su/m/Assistant/contracts/identity" "git.cin.su/m/Assistant/contracts/interaction" ) func TestNewHumanUtteranceClassifiesQuestionAndRequiresCognition(t *testing.T) { conversation := interaction.Conversation{ID: "conversation-1", ActorID: "actor-1", PrincipalID: "principal-1", Channel: interaction.ChannelWebChat, State: interaction.ConversationOpen} utterance, intent, relations, err := NewHumanUtterance(conversation, interaction.ChatMessageRequest{Message: "Кто ты?"}, time.Unix(10, 0)) if err != nil { t.Fatal(err) } if utterance.SpeechAct != interaction.SpeechActQuestion || !NeedsCognition(utterance.SpeechAct) { t.Fatalf("expected cognitive question, got %s", utterance.SpeechAct) } if intent.Text != "Кто ты?" || len(relations) != 0 { t.Fatalf("unexpected intent or relations") } } func TestNewHumanUtteranceAppendsCorrectionRelation(t *testing.T) { conversation := interaction.Conversation{ID: "conversation-1", ActorID: "actor-1", PrincipalID: "principal-1", Channel: interaction.ChannelWebChat, State: interaction.ConversationOpen} utterance, _, relations, err := NewHumanUtterance(conversation, interaction.ChatMessageRequest{Message: "исправь прошлое", CorrectsUtteranceID: "utterance-0"}, time.Unix(10, 0)) if err != nil { t.Fatal(err) } if utterance.SpeechAct != interaction.SpeechActCorrection { t.Fatalf("expected correction, got %s", utterance.SpeechAct) } if len(relations) != 1 || relations[0].Kind != interaction.RelationCorrects || relations[0].ToUtteranceID != "utterance-0" { t.Fatalf("unexpected correction relation: %#v", relations) } } func TestNewHumanUtteranceClassifiesReminderAndCapabilityConnectionAsDirective(t *testing.T) { conversation := interaction.Conversation{ID: "conversation-1", ActorID: "actor-1", PrincipalID: "principal-1", Channel: interaction.ChannelWebChat, State: interaction.ConversationOpen} for _, message := range []string{"Напомни завтра позвонить врачу.", "Подключи интеграцию с неизвестным сервисом."} { utterance, _, _, err := NewHumanUtterance(conversation, interaction.ChatMessageRequest{Message: message}, time.Unix(10, 0)) if err != nil { t.Fatal(err) } if utterance.SpeechAct != interaction.SpeechActDirective { t.Fatalf("expected directive for %q, got %s", message, utterance.SpeechAct) } } } func TestDeterministicCandidateIsInvariantToMissingQuestionMarkForIdentityAndArithmetic(t *testing.T) { cases := []struct { message string act interaction.SpeechAct }{ {"ты кто", interaction.SpeechActQuestion}, {"Ты кто?", interaction.SpeechActQuestion}, {"а ты кто", interaction.SpeechActQuestion}, {"сколько будет 2 + 2", interaction.SpeechActQuestion}, {"сколько будет 2 + 2?", interaction.SpeechActQuestion}, {"2 плюс 2 сколько", interaction.SpeechActQuestion}, } for _, tc := range cases { candidate := BuildDeterministicCandidate(tc.message, "", time.Unix(10, 0).UnixMilli()) if candidate.PrimarySpeechAct != tc.act { t.Fatalf("expected %s for %q, got %s (%+v)", tc.act, tc.message, candidate.PrimarySpeechAct, candidate) } if candidate.Applicability == interaction.SemanticApplicable && candidate.NeedsCognition { continue } if !candidate.NeedsCognition { t.Fatalf("expected cognition-worthy candidate for %q, got %+v", tc.message, candidate) } } } func TestDeterministicCandidateTreatsModalExplanationRequestsAsCognitionWorthWithoutQuestionMark(t *testing.T) { for _, message := range []string{ "можешь рассказать об утре", "можешь расказать об утре", "можете объяснить это проще", "расскажи об утре", "про утро расскажешь", } { candidate := BuildDeterministicCandidate(message, "", time.Unix(10, 0).UnixMilli()) if candidate.PrimarySpeechAct != interaction.SpeechActQuestion && candidate.PrimarySpeechAct != interaction.SpeechActRequestToInvestigate { t.Fatalf("expected broader cognitive speech act for %q, got %+v", message, candidate) } if !candidate.NeedsCognition { t.Fatalf("expected cognition for %q, got %+v", message, candidate) } } } func TestDeterministicCandidateTreatsModelRequestAsDirectiveNeedingCognition(t *testing.T) { candidate := BuildDeterministicCandidate("обратись к модели gemma4 и спроси сколько звезд на небе", "", time.Unix(10, 0).UnixMilli()) if candidate.PrimarySpeechAct != interaction.SpeechActDirective { t.Fatalf("expected directive, got %+v", candidate) } if !candidate.NeedsCognition { t.Fatalf("expected cognition-worthy model request, got %+v", candidate) } } func TestAWordModelDoesNotTriggerModelInventoryOrVerification(t *testing.T) { candidate := BuildDeterministicCandidate("модель должна понимать смысл вопроса", "", time.Unix(10, 0).UnixMilli()) for _, hint := range candidate.CapabilityHints { if hint == "VERIFY_FACT" { t.Fatalf("an incidental model mention must not trigger fact verification: %+v", candidate) } } } func TestInformationRequestsAreClassifiedOnceByInteraction(t *testing.T) { cases := []struct { message string kind interaction.InformationRequestKind }{ {"какие модели доступны?", interaction.InformationRequestCognitiveModelInventory}, {"что ты умеешь?", interaction.InformationRequestCapabilityInventory}, {"проверь состояние локального канала", interaction.InformationRequestCognitiveChannelStatus}, {"нет связи с ИИ, восстанови канал", interaction.InformationRequestCognitiveRecovery}, {"сообщии список моделей", interaction.InformationRequestCognitiveModelInventory}, {"сообщите список подключённых моделей", interaction.InformationRequestCognitiveModelInventory}, } for _, test := range cases { result, err := InterpretUtterance(interaction.HumanUtterance{Content: test.message}, nil, nil, time.Unix(10, 0)) if err != nil { t.Fatalf("interpret %q: %v", test.message, err) } if result.InformationRequestKind != test.kind { t.Fatalf("message=%q kind=%q want=%q", test.message, result.InformationRequestKind, test.kind) } } } func TestSourceDependentQuestionRequestsWebResearchWithoutEntityBranch(t *testing.T) { candidate := BuildDeterministicCandidate("какая погода в Киеве?", "", time.Unix(10, 0).UnixMilli()) seen := false for _, hint := range candidate.CapabilityHints { if hint == "WEB_RESEARCH" { seen = true } } if !seen { t.Fatalf("source-dependent question was not routed to WEB_RESEARCH: %+v", candidate) } ordinary := BuildDeterministicCandidate("что такое гравитация?", "", time.Unix(10, 0).UnixMilli()) for _, hint := range ordinary.CapabilityHints { if hint == "WEB_RESEARCH" { t.Fatalf("ordinary conceptual question was incorrectly routed to WEB_RESEARCH: %+v", ordinary) } } } func TestSourceDependentQuestionToleratesTyposAndPlaceSearch(t *testing.T) { for _, message := range []string{"какая пугода в Сочи", "найди ближайший ресторан в Сочи", "найди билеты до Ялты"} { candidate := BuildDeterministicCandidate(message, "", time.Unix(10, 0).UnixMilli()) seen := false for _, hint := range candidate.CapabilityHints { if hint == "WEB_RESEARCH" { seen = true } } if !seen { t.Fatalf("message=%q was not routed to WEB_RESEARCH: %+v", message, candidate) } } } func TestRequestToClarifyCurrentFactRequestsWebResearch(t *testing.T) { candidate := BuildDeterministicCandidate("уточни погоду в питере", "", time.Unix(10, 0).UnixMilli()) for _, hint := range candidate.CapabilityHints { if hint == "WEB_RESEARCH" { return } } t.Fatalf("clarification request for current fact was not routed to WEB_RESEARCH: %+v", candidate) } func TestRetryRequestIsAContinuationCue(t *testing.T) { for _, message := range []string{"ещё раз попробуй", "повтори", "попробуй снова"} { if !IsRetryRequest(message) { t.Fatalf("expected retry cue for %q", message) } } } func TestNamingPreferenceIsSemanticAndProducesNaturalAcceptance(t *testing.T) { result := BuildDeterministicCandidate("я буду называть тебя Дед, запомни ты дед", "", time.Unix(10, 0).UnixMilli()) if result.PrimarySpeechAct != interaction.SpeechActPreference { t.Fatalf("expected naming preference, got %+v", result) } if name := PreferredAssistantName("я буду называть тебя Дед, запомни ты дед"); name != "Дед" { t.Fatalf("expected extracted assistant name, got %q", name) } if name := PreferredAssistantName("я назову тебя дед, запомни"); name != "Дед" { t.Fatalf("expected conversational name form to be extracted, got %q", name) } if name := PreferredAssistantName("давай переименуем тебя в фею. теперь тебя зовут фея"); name != "Фея" { t.Fatalf("rename phrase returned %q", name) } if !IsIdentityInquiry("ты кто?") || !IsIdentityInquiry("как тебя зовут") { t.Fatal("identity variants should be recognized") } } func TestMemoryInventoryRequestIsSemantic(t *testing.T) { for _, message := range []string{"выведи свою память", "напиши что ты помнишь в виде таблицы"} { candidate := BuildDeterministicCandidate(message, "", time.Unix(10, 0).UnixMilli()) if got := informationRequestKind(message, candidate); got != interaction.InformationRequestMemoryInventory { t.Fatalf("%q: expected memory inventory, got %q", message, got) } } } func TestCreativeRequestsDoNotRouteToWebResearch(t *testing.T) { for _, message := range []string{"нарисуй круг", "расскажи сказку о колобке"} { candidate := BuildDeterministicCandidate(message, "", time.Now().UnixMilli()) t.Logf("%q candidate=%+v", message, candidate) for _, hint := range candidate.CapabilityHints { if hint == "WEB_RESEARCH" { t.Fatalf("creative request %q unexpectedly selected web research: %+v", message, candidate) } } } } func TestInterpretCreativeRequestDoesNotRequireExternalEvidence(t *testing.T) { utterance := interaction.HumanUtterance{Content: "нарисуй круг"} result, err := InterpretUtterance(utterance, nil, nil, time.Now()) if err != nil { t.Fatal(err) } t.Logf("semantic result=%+v", result) if result.Interpretation.SpeechAct == interaction.SpeechActQuestion || containsString(result.CapabilityHints, "WEB_RESEARCH") { t.Fatalf("creative request was routed as external research: %+v", result) } if got := CreativeDeterministicReply("нарисуй круг"); got != "◯" { t.Fatalf("circle reply = %q", got) } if !IsCreativeRequest("расскажи сказку о колобке") { t.Fatal("story request was not recognized as creative") } if got := CreativeFallbackReply("расскажи сказку о колобке"); !strings.Contains(got, "Колоб") { t.Fatalf("story fallback = %q", got) } } func containsString(values []string, expected string) bool { for _, value := range values { if value == expected { return true } } return false } func TestLocationContextReply(t *testing.T) { if got := LocationContextReply("я сейчас в Москве"); got != "Запомнил: сейчас вы в Москве." { t.Fatalf("unexpected location reply %q", got) } } func TestTimeDifferenceRequestIsSemantic(t *testing.T) { message := "какая разница во времени между Москвой и Шанхаем?" candidate := BuildDeterministicCandidate(message, "", time.Unix(10, 0).UnixMilli()) if got := informationRequestKind(message, candidate); got != interaction.InformationRequestTimeDifference { t.Fatalf("expected time difference, got %q", got) } } func TestDeterministicCandidateKeepsFirstPersonCompletedWorkAsAssertion(t *testing.T) { candidate := BuildDeterministicCandidate("Я всё уже настроил.", "", time.Unix(10, 0).UnixMilli()) if candidate.PrimarySpeechAct != interaction.SpeechActAssertion { t.Fatalf("expected assertion, got %+v", candidate) } } func TestNewConversationRequiresActorAndPrincipal(t *testing.T) { _, err := NewConversation(identity.ActorID(""), identity.PrincipalID("principal-1"), time.Unix(10, 0)) if err == nil { t.Fatal("expected invalid principal") } }