242 lines
11 KiB
Go
242 lines
11 KiB
Go
package dialogue
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"ai-operator/internal/ai"
|
|
"ai-operator/internal/call"
|
|
"ai-operator/internal/dialogue/state"
|
|
"ai-operator/internal/tools"
|
|
)
|
|
|
|
func TestOrchestratorToolFlow(t *testing.T) {
|
|
o := NewMemoryOrchestrator(nil)
|
|
_, err := o.StartCall(context.Background(), call.CallSession{CallID: "c1", AsteriskChannelID: "c1", CallerNumber: "+77771234567", Route: "test", StartedAt: time.Now()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s, ok := o.GetSession("c1")
|
|
if !ok || s.State != state.StateReadyToHelp {
|
|
t.Fatalf("state=%s ok=%t", s.State, ok)
|
|
}
|
|
denied := o.HandleToolCall(context.Background(), "c1", ai.ToolCall{ID: "t1", Name: tools.SearchKnowledgeBase})
|
|
if denied.Error != "knowledge_base_unavailable" {
|
|
t.Fatalf("expected knowledge_base_unavailable, got %+v", denied)
|
|
}
|
|
res := o.HandleToolCall(context.Background(), "c1", ai.ToolCall{ID: "t2", Name: tools.SetLanguage, Arguments: map[string]any{"language": "ru"}})
|
|
if res.Error != "" {
|
|
t.Fatalf("set language failed: %+v", res)
|
|
}
|
|
s, _ = o.GetSession("c1")
|
|
if s.State != state.StateReadyToHelp {
|
|
t.Fatalf("state=%s", s.State)
|
|
}
|
|
denied = o.HandleToolCall(context.Background(), "c1", ai.ToolCall{ID: "t3", Name: tools.SearchKnowledgeBase})
|
|
if denied.Error != "knowledge_base_unavailable" {
|
|
t.Fatalf("expected knowledge_base_unavailable, got %+v", denied)
|
|
}
|
|
res = o.HandleToolCall(context.Background(), "c1", ai.ToolCall{ID: "t4", Name: tools.SetRegion, Arguments: map[string]any{"region_code": "almaty_city"}})
|
|
if res.Error != "" {
|
|
t.Fatalf("set region failed: %+v", res)
|
|
}
|
|
s, _ = o.GetSession("c1")
|
|
if s.State != state.StateReadyToHelp {
|
|
t.Fatalf("state=%s", s.State)
|
|
}
|
|
res = o.HandleToolCall(context.Background(), "c1", ai.ToolCall{ID: "t5", Name: tools.SearchKnowledgeBase})
|
|
if res.Error != "knowledge_base_unavailable" {
|
|
t.Fatalf("expected knowledge_base_unavailable, got %+v", res)
|
|
}
|
|
res = o.HandleToolCall(context.Background(), "c1", ai.ToolCall{ID: "t6", Name: tools.RequestHumanHandoff})
|
|
if res.Error != "" {
|
|
t.Fatalf("handoff failed: %+v", res)
|
|
}
|
|
s, _ = o.GetSession("c1")
|
|
if s.State != state.StateHandoff {
|
|
t.Fatalf("state=%s", s.State)
|
|
}
|
|
res = o.HandleToolCall(context.Background(), "c1", ai.ToolCall{ID: "t7", Name: tools.EndCall})
|
|
if res.Error != "" {
|
|
t.Fatalf("end failed: %+v", res)
|
|
}
|
|
}
|
|
|
|
func TestStrictLanguageAndRegion(t *testing.T) {
|
|
o := NewMemoryOrchestrator(nil)
|
|
_, _ = o.StartCall(context.Background(), call.CallSession{CallID: "c1", AsteriskChannelID: "c1"})
|
|
if res := o.HandleToolCall(context.Background(), "c1", ai.ToolCall{ID: "x", Name: tools.SetLanguage, Arguments: map[string]any{"language": "en"}}); res.Error != "invalid_language" {
|
|
t.Fatalf("expected invalid language, got %+v", res)
|
|
}
|
|
if res := o.HandleToolCall(context.Background(), "c1", ai.ToolCall{ID: "x", Name: tools.SetRegion, Arguments: map[string]any{"region_code": "almaty_city"}}); res.Error != "" {
|
|
t.Fatalf("expected region to be accepted before explicit language, got %+v", res)
|
|
}
|
|
}
|
|
|
|
func TestEndCallCleanupAndConcurrency(t *testing.T) {
|
|
o := NewMemoryOrchestrator(nil)
|
|
for i := 0; i < 10; i++ {
|
|
_, _ = o.StartCall(context.Background(), call.CallSession{CallID: string(rune('a' + i)), AsteriskChannelID: "x"})
|
|
}
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 10; i++ {
|
|
id := string(rune('a' + i))
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
_, _ = o.GetSession(id)
|
|
_ = o.EndCall(context.Background(), id, "test")
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
if o.Count() != 0 {
|
|
t.Fatalf("count=%d", o.Count())
|
|
}
|
|
}
|
|
|
|
func TestHandleUserTextLanguageSelection(t *testing.T) {
|
|
o := NewMemoryOrchestrator(nil)
|
|
_, _ = o.StartCall(context.Background(), call.CallSession{CallID: "l1", AsteriskChannelID: "l1"})
|
|
res, err := o.HandleUserText(context.Background(), "l1", "русский")
|
|
if err != nil || !res.Applied || res.Language != state.LanguageRU || res.State != state.StateReadyToHelp {
|
|
t.Fatalf("ru selection: res=%+v err=%v", res, err)
|
|
}
|
|
o = NewMemoryOrchestrator(nil)
|
|
_, _ = o.StartCall(context.Background(), call.CallSession{CallID: "l2", AsteriskChannelID: "l2"})
|
|
res, err = o.HandleUserText(context.Background(), "l2", "қазақша")
|
|
if err != nil || !res.Applied || res.Language != state.LanguageKK || res.State != state.StateReadyToHelp {
|
|
t.Fatalf("kk selection: res=%+v err=%v", res, err)
|
|
}
|
|
o = NewMemoryOrchestrator(nil)
|
|
_, _ = o.StartCall(context.Background(), call.CallSession{CallID: "l3", AsteriskChannelID: "l3"})
|
|
res, _ = o.HandleUserText(context.Background(), "l3", "какой у меня тариф?")
|
|
if res.Applied || res.State != state.StateReadyToHelp || res.Language != state.LanguageRU {
|
|
t.Fatalf("business should auto-detect ru without IVR: %+v", res)
|
|
}
|
|
res, _ = o.HandleUserText(context.Background(), "l3", "русский или қазақша?")
|
|
if res.Applied || res.State != state.StateReadyToHelp {
|
|
t.Fatalf("ambiguous: %+v", res)
|
|
}
|
|
}
|
|
|
|
func TestSetLanguageNaturalVariantsAndTranscript(t *testing.T) {
|
|
o := NewMemoryOrchestrator(nil)
|
|
_, _ = o.StartCall(context.Background(), call.CallSession{CallID: "v1", AsteriskChannelID: "v1"})
|
|
for _, value := range []string{"русский", "russian", "қазақша", "kazakh"} {
|
|
o2 := NewMemoryOrchestrator(nil)
|
|
_, _ = o2.StartCall(context.Background(), call.CallSession{CallID: value, AsteriskChannelID: value})
|
|
res := o2.HandleToolCall(context.Background(), value, ai.ToolCall{ID: "t", Name: tools.SetLanguage, Arguments: map[string]any{"language": value}})
|
|
if res.Error != "" {
|
|
t.Fatalf("%s failed: %+v", value, res)
|
|
}
|
|
}
|
|
bad := o.HandleToolCall(context.Background(), "v1", ai.ToolCall{ID: "bad", Name: tools.SetLanguage, Arguments: map[string]any{"language": "english"}})
|
|
if bad.Error != "invalid_language" {
|
|
t.Fatalf("expected invalid_language, got %+v", bad)
|
|
}
|
|
o = NewMemoryOrchestrator(nil)
|
|
_, _ = o.StartCall(context.Background(), call.CallSession{CallID: "tr", AsteriskChannelID: "tr"})
|
|
if err := o.HandleVoiceEvent(context.Background(), "tr", ai.VoiceEvent{Type: ai.VoiceEventUserTranscriptDone, Text: "русский"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s, _ := o.GetSession("tr")
|
|
if s.Language != state.LanguageRU || s.State != state.StateReadyToHelp {
|
|
t.Fatalf("transcript not applied: %+v", s)
|
|
}
|
|
if err := o.HandleVoiceEvent(context.Background(), "tr", ai.VoiceEvent{Type: ai.VoiceEventAssistantTranscriptDelta, Text: "қазақша"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s, _ = o.GetSession("tr")
|
|
if s.Language != state.LanguageRU {
|
|
t.Fatal("assistant transcript changed language")
|
|
}
|
|
}
|
|
|
|
func TestLanguageChangeAfterReadyPreservesRegion(t *testing.T) {
|
|
o := NewMemoryOrchestrator(nil)
|
|
_, _ = o.StartCall(context.Background(), call.CallSession{CallID: "r", AsteriskChannelID: "r"})
|
|
_, _ = o.HandleUserText(context.Background(), "r", "русский")
|
|
_ = o.HandleToolCall(context.Background(), "r", ai.ToolCall{ID: "reg", Name: tools.SetRegion, Arguments: map[string]any{"region_code": "almaty_city"}})
|
|
res, _ := o.HandleUserText(context.Background(), "r", "перейдите на казахский")
|
|
if !res.Applied || res.Language != state.LanguageKK || res.RegionCode != "almaty_city" || res.State != state.StateReadyToHelp {
|
|
t.Fatalf("switch failed: %+v", res)
|
|
}
|
|
res, _ = o.HandleUserText(context.Background(), "r", "русский клиент спрашивает про тариф")
|
|
if res.Applied || res.Language != state.LanguageKK {
|
|
t.Fatalf("accidental switch: %+v", res)
|
|
}
|
|
}
|
|
|
|
func TestHandleUserTextRegionSelection(t *testing.T) {
|
|
o := NewMemoryOrchestrator(nil)
|
|
_, _ = o.StartCall(context.Background(), call.CallSession{CallID: "reg1", AsteriskChannelID: "reg1"})
|
|
res, _ := o.HandleUserText(context.Background(), "reg1", "Астана")
|
|
if !res.Applied || res.State != state.StateReadyToHelp || res.RegionCode != "astana_city" {
|
|
t.Fatalf("region should be usable without explicit language: %+v", res)
|
|
}
|
|
res, _ = o.HandleUserText(context.Background(), "reg1", "русский")
|
|
if res.State != state.StateReadyToHelp {
|
|
t.Fatalf("language not selected: %+v", res)
|
|
}
|
|
}
|
|
|
|
func TestAlmatyPendingClarification(t *testing.T) {
|
|
o := NewMemoryOrchestrator(nil)
|
|
_, _ = o.StartCall(context.Background(), call.CallSession{CallID: "alm", AsteriskChannelID: "alm"})
|
|
_, _ = o.HandleUserText(context.Background(), "alm", "русский")
|
|
res, _ := o.HandleUserText(context.Background(), "alm", "Алматы")
|
|
if res.Applied || !res.NeedsClarification || res.State != state.StateReadyToHelp || res.MessageKey != "region.almaty_clarify" {
|
|
t.Fatalf("expected almaty clarification: %+v", res)
|
|
}
|
|
s, _ := o.GetSession("alm")
|
|
if s.Region.Status != state.RegionPendingClarification || s.Metadata[state.PendingRegionCandidatesMetadataKey] == "" {
|
|
t.Fatalf("pending not stored: %+v", s)
|
|
}
|
|
res, _ = o.HandleUserText(context.Background(), "alm", "область")
|
|
if !res.Applied || res.RegionCode != "almaty_region" || res.State != state.StateReadyToHelp {
|
|
t.Fatalf("oblast clarification failed: %+v", res)
|
|
}
|
|
kb := o.HandleToolCall(context.Background(), "alm", ai.ToolCall{ID: "kb", Name: tools.SearchKnowledgeBase})
|
|
if kb.Error != "knowledge_base_unavailable" {
|
|
t.Fatalf("search after region should reach kb unavailable without service: %+v", kb)
|
|
}
|
|
}
|
|
|
|
func TestSetRegionNaturalVariantsAndDisabled(t *testing.T) {
|
|
o := NewMemoryOrchestrator(nil)
|
|
_, _ = o.StartCall(context.Background(), call.CallSession{CallID: "tool-region", AsteriskChannelID: "tool-region"})
|
|
_, _ = o.HandleUserText(context.Background(), "tool-region", "русский")
|
|
res := o.HandleToolCall(context.Background(), "tool-region", ai.ToolCall{ID: "r", Name: tools.SetRegion, Arguments: map[string]any{"region": "Шымкент"}})
|
|
if res.Error != "" {
|
|
t.Fatalf("natural set_region failed: %+v", res)
|
|
}
|
|
s, _ := o.GetSession("tool-region")
|
|
if s.Region.Code != "shymkent_city" || s.State != state.StateReadyToHelp {
|
|
t.Fatalf("bad region session: %+v", s)
|
|
}
|
|
o = NewMemoryOrchestrator(nil)
|
|
_, _ = o.StartCall(context.Background(), call.CallSession{CallID: "disabled-region", AsteriskChannelID: "disabled-region"})
|
|
_, _ = o.HandleUserText(context.Background(), "disabled-region", "русский")
|
|
res = o.HandleToolCall(context.Background(), "disabled-region", ai.ToolCall{ID: "b", Name: tools.SetRegion, Arguments: map[string]any{"region": "Байконур"}})
|
|
if res.Error != "disabled_region" {
|
|
t.Fatalf("expected disabled_region, got %+v", res)
|
|
}
|
|
}
|
|
|
|
func TestRegionChangeAfterReadyPreservesLanguage(t *testing.T) {
|
|
o := NewMemoryOrchestrator(nil)
|
|
_, _ = o.StartCall(context.Background(), call.CallSession{CallID: "rch", AsteriskChannelID: "rch"})
|
|
_, _ = o.HandleUserText(context.Background(), "rch", "русский")
|
|
_, _ = o.HandleUserText(context.Background(), "rch", "Астана")
|
|
res, _ := o.HandleUserText(context.Background(), "rch", "сменить регион на Шымкент")
|
|
if !res.Applied || res.RegionCode != "shymkent_city" || res.Language != state.LanguageRU || res.State != state.StateReadyToHelp {
|
|
t.Fatalf("switch region failed: %+v", res)
|
|
}
|
|
res, _ = o.HandleUserText(context.Background(), "rch", "Алматы тарифы")
|
|
if res.Applied || res.RegionCode != "shymkent_city" {
|
|
t.Fatalf("accidental region switch: %+v", res)
|
|
}
|
|
}
|