package call import ( "context" "testing" "time" "ai-operator/internal/asterisk/ari" ) type fakeActions struct{ answered, hungup, played []string } func (f *fakeActions) AnswerChannel(ctx context.Context, channelID string) error { f.answered = append(f.answered, channelID) return nil } func (f *fakeActions) HangupChannel(ctx context.Context, channelID string) error { f.hungup = append(f.hungup, channelID) return nil } func (f *fakeActions) GetChannel(ctx context.Context, channelID string) (*ari.ARIChannel, error) { return &ari.ARIChannel{ID: channelID}, nil } func (f *fakeActions) PlayChannel(ctx context.Context, channelID string, media string) error { f.played = append(f.played, channelID+" "+media) return nil } func startEvent(id string, args []string) ari.StasisStartEvent { return ari.StasisStartEvent{BaseEvent: ari.BaseEvent{Type: ari.EventStasisStart}, Args: args, Channel: ari.ARIChannel{ID: id, Caller: ari.ARICallerID{Number: "+77771234567"}}} } func TestManagerObserveOnly(t *testing.T) { fake := &fakeActions{} store := NewSessionStore() m := NewManager(DefaultManagerConfig(ManagerModeObserveOnly, time.Millisecond), fake, store, nil) if err := m.HandleEvent(context.Background(), startEvent("c1", []string{"test"})); err != nil { t.Fatal(err) } if len(fake.answered) != 0 || len(fake.hungup) != 0 { t.Fatal("observe-only controlled channel") } if store.Count() != 1 { t.Fatalf("count=%d", store.Count()) } } func TestManagerCallControlTestRoute(t *testing.T) { fake := &fakeActions{} m := NewManager(DefaultManagerConfig(ManagerModeCallControl, 5*time.Millisecond), fake, NewSessionStore(), nil) if err := m.HandleEvent(context.Background(), startEvent("c1", []string{"test"})); err != nil { t.Fatal(err) } if len(fake.answered) != 1 { t.Fatalf("answered=%v", fake.answered) } time.Sleep(20 * time.Millisecond) if len(fake.hungup) != 1 { t.Fatalf("hungup=%v", fake.hungup) } } func TestManagerPlaybackDiagnostic(t *testing.T) { fake := &fakeActions{} cfg := DefaultManagerConfig(ManagerModeCallControl, 5*time.Millisecond) cfg.MediaTestMode = "playback" m := NewManager(cfg, fake, NewSessionStore(), nil) if err := m.HandleEvent(context.Background(), startEvent("c1", []string{"test"})); err != nil { t.Fatal(err) } if len(fake.answered) != 1 { t.Fatalf("answered=%v", fake.answered) } if len(fake.played) != 1 || fake.played[0] != "c1 sound:hello-world" { t.Fatalf("played=%v", fake.played) } } func TestManagerRejectsProductionAndCleans(t *testing.T) { fake := &fakeActions{} store := NewSessionStore() m := NewManager(DefaultManagerConfig(ManagerModeCallControl, time.Millisecond), fake, store, nil) if err := m.HandleEvent(context.Background(), startEvent("c1", []string{"production"})); err != nil { t.Fatal(err) } if len(fake.answered) != 0 || len(fake.hungup) != 1 { t.Fatalf("answered=%v hungup=%v", fake.answered, fake.hungup) } _ = m.HandleEvent(context.Background(), ari.StasisEndEvent{BaseEvent: ari.BaseEvent{Type: ari.EventStasisEnd}, Channel: ari.ARIChannel{ID: "c1"}}) if store.Count() != 0 { t.Fatalf("count=%d", store.Count()) } _ = m.HandleEvent(context.Background(), startEvent("c2", []string{"test"})) _ = m.HandleEvent(context.Background(), ari.ChannelDestroyedEvent{BaseEvent: ari.BaseEvent{Type: ari.EventChannelDestroyed}, Channel: ari.ARIChannel{ID: "c2"}}) if store.Count() != 0 { t.Fatalf("count=%d", store.Count()) } } type fakeMediaStarter struct{ calls int } func (f *fakeMediaStarter) StartCallMedia(ctx context.Context, session *CallSession) (MediaClient, error) { f.calls++ session.MediaConnected = true return nil, nil } func TestManagerMediaIntegrationAndMediaChannelIgnored(t *testing.T) { fake := &fakeActions{} media := &fakeMediaStarter{} cfg := DefaultManagerConfig(ManagerModeCallControl, time.Millisecond) cfg.MediaEnabled = true cfg.MediaStarter = media m := NewManager(cfg, fake, NewSessionStore(), nil) if err := m.HandleEvent(context.Background(), startEvent("c1", []string{"test"})); err != nil { t.Fatal(err) } if media.calls != 1 { t.Fatalf("media calls=%d", media.calls) } if err := m.HandleEvent(context.Background(), startEvent("aiop-media-c1", []string{"test"})); err != nil { t.Fatal(err) } if m.Store().Count() != 1 { t.Fatalf("media channel treated as caller, count=%d", m.Store().Count()) } } func TestManagerDialogueIntegration(t *testing.T) { fake := &fakeActions{} started := false ended := false cfg := DefaultManagerConfig(ManagerModeCallControl, time.Millisecond) cfg.StartDialogue = func(ctx context.Context, session *CallSession) (string, error) { started = true if session.Route != "test" { t.Fatalf("route=%s", session.Route) } return "state prompt", nil } cfg.EndDialogue = func(ctx context.Context, callID string, reason string) error { ended = true return nil } m := NewManager(cfg, fake, NewSessionStore(), nil) if err := m.HandleEvent(context.Background(), startEvent("c1", []string{"test"})); err != nil { t.Fatal(err) } if !started { t.Fatal("dialogue not started") } _ = m.HandleEvent(context.Background(), ari.StasisEndEvent{BaseEvent: ari.BaseEvent{Type: ari.EventStasisEnd}, Channel: ari.ARIChannel{ID: "c1"}}) if !ended { t.Fatal("dialogue not ended") } } func TestManagerDoesNotStartDialogueForProduction(t *testing.T) { fake := &fakeActions{} started := false cfg := DefaultManagerConfig(ManagerModeCallControl, time.Millisecond) cfg.StartDialogue = func(ctx context.Context, session *CallSession) (string, error) { started = true return "", nil } m := NewManager(cfg, fake, NewSessionStore(), nil) if err := m.HandleEvent(context.Background(), startEvent("c1", []string{"production"})); err != nil { t.Fatal(err) } if started { t.Fatal("dialogue started for production route") } }