Files
Assistant/modules/interaction/domain/semantics_test.go
m 8b9b841645
CI / test (push) Successful in 21s
CI / test (pull_request) Successful in 16s
DEV-000066 human response continuation semantics
2026-08-17 17:02:33 +03:00

152 lines
7.3 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 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 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(),
}
}