Files
Assistant/modules/interaction/domain/chat_test.go

245 lines
11 KiB
Go
Raw Permalink 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 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 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 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")
}
}