package domain import ( "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 TestPlaneRequestReceivesTravelResearchCapability(t *testing.T) { for _, message := range []string{"найди ближайший самолет до Казани", "найди сомалет до Казани"} { candidate := BuildDeterministicCandidate(message, "", 100) if !containsToken(candidate.CapabilityHints, "WEB_RESEARCH") { t.Fatalf("plane request did not select web research for %q: %+v", message, candidate) } } } 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 TestDeterministicCandidateTreatsFormattingImperativeAsCognitionWorthyDirective(t *testing.T) { candidate := BuildDeterministicCandidate("выведи пустую таблицу из трех колонок и пяти строк", "", time.Unix(10, 0).UnixMilli()) if candidate.PrimarySpeechAct != interaction.SpeechActDirective { t.Fatalf("formatting request was not recognized as a directive: %+v", candidate) } if !candidate.NeedsCognition { t.Fatalf("formatting request must reach cognitive reasoning: %+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.InformationRequestCognitiveModelInventory}, {"какая модель отвечает?", interaction.InformationRequestCognitiveCurrentModel}, {"почему ты не смог найти ответ?", interaction.InformationRequestResearchFailureExplanation}, {"что ты можешь сделать без внешних моделей?", interaction.InformationRequestLocalCapabilityInventory}, {"что ты умеешь?", interaction.InformationRequestCapabilityInventory}, {"что доступно в этом чате?", 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{"какая пугода в Сочи", "найди ближайший ресторан в Сочи", "найди билеты до Ялты", "найди на WB бренд marka-wear"} { 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 TestDecomposeMarketplaceBrandRequestIntoExecutionAtoms(t *testing.T) { lexicon := TaskLexicon{ObjectAliases: []TaskLexiconEntry{{Alias: "бренд", Value: "BRAND"}}, SourceAliases: []TaskLexiconEntry{{Alias: "wb", Value: "WILDBERRIES"}}} got := DecomposeTaskWithLexicon("найди на WB бренд marka-wear", lexicon) if got.Intent != TaskIntentSearch || got.Object != "BRAND" || got.Source != "WILDBERRIES" || got.Target != "marka-wear" || !got.NeedsResearch { t.Fatalf("unexpected marketplace decomposition: %+v", got) } if len(got.Missing) != 0 { t.Fatalf("complete marketplace request marked incomplete: %+v", got) } } func TestDecomposeDoesNotInventMutableAliases(t *testing.T) { got := DecomposeTask("найди на WB бренд marka-wear") if got.Source != "" || got.Object != "" || got.NeedsResearch { t.Fatalf("parser invented a mutable alias without memory: %+v", got) } } func TestDecomposeUsesRememberedAliasesAcrossWordings(t *testing.T) { lexicon := TaskLexicon{ ObjectAliases: []TaskLexiconEntry{{Alias: "бренд", Value: "BRAND"}, {Alias: "марку", Value: "BRAND"}}, SourceAliases: []TaskLexiconEntry{{Alias: "wb", Value: "WILDBERRIES"}, {Alias: "wildberries", Value: "WILDBERRIES"}}, } for _, message := range []string{"найди на WB бренд marka-wear", "поищи марку marka-wear в Wildberries"} { got := DecomposeTaskWithLexicon(message, lexicon) if got.Intent != TaskIntentSearch || got.Object != "BRAND" || got.Source != "WILDBERRIES" || got.Target != "marka-wear" || !got.NeedsResearch { t.Fatalf("remembered aliases failed for %q: %+v", message, got) } } } func TestTaskLexiconFromHistoryFeedsSearchQueryWithoutCoreAlias(t *testing.T) { history := []interaction.HumanUtterance{{Content: "запомни: WB — это Wildberries, источник для поиска"}} lexicon := TaskLexiconFromHistory(history) if got := SearchQueryWithLexicon("найди на WB бренд marka-wear", lexicon); got != "на WB бренд marka-wear wildberries" { t.Fatalf("learned source was not applied to search: %q", got) } } func TestSearchQueryWithoutLearnedSourceOmitsImperative(t *testing.T) { if got := SearchQueryWithLexicon("найди на WB бренд marka-wear", TaskLexicon{}); got != "на WB бренд marka-wear" { t.Fatalf("search imperative leaked into external query: %q", got) } } func TestDecomposeCreativeRequestDoesNotInventResearch(t *testing.T) { lexicon := TaskLexicon{ObjectAliases: []TaskLexiconEntry{{Alias: "медвед", Value: "IMAGE"}}} got := DecomposeTaskWithLexicon("нарисуй бурого медведя", lexicon) if got.Intent != TaskIntentCreate || got.Object != "IMAGE" || got.Target != "бурого медведя" || got.NeedsResearch { t.Fatalf("unexpected creative decomposition: %+v", got) } } 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 name := PreferredAssistantName("назови доступные модели"); name != "" { t.Fatalf("model inventory request was mistaken for a name: %q", name) } if result := BuildDeterministicCandidate("назови доступные модели", "", time.Unix(10, 0).UnixMilli()); result.PrimarySpeechAct == interaction.SpeechActPreference { t.Fatalf("model inventory request was mistaken for a preference: %+v", result) } if !IsIdentityInquiry("ты кто?") || !IsIdentityInquiry("как тебя зовут") { t.Fatal("identity variants should be recognized") } if !IsExplicitMemoryInstruction("запомни: сокращение — это источник для поиска") { t.Fatal("explicit memory instruction was not recognized") } if IsExplicitMemoryInstruction("что ты помнишь обо мне?") { t.Fatal("memory inventory question must not be treated as an instruction") } } 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 TestRememberedPreferenceIsNotRenderedAsIdentity(t *testing.T) { result := BuildDeterministicCandidate("запомни: мне нравится красный цвет", "", time.Unix(10, 0).UnixMilli()) if result.PrimarySpeechAct != interaction.SpeechActPreference { t.Fatalf("expected remembered preference, got %+v", result) } if got := semanticReply("запомни: мне нравится красный цвет", SemanticResult{Interpretation: interaction.UtteranceInterpretation{SpeechAct: result.PrimarySpeechAct}}, nil); got != "Хорошо, учту это дальше." { t.Fatalf("unexpected preference reply %q", got) } } func TestStableAcknowledgementsDoNotBecomeTravelRequests(t *testing.T) { for _, message := range []string{"Хорошо", "Ладно", "Понял"} { candidate := BuildDeterministicCandidate(message, "", time.Unix(10, 0).UnixMilli()) if candidate.PrimarySpeechAct != interaction.SpeechActConfirmation { t.Fatalf("%q: expected confirmation, got %s", message, candidate.PrimarySpeechAct) } if reply, stable := StableConversationalReply(message, candidate.PrimarySpeechAct); !stable || reply != "Хорошо." { t.Fatalf("%q: stable reply = %q, %t", message, reply, stable) } } } 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("creative request received a canned reply = %q", got) } if !IsCreativeRequest("расскажи сказку о колобке") { t.Fatal("story request was not recognized as creative") } if got := CreativeFallbackReply("расскажи сказку о колобке"); got != "" { t.Fatalf("creative request received canned content = %q", got) } for _, message := range []string{"нарисуй птицу", "нарисуй зеленую птицу", "нарисуй птицу в HD", "нарисуй коня"} { if got := CreativeDeterministicReply(message); got != "" { t.Fatalf("creative request %q received a canned reply = %q", message, 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 TestTimeDifferenceResearchQueryKeepsEntitiesAndAsksForMeasurement(t *testing.T) { got := TimeDifferenceResearchQuery("какая разница во времени между Москвой и Шанхаем?") if got != "какая разница во времени между Москвой и Шанхаем? сколько часов" { t.Fatalf("time difference query = %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") } }