package routing import ( "errors" "testing" "time" "aigw/internal/catalog" "aigw/internal/config" "aigw/internal/domain" "aigw/internal/providerhealth" ) func TestPlanHonorsPriorityAndProtocol(t *testing.T) { cfg := config.Config{ Providers: []config.ProviderConfig{ {ID: "openai-primary", Protocol: domain.ProtocolOpenAI, BaseURL: "https://one.test", APIKey: "one"}, {ID: "openai-fallback", Protocol: domain.ProtocolOpenAI, BaseURL: "https://two.test", APIKey: "two"}, {ID: "anthropic", Protocol: domain.ProtocolAnthropic, BaseURL: "https://three.test", APIKey: "three"}, }, Models: []config.ModelConfig{{ ID: "public/model", Routes: []config.RouteConfig{ {Provider: "openai-fallback", UpstreamModel: "fallback", Priority: 10, Weight: 1}, {Provider: "anthropic", UpstreamModel: "claude", Priority: 0, Weight: 1}, {Provider: "openai-primary", UpstreamModel: "primary", Priority: 0, Weight: 1}, }, }}, } router := New(catalog.New(cfg)) plan, err := router.Plan("public/model", domain.ProtocolOpenAI) if err != nil { t.Fatal(err) } if len(plan) != 2 || plan[0].Provider.ID != "openai-primary" || plan[1].Provider.ID != "openai-fallback" { t.Fatalf("unexpected plan: %+v", plan) } } func TestPlanSkipsOpenCircuitAndRecoversAfterCooldown(t *testing.T) { now := time.Date(2026, time.August, 6, 0, 0, 0, 0, time.UTC) health := providerhealth.New(providerhealth.Options{FailureThreshold: 3, OpenDuration: 30 * time.Second, Now: func() time.Time { return now }}) cfg := config.Config{ Providers: []config.ProviderConfig{ {ID: "primary", Protocol: domain.ProtocolOpenAI, BaseURL: "https://primary.test", APIKey: "one"}, {ID: "fallback", Protocol: domain.ProtocolOpenAI, BaseURL: "https://fallback.test", APIKey: "two"}, }, Models: []config.ModelConfig{{ID: "public/model", Routes: []config.RouteConfig{ {Provider: "primary", UpstreamModel: "model", Priority: 0, Weight: 1}, {Provider: "fallback", UpstreamModel: "model", Priority: 10, Weight: 1}, }}}, } for range 3 { health.Observe(providerhealth.RouteKey{ModelID: "public/model", ProviderID: "primary", WireAPI: "chat_completions"}, providerhealth.Observation{StatusCode: 503, Failed: true}) } router := New(catalog.New(cfg), health) plan, err := router.Plan("public/model", domain.ProtocolOpenAI) if err != nil || len(plan) != 1 || plan[0].Provider.ID != "fallback" { t.Fatalf("open primary was not skipped: plan=%+v err=%v", plan, err) } for range 3 { health.Observe(providerhealth.RouteKey{ModelID: "public/model", ProviderID: "fallback", WireAPI: "chat_completions"}, providerhealth.Observation{StatusCode: 503, Failed: true}) } if _, err := router.Plan("public/model", domain.ProtocolOpenAI); !errors.Is(err, ErrNoHealthyRoute) { t.Fatalf("Plan() error = %v, want ErrNoHealthyRoute", err) } now = now.Add(31 * time.Second) plan, err = router.Plan("public/model", domain.ProtocolOpenAI) if err != nil || len(plan) != 2 || plan[0].Provider.ID != "primary" { t.Fatalf("routes did not recover after cooldown: plan=%+v err=%v", plan, err) } } func TestPlanUsesWeightsForPrimarySelection(t *testing.T) { cfg := config.Config{ Providers: []config.ProviderConfig{ {ID: "one", Protocol: domain.ProtocolOpenAI, BaseURL: "https://one.test", APIKey: "one"}, {ID: "two", Protocol: domain.ProtocolOpenAI, BaseURL: "https://two.test", APIKey: "two"}, }, Models: []config.ModelConfig{{ID: "public/model", Routes: []config.RouteConfig{ {Provider: "one", UpstreamModel: "one", Weight: 3}, {Provider: "two", UpstreamModel: "two", Weight: 1}, }}}, } router := New(catalog.New(cfg)) counts := map[string]int{} for range 8 { plan, err := router.Plan("public/model", domain.ProtocolOpenAI) if err != nil { t.Fatal(err) } counts[plan[0].Provider.ID]++ } if counts["one"] != 6 || counts["two"] != 2 { t.Fatalf("unexpected weighted distribution: %+v", counts) } } func TestPlanSeparatesOpenAIWireAPIs(t *testing.T) { cfg := config.Config{ Providers: []config.ProviderConfig{ {ID: "chat", Protocol: domain.ProtocolOpenAI, WireAPI: "chat_completions", BaseURL: "https://chat.test", APIKey: "one"}, {ID: "responses", Protocol: domain.ProtocolOpenAI, WireAPI: "responses", BaseURL: "https://responses.test", APIKey: "two"}, }, Models: []config.ModelConfig{{ID: "public/model", Routes: []config.RouteConfig{ {Provider: "chat", UpstreamModel: "chat-model", Weight: 1}, {Provider: "responses", UpstreamModel: "responses-model", Weight: 1}, }}}, } router := New(catalog.New(cfg)) chat, err := router.Plan("public/model", domain.ProtocolOpenAI) if err != nil || len(chat) != 1 || chat[0].Provider.ID != "chat" { t.Fatalf("unexpected Chat plan: %+v err=%v", chat, err) } responses, err := router.Plan("public/model", domain.ProtocolOpenAIResponses) if err != nil || len(responses) != 1 || responses[0].Provider.ID != "responses" { t.Fatalf("unexpected Responses plan: %+v err=%v", responses, err) } } func TestPlanProviderPinsWithoutFallbackToOtherProviders(t *testing.T) { cfg := config.Config{ Providers: []config.ProviderConfig{ {ID: "primary", Protocol: domain.ProtocolOpenAI, BaseURL: "https://primary.test", APIKey: "one"}, {ID: "backup", Protocol: domain.ProtocolOpenAI, BaseURL: "https://backup.test", APIKey: "two"}, }, Models: []config.ModelConfig{{ID: "public/model", Routes: []config.RouteConfig{ {Provider: "primary", UpstreamModel: "primary-model", Priority: 0, Weight: 1}, {Provider: "backup", UpstreamModel: "backup-model", Priority: 10, Weight: 1}, }}}, } router := New(catalog.New(cfg)) plan, err := router.PlanProvider("public/model", domain.ProtocolOpenAI, "backup") if err != nil || len(plan) != 1 || plan[0].Provider.ID != "backup" { t.Fatalf("unexpected pinned plan: %+v err=%v", plan, err) } if _, err := router.PlanProvider("public/model", domain.ProtocolOpenAI, "missing"); !errors.Is(err, ErrProviderNotFound) { t.Fatalf("missing provider error = %v, want ErrProviderNotFound", err) } } func TestPlanProviderHonorsCircuitBreaker(t *testing.T) { now := time.Date(2026, time.August, 6, 0, 0, 0, 0, time.UTC) health := providerhealth.New(providerhealth.Options{FailureThreshold: 1, OpenDuration: time.Minute, Now: func() time.Time { return now }}) cfg := config.Config{ Providers: []config.ProviderConfig{{ID: "primary", Protocol: domain.ProtocolOpenAI, BaseURL: "https://primary.test", APIKey: "one"}}, Models: []config.ModelConfig{{ID: "public/model", Routes: []config.RouteConfig{{Provider: "primary", UpstreamModel: "model", Weight: 1}}}}, } health.Observe(providerhealth.RouteKey{ModelID: "public/model", ProviderID: "primary", WireAPI: "chat_completions"}, providerhealth.Observation{StatusCode: 503, Failed: true}) router := New(catalog.New(cfg), health) if _, err := router.PlanProvider("public/model", domain.ProtocolOpenAI, "primary"); !errors.Is(err, ErrNoHealthyRoute) { t.Fatalf("open pinned provider error = %v, want ErrNoHealthyRoute", err) } }