package domain import ( "strings" "testing" "time" "git.cin.su/m/Assistant/contracts/authority" "git.cin.su/m/Assistant/contracts/continuity" "git.cin.su/m/Assistant/contracts/identity" ) func TestInterpretHumanResponseCompletesSingleDeliveredReminder(t *testing.T) { result := interpretResponseForTest(t, "Готово", []ResponseCandidate{responseCandidate("a1", "p1", "позвонить врачу")}) if !result.Handled || result.Response.Operation != continuity.ResponseComplete || result.Response.Outcome != continuity.ResponseOutcomeUserReportedComplete || !result.Response.UserReportedNotVerified { t.Fatalf("unexpected complete response: %#v", result) } } func TestInterpretHumanResponseSnoozesSingleReminder(t *testing.T) { result := interpretResponseForTest(t, "Отложи на 10 минут", []ResponseCandidate{responseCandidate("a1", "p1", "проверить базу")}) if !result.Handled || result.Response.Operation != continuity.ResponseSnooze || result.Response.Outcome != continuity.ResponseOutcomeSnoozed { t.Fatalf("unexpected snooze response: %#v", result) } if result.Response.SnoozeUntilUnixMillis != time.Unix(100, 0).Add(10*time.Minute).UnixMilli() { t.Fatalf("unexpected snooze time: %d", result.Response.SnoozeUntilUnixMillis) } } func TestInterpretHumanResponseCancelsSingleReminder(t *testing.T) { result := interpretResponseForTest(t, "Отмени", []ResponseCandidate{responseCandidate("a1", "p1", "встретить курьера")}) if !result.Handled || result.Response.Operation != continuity.ResponseCancel || result.Response.Outcome != continuity.ResponseOutcomeCancelled { t.Fatalf("unexpected cancel response: %#v", result) } } func TestInterpretHumanResponseCancelsArmedProspectiveBeforeAttention(t *testing.T) { result := interpretResponseForTest(t, "Отмени встретить курьера", []ResponseCandidate{{ Prospective: continuity.ProspectiveItem{ID: "p1", OwnerPrincipalID: "principal-1", ScopeID: "scope-1", Summary: "встретить курьера", State: continuity.ProspectiveArmed, Revision: 1}, }}) if !result.Handled || result.Response.TargetKind != continuity.ResponseTargetProspectiveItem || result.Response.TargetProspectiveID != "p1" || result.Response.Outcome != continuity.ResponseOutcomeCancelled { t.Fatalf("unexpected prospective cancel response: %#v", result) } } func TestInterpretHumanResponseResolvesArmedProspectiveByNaturalSummaryReference(t *testing.T) { result := interpretResponseForTest(t, "Отмени встретить курьера", []ResponseCandidate{ {Prospective: continuity.ProspectiveItem{ID: "p1", OwnerPrincipalID: "principal-1", ScopeID: "scope-1", Summary: "позвонить врачу", State: continuity.ProspectiveArmed, Revision: 1}}, {Prospective: continuity.ProspectiveItem{ID: "p2", OwnerPrincipalID: "principal-1", ScopeID: "scope-1", Summary: "встретить курьера", State: continuity.ProspectiveArmed, Revision: 1}}, }) if !result.Handled || result.Response.TargetKind != continuity.ResponseTargetProspectiveItem || result.Response.TargetProspectiveID != "p2" || result.Response.Outcome != continuity.ResponseOutcomeCancelled { t.Fatalf("unexpected prospective natural target response: %#v", result) } } func TestInterpretHumanResponseResolvesSummaryReferenceBeyondFirstThreeCandidates(t *testing.T) { candidates := []ResponseCandidate{ responseCandidate("a1", "p1", "первое ожидание"), responseCandidate("a2", "p2", "второе ожидание"), responseCandidate("a3", "p3", "третье ожидание"), {Prospective: continuity.ProspectiveItem{ID: "p4", OwnerPrincipalID: "principal-1", ScopeID: "scope-1", Summary: "встретить курьера", State: continuity.ProspectiveArmed, Revision: 1}}, } result := interpretResponseForTest(t, "Отмени встретить курьера", candidates) if !result.Handled || result.Response.ResolutionState != continuity.ResponseResolved || result.Response.TargetProspectiveID != "p4" { t.Fatalf("expected target resolution beyond first three candidates, got %#v", result) } } func TestInterpretHumanResponseAmbiguousTargetAsksClarificationWithoutResolvedMutation(t *testing.T) { result := interpretResponseForTest(t, "Готово", []ResponseCandidate{responseCandidate("a1", "p1", "первое"), responseCandidate("a2", "p2", "второе")}) if !result.Handled || result.Response.ResolutionState != continuity.ResponseAmbiguous || result.Response.Outcome != continuity.ResponseOutcomeClarificationNeeded || !strings.Contains(result.Reply, "несколько") { t.Fatalf("expected ambiguity clarification, got %#v", result) } } func TestInterpretHumanResponseResolvesAmbiguousTargetByNaturalSummaryReference(t *testing.T) { result := interpretResponseForTest(t, "Готово, проверить базу сделано", []ResponseCandidate{responseCandidate("a1", "p1", "позвонить врачу"), responseCandidate("a2", "p2", "проверить базу")}) if !result.Handled || result.Response.ResolutionState != continuity.ResponseResolved || result.Response.TargetAttentionID != "a2" { t.Fatalf("expected natural summary target resolution, got %#v", result) } } func TestInterpretHumanResponseNoTargetAsksClarification(t *testing.T) { result := interpretResponseForTest(t, "Готово", nil) if !result.Handled || result.Response.ResolutionState != continuity.ResponseNoTarget || result.Response.Outcome != continuity.ResponseOutcomeClarificationNeeded { t.Fatalf("expected no-target clarification, got %#v", result) } } func TestInterpretHumanResponseContinuesAgentDelegationAttention(t *testing.T) { result := interpretResponseForTest(t, "Продолжай", []ResponseCandidate{{ Attention: continuity.AttentionItem{ ID: "attention-1", OwnerPrincipalID: "principal-1", ScopeID: "scope-1", SourceKind: continuity.AttentionSourceAgentDelegation, SourceExternalRef: "work:delegation:delegation-1", State: continuity.AttentionDelivered, Summary: "нужен твой выбор по задаче coding-agent", Revision: 1, }, }}) if !result.Handled || result.Response.Operation != continuity.ResponseContinue || result.Response.Outcome != continuity.ResponseOutcomeContinued || result.Response.TargetExternalRef != "work:delegation:delegation-1" { t.Fatalf("unexpected delegation continue response: %#v", result) } } func TestInterpretHumanResponseRejectsAgentDelegationAttention(t *testing.T) { result := interpretResponseForTest(t, "Нет, нужен другой вариант", []ResponseCandidate{{ Attention: continuity.AttentionItem{ ID: "attention-1", OwnerPrincipalID: "principal-1", ScopeID: "scope-1", SourceKind: continuity.AttentionSourceAgentDelegation, SourceExternalRef: "work:delegation:delegation-1", State: continuity.AttentionDelivered, Summary: "нужен твой выбор по задаче coding-agent", Revision: 1, }, }}) if !result.Handled || result.Response.Operation != continuity.ResponseReject || result.Response.Outcome != continuity.ResponseOutcomeRejected { t.Fatalf("unexpected delegation reject response: %#v", result) } } func interpretResponseForTest(t *testing.T, message string, candidates []ResponseCandidate) continuity.HumanResponseResult { t.Helper() result, err := InterpretHumanResponse(continuity.HumanResponseRequest{ SourceUtteranceID: "utterance-1", SourceIntentID: "intent-1", Message: message, RequestedAtUnixMillis: time.Unix(100, 0).UnixMilli(), }, identity.PrincipalID("principal-1"), authority.ScopeID("scope-1"), continuity.HumanResponseID("response-1"), candidates, time.Unix(100, 0)) if err != nil { t.Fatal(err) } return result } func responseCandidate(attentionID continuity.AttentionItemID, prospectiveID continuity.ProspectiveItemID, summary string) ResponseCandidate { return ResponseCandidate{ Attention: continuity.AttentionItem{ ID: attentionID, OwnerPrincipalID: "principal-1", ScopeID: "scope-1", SourceKind: continuity.AttentionSourceProspectiveReminder, SourceProspectiveID: prospectiveID, State: continuity.AttentionDelivered, Summary: summary, Revision: 1, }, Prospective: continuity.ProspectiveItem{ ID: prospectiveID, OwnerPrincipalID: "principal-1", ScopeID: "scope-1", Summary: summary, State: continuity.ProspectiveDelivered, Revision: 1, }, } }