package domain import ( "strings" "testing" "time" "git.cin.su/m/Assistant/contracts/identity" "git.cin.su/m/Assistant/contracts/interaction" "git.cin.su/m/Assistant/contracts/situation" ) func TestInterpretUtteranceRecordsExplicitProjectFocus(t *testing.T) { result := interpretForTest(t, "Сейчас работаем с УПО.", nil, "") if result.ActiveFocus == nil || result.ActiveFocus.ProjectID != "upo" { t.Fatalf("expected explicit UPO focus, got %#v", result.ActiveFocus) } if result.Interpretation.SpeechAct != interaction.SpeechActAssertion { t.Fatalf("expected assertion, got %s", result.Interpretation.SpeechAct) } if result.Projects[0].Provenance != situation.ProjectResolutionExplicit { t.Fatalf("expected explicit provenance, got %#v", result.Projects) } } func TestInterpretUtteranceAssertionIsNotVerifiedFact(t *testing.T) { result := interpretForTest(t, "На сервере сейчас свободно 300 ГБ.", nil, "") if result.Interpretation.SpeechAct != interaction.SpeechActAssertion { t.Fatalf("expected assertion, got %s", result.Interpretation.SpeechAct) } if result.Interpretation.AssertionVerification != "UNVERIFIED" || !strings.Contains(result.Reply, "не проверенный факт") { t.Fatalf("expected unverified human assertion, got %#v reply=%q", result.Interpretation, result.Reply) } } func TestInterpretUtteranceDirectiveRecordsOutcomeWithoutExecution(t *testing.T) { result := interpretForTest(t, "Поменяй базу на УПО1.", activeFocus("upo", "УПО"), "") if result.Interpretation.SpeechAct != interaction.SpeechActDirective { t.Fatalf("expected directive, got %s", result.Interpretation.SpeechAct) } if result.Interpretation.RequestedOutcome == "" || !strings.Contains(result.Reply, "Ничего не выполнено") { t.Fatalf("expected non-executed requested outcome, got %#v reply=%q", result.Interpretation, result.Reply) } } func TestInterpretUtteranceCorrectionIsAppendOnlySupersedes(t *testing.T) { result := interpretForTest(t, "Нет, используй УПО2.", activeFocus("upo", "УПО"), "utterance-previous") if result.Interpretation.SpeechAct != interaction.SpeechActCorrection { t.Fatalf("expected correction, got %s", result.Interpretation.SpeechAct) } if len(result.Relations) != 1 || result.Relations[0].Kind != interaction.RelationSupersedes || result.Relations[0].ToUtteranceID != "utterance-previous" { t.Fatalf("expected supersedes relation, got %#v", result.Relations) } } func TestInterpretUtteranceQuestionAboutCurrentWorkUsesFocus(t *testing.T) { result := interpretForTest(t, "Что мы сейчас делаем?", activeFocus("assistant", "Assistant"), "") if result.Interpretation.SpeechAct != interaction.SpeechActQuestion { t.Fatalf("expected question, got %s", result.Interpretation.SpeechAct) } if !strings.Contains(result.Reply, "Assistant") { t.Fatalf("expected focus-aware reply, got %q", result.Reply) } if result.NeedsCognition { t.Fatalf("focus question should be answered from canonical focus without cognition") } } func TestInterpretUtteranceInheritedFocusForThere(t *testing.T) { result := interpretForTest(t, "Добавь туда новую настройку.", activeFocus("assistant", "Assistant"), "") if len(result.Projects) != 1 || result.Projects[0].ProjectID != "assistant" || result.Projects[0].Provenance != situation.ProjectResolutionInheritedFocus { t.Fatalf("expected inherited Assistant focus, got %#v", result.Projects) } } func TestProjectFocusIsContextualEvidenceNotGlobalMode(t *testing.T) { explicit := interpretForTest(t, "Сейчас работаем с УПО.", nil, "") if explicit.ActiveFocus == nil || explicit.ActiveFocus.ProjectID != "upo" { t.Fatalf("expected explicit UPO focus, got %#v", explicit.ActiveFocus) } focusedQuestion := interpretForTest(t, "Почему тормозит база?", activeFocus("upo", "УПО"), "") if len(focusedQuestion.Projects) != 1 || focusedQuestion.Projects[0].ProjectID != "upo" || focusedQuestion.Projects[0].Provenance != situation.ProjectResolutionInheritedFocus { t.Fatalf("database follow-up can use previous project as contextual evidence, got %#v", focusedQuestion.Projects) } unrelated := interpretForTest(t, "Помоги ответить на письмо бухгалтеру.", activeFocus("upo", "УПО"), "") if len(unrelated.Projects) != 0 { t.Fatalf("unrelated mail intent must not inherit UPO focus, got %#v", unrelated.Projects) } returned := interpretForTest(t, "Вернемся к УПО.", activeFocus("assistant", "Assistant"), "") if returned.ActiveFocus == nil || returned.ActiveFocus.ProjectID != "upo" { t.Fatalf("expected explicit return to UPO, got %#v", returned.ActiveFocus) } } func TestInterpretUtteranceMultiProjectReferenceRoles(t *testing.T) { result := interpretForTest(t, "Посмотри, как это сделано в Assistant, и сделай аналогично в УПО.", nil, "") if len(result.Projects) != 2 { t.Fatalf("expected two project references, got %#v", result.Projects) } roles := map[situation.ProjectID]interaction.ProjectReferenceRole{} for _, ref := range result.Projects { roles[ref.ProjectID] = ref.Role } if roles["assistant"] != interaction.ProjectRoleReferenceSource || roles["upo"] != interaction.ProjectRolePrimary { t.Fatalf("unexpected roles: %#v", roles) } } func TestInterpretUtteranceAmbiguousDestructiveRequiresClarification(t *testing.T) { result := interpretForTest(t, "Удали туда старую базу.", activeFocus("assistant", "Assistant"), "") if result.Interpretation.Clarification != interaction.ClarificationRequired { t.Fatalf("expected clarification, got %#v", result.Interpretation) } if result.NeedsCognition { t.Fatalf("ambiguous destructive directive must not require cognition") } } func TestInterpretUtterancePunctuationFreeRussianQuestionStillRequestsBroaderInterpretation(t *testing.T) { result := interpretForTest(t, "ты кто", nil, "") if result.Interpretation.SpeechAct != interaction.SpeechActQuestion { t.Fatalf("expected question, got %#v", result.Interpretation) } if !result.NeedsCognition { t.Fatalf("expected cognition route for punctuation-free identity question, got %#v", result) } } func TestInterpretUtterancePunctuationFreeArithmeticStillRequestsCognition(t *testing.T) { result := interpretForTest(t, "сколько будет 2 + 2", nil, "") if result.Interpretation.SpeechAct != interaction.SpeechActQuestion { t.Fatalf("expected question, got %#v", result.Interpretation) } if !result.NeedsCognition { t.Fatalf("expected cognition route for punctuation-free arithmetic, got %#v", result) } } func TestInterpretUtteranceModalExplanationRequestDefersToCognition(t *testing.T) { for _, message := range []string{"можешь расказать об утре", "расскажи об утре", "про утро расскажешь"} { result := interpretForTest(t, message, nil, "") if result.Interpretation.SpeechAct != interaction.SpeechActQuestion && result.Interpretation.SpeechAct != interaction.SpeechActRequestToInvestigate { t.Fatalf("expected cognition-worthy interpretation for %q, got %#v", message, result.Interpretation) } if !result.NeedsCognition { t.Fatalf("expected cognition route for %q, got %#v", message, result) } } } func TestInterpretUtteranceNamedModelRequestIsDirectiveNeedingCognition(t *testing.T) { result := interpretForTest(t, "обратись к модели gemma4 и спроси сколько звезд на небе", nil, "") if result.Interpretation.SpeechAct != interaction.SpeechActDirective { t.Fatalf("expected directive, got %#v", result.Interpretation) } if !result.NeedsCognition { t.Fatalf("expected cognition route for named-model request, got %#v", result) } } func interpretForTest(t *testing.T, message string, previous *situation.ActiveProjectFocus, corrects interaction.HumanUtteranceID) SemanticResult { t.Helper() conversation := interaction.Conversation{ID: "conversation-1", ActorID: "actor-1", PrincipalID: "principal-1", Channel: interaction.ChannelWebChat, State: interaction.ConversationOpen} utterance, _, _, err := NewHumanUtterance(conversation, interaction.ChatMessageRequest{Message: message, CorrectsUtteranceID: corrects}, time.Unix(100, 0)) if err != nil { t.Fatal(err) } result, err := InterpretUtterance(utterance, projectRegistryForTest(), previous, time.Unix(100, 0)) if err != nil { t.Fatal(err) } return result } func projectRegistryForTest() []situation.ProjectDescriptor { return []situation.ProjectDescriptor{ {ID: "assistant", DisplayName: "Assistant", Aliases: []situation.ProjectAlias{"assistant", "ассистент", "домовой"}}, {ID: "upo", DisplayName: "УПО", Aliases: []situation.ProjectAlias{"упо", "upo"}}, } } func activeFocus(projectID situation.ProjectID, displayName string) *situation.ActiveProjectFocus { return &situation.ActiveProjectFocus{ ActorID: identity.ActorID("actor-1"), PrincipalID: identity.PrincipalID("principal-1"), ProjectID: projectID, ProjectDisplayName: displayName, Provenance: situation.ProjectResolutionExplicit, ConfidenceMillis: 980, SourceUtteranceID: "utterance-focus", UpdatedAtUnixMillis: time.Unix(90, 0).UnixMilli(), } }