452 lines
24 KiB
Go
452 lines
24 KiB
Go
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 TestInterpretUtteranceNamingPreferenceIsAccepted(t *testing.T) {
|
||
result := interpretForTest(t, "я буду называть тебя Дед, запомни ты дед", nil, "")
|
||
if result.Interpretation.SpeechAct != interaction.SpeechActPreference {
|
||
t.Fatalf("expected preference, got %s", result.Interpretation.SpeechAct)
|
||
}
|
||
if result.Reply != "Хорошо, зови меня Дед." {
|
||
t.Fatalf("unexpected preference reply: %q", result.Reply)
|
||
}
|
||
}
|
||
|
||
func TestInterpretUtteranceThanksHasNaturalConversationalReply(t *testing.T) {
|
||
result := interpretForTest(t, "спасибо", nil, "")
|
||
if result.Interpretation.SpeechAct != interaction.SpeechActConversational {
|
||
t.Fatalf("expected conversational act, got %s", result.Interpretation.SpeechAct)
|
||
}
|
||
if result.Reply != "Пожалуйста." {
|
||
t.Fatalf("unexpected thanks reply: %q", result.Reply)
|
||
}
|
||
}
|
||
|
||
func TestStableConversationalReplySurvivesCognitiveRewrite(t *testing.T) {
|
||
reply, stable := StableConversationalReply("спасибо", interaction.SpeechActConversational)
|
||
if !stable || reply != "Пожалуйста." {
|
||
t.Fatalf("unexpected stable conversational reply: stable=%t reply=%q", stable, reply)
|
||
}
|
||
}
|
||
|
||
func TestStripAssistantVocative(t *testing.T) {
|
||
if got := StripAssistantVocative("Дед, какая погода в Сочи?"); got != "какая погода в Сочи?" {
|
||
t.Fatalf("unexpected cleaned query: %q", got)
|
||
}
|
||
if got := StripAssistantVocative("домовой: найди ресторан"); got != "найди ресторан" {
|
||
t.Fatalf("unexpected cleaned query: %q", got)
|
||
}
|
||
}
|
||
|
||
func TestIndependentRequestParts(t *testing.T) {
|
||
parts := IndependentRequestParts("Дед, узнай погоду в Сочи и найди ресторан")
|
||
if len(parts) != 2 || parts[0] != "узнай погоду в Сочи" || parts[1] != "найди ресторан" {
|
||
t.Fatalf("unexpected request parts: %#v", parts)
|
||
}
|
||
}
|
||
|
||
func TestLocalSearchClarification(t *testing.T) {
|
||
if got := LocalSearchClarification("найди ближайший ресторан"); got == "" {
|
||
t.Fatal("proximity search without place must ask for location")
|
||
}
|
||
if got := LocalSearchClarification("найди ближайший ресторан в Сочи"); got != "" {
|
||
t.Fatalf("explicit place must proceed, got %q", got)
|
||
}
|
||
}
|
||
|
||
func TestTicketSearchDestinationUsesRequestDestination(t *testing.T) {
|
||
if got := TicketSearchDestination("найди билеты до Ялты сегодня"); got != "Ялты" {
|
||
t.Fatalf("ticket destination = %q", got)
|
||
}
|
||
if got := TicketSearchDestination("найди билеты"); got != "" {
|
||
t.Fatalf("destination invented without one: %q", got)
|
||
}
|
||
}
|
||
|
||
func TestContinueLocalSearch(t *testing.T) {
|
||
history := []interaction.HumanUtterance{{Content: "Найди ближайший ресторан"}}
|
||
if got := ContinueLocalSearch("в Сочи", history); got != "Найди ближайший ресторан в Сочи" {
|
||
t.Fatalf("unexpected continuation: %q", got)
|
||
}
|
||
if got := ContinueLocalSearch("в Сочи", nil); got != "" {
|
||
t.Fatalf("location must not create a goal without conversation context: %q", got)
|
||
}
|
||
}
|
||
|
||
func TestContinueTravelOrigin(t *testing.T) {
|
||
history := []interaction.HumanUtterance{{Content: "Как можно добраться до Сочи?"}}
|
||
if got := ContinueTravelOrigin("я в Казани", history); got != "Как можно добраться до Сочи из Казани" {
|
||
t.Fatalf("unexpected travel continuation: %q", got)
|
||
}
|
||
if got := ContinueTravelOrigin("я в Казани", nil); got != "" {
|
||
t.Fatalf("location must not create a travel task without context: %q", got)
|
||
}
|
||
if got := ContinueTravelOrigin("Москва", history); got != "Как можно добраться до Сочи из города Москва" {
|
||
t.Fatalf("bare city should complete travel origin: %q", got)
|
||
}
|
||
}
|
||
|
||
func TestContinueTravelDate(t *testing.T) {
|
||
history := []interaction.HumanUtterance{{Content: "Найди билеты до Ялты", ReceivedAtUnixMillis: 100}}
|
||
if got := ContinueTravelDate("сегодня", history); got != "Найди билеты до Ялты сегодня" {
|
||
t.Fatalf("date answer did not complete ticket search: %q", got)
|
||
}
|
||
if got := ContinueTravelDate("завтра", []interaction.HumanUtterance{{Content: "Найди билеты до Ялты сегодня", ReceivedAtUnixMillis: 100}}); got != "" {
|
||
t.Fatalf("dated ticket search must not be continued again: %q", got)
|
||
}
|
||
if got := ContinueTravelDate("сегодня", []interaction.HumanUtterance{{Content: "Какая погода в Ялте?", ReceivedAtUnixMillis: 100}}); got != "" {
|
||
t.Fatalf("date answer must not create travel work without travel context: %q", got)
|
||
}
|
||
}
|
||
|
||
func TestTravelSearchClarification(t *testing.T) {
|
||
if got := TravelSearchClarification("Найди билеты", nil); got == "" {
|
||
t.Fatal("underspecified travel request must ask for missing route and date")
|
||
}
|
||
if got := TravelSearchClarification("Покажи ближайшие рейсы до Сочи", nil); got != "Из какого города вылетать?" {
|
||
t.Fatalf("nearest-flight request should default date to now, got %q", got)
|
||
}
|
||
if got := TravelSearchClarification("Найди билеты из Москвы в Сочи на 12 сентября", nil); got != "" {
|
||
t.Fatalf("complete travel request must proceed, got %q", got)
|
||
}
|
||
history := []interaction.HumanUtterance{{Content: "Найди билеты"}}
|
||
if got := TravelSearchClarification("а куда летим?", history); got == "" {
|
||
t.Fatal("follow-up must remain attached to the travel goal")
|
||
}
|
||
if got := TravelSearchClarification("какая погода в Сочи?", history); got != "" {
|
||
t.Fatalf("unrelated question must not inherit travel goal, got %q", got)
|
||
}
|
||
for _, message := range []string{"нарисуй птицу", "расскажи сказку", "ты кто?", "выведи свою память"} {
|
||
if got := TravelSearchClarification(message, history); got != "" {
|
||
t.Fatalf("non-travel request %q inherited travel goal, got %q", message, got)
|
||
}
|
||
}
|
||
if got := TravelSearchClarification("Как можно добраться до Сочи?", nil); got != "Из какого города выезжать в Сочи? Подберу самолёт, поезд, автобус или машину." {
|
||
t.Fatalf("general travel question should ask departure city, got %q", got)
|
||
}
|
||
if got := TravelSearchClarification("Как можно добраться до Сочи из Казани", nil); got != "" {
|
||
t.Fatalf("complete general travel route must proceed, got %q", got)
|
||
}
|
||
contextHistory := []interaction.HumanUtterance{{Content: "Как можно добраться до Сочи?"}}
|
||
if got := TravelSearchClarification("Найди билеты, какой ближайший самолёт", contextHistory); got != "Из какого города вылетать в Сочи?" {
|
||
t.Fatalf("travel destination should be inherited from context, got %q", got)
|
||
}
|
||
noisyContext := []interaction.HumanUtterance{
|
||
{Content: "Как можно добраться до Сочи?"},
|
||
{Content: "Какая погода в Питере?"},
|
||
}
|
||
if got := TravelSearchClarification("Найди билеты", noisyContext); got != "Из какого города вылетать в Сочи и на какую дату?" {
|
||
t.Fatalf("unrelated weather history must not replace travel destination, got %q", got)
|
||
}
|
||
}
|
||
|
||
func TestTravelSearchQueryCarriesNearestFlightDate(t *testing.T) {
|
||
now := time.Date(2026, time.August, 25, 18, 0, 0, 0, time.UTC)
|
||
if got := TravelSearchQuery("какой ближайший рейс из Москвы в Казань", now); got != "какой ближайший рейс из Москвы в Казань сегодня 25.08.2026" {
|
||
t.Fatalf("nearest flight query=%q", got)
|
||
}
|
||
if got := TravelSearchQuery("рейс из Москвы в Казань завтра", now); got != "рейс из Москвы в Казань завтра" {
|
||
t.Fatalf("explicit date was changed: %q", got)
|
||
}
|
||
}
|
||
|
||
func TestTravelResearchQueryUsesKnownOriginForDistance(t *testing.T) {
|
||
history := []interaction.HumanUtterance{{Content: "я сейчас в Москве"}}
|
||
got := TravelResearchQuery("сколько километров до Сочи?", history, time.Time{})
|
||
if got != "расстояние между Москве и Сочи в километрах по прямой и по дороге" {
|
||
t.Fatalf("distance query lost route context: %q", got)
|
||
}
|
||
}
|
||
|
||
func TestTravelResearchQueryCompilesLocalPlaceSearchWithoutSearchImperative(t *testing.T) {
|
||
query := TravelResearchQuery("найди ближайший ресторан в Сочи", nil, time.Time{})
|
||
if strings.Contains(query, "найди") || strings.Contains(query, "ближайший") || !strings.Contains(query, "сочи") || !strings.Contains(query, "адрес") {
|
||
t.Fatalf("local place query was not compiled semantically: %q", query)
|
||
}
|
||
}
|
||
|
||
func TestTravelRouteWithKnownOriginDoesNotAskForTicketDate(t *testing.T) {
|
||
history := []interaction.HumanUtterance{{Content: "я сейчас в Москве"}}
|
||
if got := TravelSearchClarification("как можно добраться до Сочи?", history); got != "" {
|
||
t.Fatalf("general route was incorrectly reduced to ticket clarification: %q", got)
|
||
}
|
||
got := TravelResearchQuery("как можно добраться до Сочи?", history, time.Time{})
|
||
if got != "способы добраться из Москве в Сочи: самолет поезд автобус автомобиль" {
|
||
t.Fatalf("general route query lost known route context: %q", got)
|
||
}
|
||
}
|
||
|
||
func TestTravelContextUsesNewestLocationRegardlessOfRepositoryOrder(t *testing.T) {
|
||
history := []interaction.HumanUtterance{
|
||
{Content: "я сейчас в Санкт-Петербурге", ReceivedAtUnixMillis: 200},
|
||
{Content: "я сейчас в Москве", ReceivedAtUnixMillis: 300},
|
||
{Content: "я сейчас в Казани", ReceivedAtUnixMillis: 100},
|
||
}
|
||
if got := TravelSearchClarification("какой ближайший рейс до Сочи?", history); got != "" {
|
||
t.Fatalf("newest remembered origin was not applied: %q", got)
|
||
}
|
||
if got := TravelResearchQuery("какой ближайший рейс до Сочи?", history, time.Time{}); !strings.Contains(got, "пункт отправления: Москве") {
|
||
t.Fatalf("newest origin missing from travel research query: %q", got)
|
||
}
|
||
}
|
||
|
||
func TestNearestPlaneUsesTravelContextAndCurrentDay(t *testing.T) {
|
||
now := time.Date(2026, time.August, 25, 18, 0, 0, 0, time.UTC)
|
||
history := []interaction.HumanUtterance{{Content: "я сейчас в Москве", ReceivedAtUnixMillis: 100}}
|
||
query := TravelResearchQuery("найди ближайший самолет до Казани", history, now)
|
||
if query != "ближайшие авиарейсы Москве Казани сегодня 25.08.2026" {
|
||
t.Fatalf("nearest plane search formulation lost current context: %q", query)
|
||
}
|
||
if got := TravelSearchClarification("найди ближайший самолет до Казани", history); got != "" {
|
||
t.Fatalf("known origin and nearest-time meaning should proceed, got %q", got)
|
||
}
|
||
}
|
||
|
||
func TestTravelResearchQueryUsesKnownOriginForTrainSchedule(t *testing.T) {
|
||
now := time.Date(2026, time.August, 26, 9, 0, 0, 0, time.UTC)
|
||
history := []interaction.HumanUtterance{{Content: "я сейчас в Москве"}}
|
||
got := TravelResearchQuery("найди поезд до Петербурга", history, now)
|
||
want := "расписание поездов из Москве в Петербурга время отправления сегодня 26.08.2026"
|
||
if got != want {
|
||
t.Fatalf("train schedule query lost route context or primary outcome: %q", got)
|
||
}
|
||
}
|
||
|
||
func TestTravelResearchQueryNormalizesNearUnambiguousTravelTypo(t *testing.T) {
|
||
history := []interaction.HumanUtterance{{Content: "я сейчас в Москве"}}
|
||
got := TravelResearchQuery("найди сомалет до Казани", history, time.Time{})
|
||
if !strings.Contains(got, "самолёт") || !strings.Contains(got, "пункт отправления: Москве") {
|
||
t.Fatalf("travel typo was not normalized with remembered route context: %q", got)
|
||
}
|
||
}
|
||
|
||
func TestIndependentRequestPartsKeepsCoordinatedEntitiesTogether(t *testing.T) {
|
||
if got := IndependentRequestParts("какая разница во времени между Москвой и Шанхаем?"); len(got) != 1 {
|
||
t.Fatalf("coordinated entities became independent requests: %#v", got)
|
||
}
|
||
if got := IndependentRequestParts("проверь погоду в Казани и найди кафе рядом"); len(got) != 2 {
|
||
t.Fatalf("independent actions were not decomposed: %#v", got)
|
||
}
|
||
}
|
||
|
||
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 TestInterpretUtteranceEnglishFocusIntrospectionStaysLocal(t *testing.T) {
|
||
result := interpretForTest(t, "Какой сейчас Focus разговора?", activeFocus("assistant", "Assistant"), "")
|
||
if result.Reply != "Сейчас в фокусе: Assistant." || result.NeedsCognition || len(result.CapabilityHints) != 0 {
|
||
t.Fatalf("focus introspection escaped local route: %+v", result)
|
||
}
|
||
}
|
||
|
||
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 TestCarryForwardPreviousTaskPreservesRoutingForShortRetry(t *testing.T) {
|
||
current := SemanticResult{
|
||
Interpretation: interaction.UtteranceInterpretation{UtteranceID: "retry", SpeechAct: interaction.SpeechActAssertion},
|
||
Candidates: []interaction.SemanticCandidate{{ID: "current", UtteranceID: "retry"}},
|
||
}
|
||
previous := SemanticResult{
|
||
Interpretation: interaction.UtteranceInterpretation{
|
||
UtteranceID: "previous",
|
||
SpeechAct: interaction.SpeechActRequestToInvestigate,
|
||
RequestedOutcome: "узнать текущую погоду",
|
||
NeedsCognition: false,
|
||
Applicability: interaction.SemanticApplicable,
|
||
Clarification: interaction.ClarificationNotRequired,
|
||
RecordedAtUnixMillis: 100,
|
||
},
|
||
Candidates: []interaction.SemanticCandidate{{ID: "previous-candidate", PrimarySpeechAct: interaction.SpeechActRequestToInvestigate, CapabilityHints: []string{"WEB_RESEARCH"}}},
|
||
CapabilityHints: []string{"WEB_RESEARCH"},
|
||
}
|
||
result := CarryForwardPreviousTask(current, previous)
|
||
if result.Interpretation.SpeechAct != interaction.SpeechActRequestToInvestigate || result.CapabilityHints[0] != "WEB_RESEARCH" || result.Candidates[0].UtteranceID != "retry" {
|
||
t.Fatalf("previous routing was not carried forward: %#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(),
|
||
}
|
||
}
|