package routing import ( "testing" "git.gobha.me/xcaliber/chat-switchboard/models" ) func strPtr(s string) *string { return &s } func f64Ptr(f float64) *float64 { return &f } func baseCandidates() []Candidate { return []Candidate{ {ConfigID: "cfg-openai", ProviderID: "openai", Model: "gpt-4o"}, {ConfigID: "cfg-anthropic", ProviderID: "anthropic", Model: "claude-sonnet-4-20250514"}, {ConfigID: "cfg-venice", ProviderID: "venice", Model: "llama-3.1-405b"}, } } func TestEvaluate_NoPolices(t *testing.T) { e := NewEvaluator() ctx := &Context{UserID: "u1", Model: "gpt-4o", HealthStatus: map[string]models.ProviderStatus{}} result, decision := e.Evaluate(ctx, nil, baseCandidates()) if len(result) != 3 { t.Fatalf("expected 3 candidates, got %d", len(result)) } if decision.Selected != "cfg-openai" { t.Errorf("expected first candidate selected, got %s", decision.Selected) } if decision.FallbackDepth != 0 { t.Errorf("expected fallback depth 0, got %d", decision.FallbackDepth) } } func TestEvaluate_ProviderPrefer(t *testing.T) { e := NewEvaluator() ctx := &Context{UserID: "u1", Model: "gpt-4o", HealthStatus: map[string]models.ProviderStatus{}} policies := []Policy{ { ID: "p1", Name: "Prefer Anthropic", Priority: 1, Type: PolicyProviderPrefer, IsActive: true, Scope: "global", Config: PolicyConfig{ PreferredProviders: []string{"cfg-anthropic", "cfg-venice"}, }, }, } result, decision := e.Evaluate(ctx, policies, baseCandidates()) if result[0].ConfigID != "cfg-anthropic" { t.Errorf("expected anthropic first, got %s", result[0].ConfigID) } if result[1].ConfigID != "cfg-venice" { t.Errorf("expected venice second, got %s", result[1].ConfigID) } // openai should still be present as fallback if result[2].ConfigID != "cfg-openai" { t.Errorf("expected openai last, got %s", result[2].ConfigID) } if decision.PolicyID != "p1" { t.Errorf("expected policy p1, got %s", decision.PolicyID) } } func TestEvaluate_TeamRoute(t *testing.T) { e := NewEvaluator() teamID := "team-1" ctx := &Context{ UserID: "u1", TeamIDs: []string{"team-1"}, Model: "gpt-4o", HealthStatus: map[string]models.ProviderStatus{}, } policies := []Policy{ { ID: "p1", Name: "Team uses Anthropic only", Priority: 1, Type: PolicyTeamRoute, IsActive: true, Scope: "team", TeamID: &teamID, Config: PolicyConfig{ AllowedProviders: []string{"cfg-anthropic"}, }, }, } result, _ := e.Evaluate(ctx, policies, baseCandidates()) if len(result) != 1 { t.Fatalf("expected 1 candidate after team filter, got %d", len(result)) } if result[0].ConfigID != "cfg-anthropic" { t.Errorf("expected anthropic only, got %s", result[0].ConfigID) } } func TestEvaluate_TeamRoute_WrongTeam(t *testing.T) { e := NewEvaluator() teamID := "team-2" ctx := &Context{ UserID: "u1", TeamIDs: []string{"team-1"}, // user is NOT in team-2 Model: "gpt-4o", HealthStatus: map[string]models.ProviderStatus{}, } policies := []Policy{ { ID: "p1", Name: "Team 2 only", Priority: 1, Type: PolicyTeamRoute, IsActive: true, Scope: "team", TeamID: &teamID, Config: PolicyConfig{AllowedProviders: []string{"cfg-anthropic"}}, }, } result, _ := e.Evaluate(ctx, policies, baseCandidates()) // Policy should not apply — all 3 candidates remain if len(result) != 3 { t.Fatalf("expected 3 candidates (policy doesn't apply), got %d", len(result)) } } func TestEvaluate_ModelAlias(t *testing.T) { e := NewEvaluator() ctx := &Context{ UserID: "u1", Model: "fast", HealthStatus: map[string]models.ProviderStatus{}, } policies := []Policy{ { ID: "p1", Name: "fast = haiku", Priority: 1, Type: PolicyModelAlias, IsActive: true, Scope: "global", Config: PolicyConfig{ AliasFrom: "fast", AliasTo: "claude-haiku-4-20250414", AliasProvider: "cfg-anthropic", }, }, } result, _ := e.Evaluate(ctx, policies, baseCandidates()) if result[0].ConfigID != "cfg-anthropic" { t.Errorf("expected anthropic first for alias, got %s", result[0].ConfigID) } if result[0].Model != "claude-haiku-4-20250414" { t.Errorf("expected model rewritten to haiku, got %s", result[0].Model) } } func TestEvaluate_HealthSkipDown(t *testing.T) { e := NewEvaluator() ctx := &Context{ UserID: "u1", Model: "gpt-4o", HealthStatus: map[string]models.ProviderStatus{ "cfg-openai": models.StatusDown, "cfg-anthropic": models.StatusHealthy, "cfg-venice": models.StatusDegraded, }, } policies := []Policy{ { ID: "p1", Name: "Skip down", Priority: 1, Type: PolicyProviderPrefer, IsActive: true, Scope: "global", Config: PolicyConfig{ PreferredProviders: []string{"cfg-openai", "cfg-anthropic"}, SkipDown: true, }, }, } result, _ := e.Evaluate(ctx, policies, baseCandidates()) // openai is down and should be skipped if result[0].ConfigID != "cfg-anthropic" { t.Errorf("expected anthropic first (openai is down), got %s", result[0].ConfigID) } // openai should not be in results at all for _, c := range result { if c.ConfigID == "cfg-openai" { t.Error("down provider should be skipped") } } } func TestEvaluate_HealthSorting(t *testing.T) { e := NewEvaluator() ctx := &Context{ UserID: "u1", Model: "gpt-4o", HealthStatus: map[string]models.ProviderStatus{ "cfg-openai": models.StatusDegraded, "cfg-anthropic": models.StatusHealthy, "cfg-venice": models.StatusUnknown, }, } result, _ := e.Evaluate(ctx, nil, baseCandidates()) // Should be sorted: healthy (anthropic) > degraded (openai) > unknown (venice) if result[0].ConfigID != "cfg-anthropic" { t.Errorf("expected healthy provider first, got %s", result[0].ConfigID) } if result[1].ConfigID != "cfg-openai" { t.Errorf("expected degraded provider second, got %s", result[1].ConfigID) } } func TestEvaluate_AllDown_KeepsCandidates(t *testing.T) { e := NewEvaluator() ctx := &Context{ UserID: "u1", Model: "gpt-4o", HealthStatus: map[string]models.ProviderStatus{ "cfg-openai": models.StatusDown, "cfg-anthropic": models.StatusDown, "cfg-venice": models.StatusDown, }, } policies := []Policy{ { ID: "p1", Priority: 1, Type: PolicyProviderPrefer, IsActive: true, Scope: "global", Config: PolicyConfig{SkipDown: true}, }, } result, _ := e.Evaluate(ctx, policies, baseCandidates()) // When ALL are down, should keep them rather than returning empty if len(result) == 0 { t.Error("should keep all candidates when all are down") } } func TestEvaluate_PriorityOrder(t *testing.T) { e := NewEvaluator() ctx := &Context{ UserID: "u1", TeamIDs: []string{"team-1"}, Model: "gpt-4o", HealthStatus: map[string]models.ProviderStatus{}, } teamID := "team-1" policies := []Policy{ { ID: "p-low", Name: "Low priority prefer", Priority: 10, Type: PolicyProviderPrefer, IsActive: true, Scope: "global", Config: PolicyConfig{PreferredProviders: []string{"cfg-venice"}}, }, { ID: "p-high", Name: "High priority team route", Priority: 1, Type: PolicyTeamRoute, IsActive: true, Scope: "team", TeamID: &teamID, Config: PolicyConfig{AllowedProviders: []string{"cfg-anthropic", "cfg-openai"}}, }, } result, decision := e.Evaluate(ctx, policies, baseCandidates()) // Team route (priority 1) should filter first, then prefer (priority 10) reorders if decision.PolicyType != string(PolicyTeamRoute) { t.Errorf("expected team_route to be recorded (first match), got %s", decision.PolicyType) } // Venice should be filtered out by team route for _, c := range result { if c.ConfigID == "cfg-venice" { t.Error("venice should be filtered out by team route") } } } func TestEvaluate_EmptyCandidates(t *testing.T) { e := NewEvaluator() ctx := &Context{UserID: "u1"} result, decision := e.Evaluate(ctx, nil, nil) if result != nil { t.Error("expected nil result for empty candidates") } if decision != nil { t.Error("expected nil decision for empty candidates") } } func TestEvaluate_InactivePolicy(t *testing.T) { e := NewEvaluator() ctx := &Context{UserID: "u1", Model: "gpt-4o", HealthStatus: map[string]models.ProviderStatus{}} policies := []Policy{ { ID: "p1", Priority: 1, Type: PolicyTeamRoute, IsActive: false, Scope: "global", Config: PolicyConfig{AllowedProviders: []string{"cfg-anthropic"}}, }, } result, _ := e.Evaluate(ctx, policies, baseCandidates()) // Inactive policy should not filter if len(result) != 3 { t.Fatalf("inactive policy should not filter, got %d candidates", len(result)) } } // ── capability_match tests ────────────────── func capContext() *Context { return &Context{ UserID: "u1", Model: "any", HealthStatus: map[string]models.ProviderStatus{}, Capabilities: map[string]*models.ModelCapabilities{ "cfg-openai:gpt-4o": {ToolCalling: true, Vision: true, Streaming: true}, "cfg-anthropic:claude-sonnet-4-20250514": {ToolCalling: true, Vision: true, Thinking: true, Streaming: true}, "cfg-venice:llama-3.1-405b": {ToolCalling: false, Vision: false, Streaming: true}, }, Pricing: map[string]*models.ModelPricing{ "cfg-openai:gpt-4o": {OutputPerM: 15.0}, "cfg-anthropic:claude-sonnet-4-20250514": {OutputPerM: 3.0}, "cfg-venice:llama-3.1-405b": {OutputPerM: 0.5}, }, } } func TestEvaluate_CapabilityMatch_ToolCalling(t *testing.T) { e := NewEvaluator() ctx := capContext() policies := []Policy{ { ID: "p1", Name: "Needs tool calling", Priority: 1, Type: PolicyCapabilityMatch, IsActive: true, Scope: "global", Config: PolicyConfig{RequiredCaps: []string{"tool_calling"}}, }, } result, decision := e.Evaluate(ctx, policies, baseCandidates()) // Venice (llama) has no tool_calling — should be filtered out if len(result) != 2 { t.Fatalf("expected 2 candidates with tool_calling, got %d", len(result)) } for _, c := range result { if c.ConfigID == "cfg-venice" { t.Error("venice should be filtered (no tool_calling)") } } if decision.PolicyID != "p1" { t.Errorf("expected policy p1, got %s", decision.PolicyID) } } func TestEvaluate_CapabilityMatch_MultipleCaps(t *testing.T) { e := NewEvaluator() ctx := capContext() policies := []Policy{ { ID: "p1", Name: "Needs thinking + tool_calling", Priority: 1, Type: PolicyCapabilityMatch, IsActive: true, Scope: "global", Config: PolicyConfig{RequiredCaps: []string{"thinking", "tool_calling"}}, }, } result, _ := e.Evaluate(ctx, policies, baseCandidates()) // Only anthropic has both thinking AND tool_calling if len(result) != 1 { t.Fatalf("expected 1 candidate with thinking+tool_calling, got %d", len(result)) } if result[0].ConfigID != "cfg-anthropic" { t.Errorf("expected anthropic, got %s", result[0].ConfigID) } } func TestEvaluate_CapabilityMatch_PreferCheapest(t *testing.T) { e := NewEvaluator() ctx := capContext() policies := []Policy{ { ID: "p1", Name: "Tool calling, cheapest first", Priority: 1, Type: PolicyCapabilityMatch, IsActive: true, Scope: "global", Config: PolicyConfig{ RequiredCaps: []string{"tool_calling"}, PreferCheapest: true, }, }, } result, _ := e.Evaluate(ctx, policies, baseCandidates()) if len(result) != 2 { t.Fatalf("expected 2 candidates, got %d", len(result)) } // Anthropic ($3/M) should be before OpenAI ($15/M) if result[0].ConfigID != "cfg-anthropic" { t.Errorf("expected cheapest (anthropic) first, got %s", result[0].ConfigID) } if result[1].ConfigID != "cfg-openai" { t.Errorf("expected openai second, got %s", result[1].ConfigID) } } func TestEvaluate_CapabilityMatch_NoneMatch_FallsBack(t *testing.T) { e := NewEvaluator() ctx := capContext() policies := []Policy{ { ID: "p1", Name: "Needs web_search", Priority: 1, Type: PolicyCapabilityMatch, IsActive: true, Scope: "global", Config: PolicyConfig{RequiredCaps: []string{"web_search"}}, }, } result, _ := e.Evaluate(ctx, policies, baseCandidates()) // None of the candidates have web_search — should fall back to all 3 if len(result) != 3 { t.Fatalf("expected 3 candidates (fallback), got %d", len(result)) } } func TestEvaluate_CapabilityMatch_StreamingOnly(t *testing.T) { e := NewEvaluator() ctx := capContext() policies := []Policy{ { ID: "p1", Name: "Streaming only", Priority: 1, Type: PolicyCapabilityMatch, IsActive: true, Scope: "global", Config: PolicyConfig{RequiredCaps: []string{"streaming"}}, }, } result, _ := e.Evaluate(ctx, policies, baseCandidates()) // All three have streaming — all should pass if len(result) != 3 { t.Fatalf("expected 3 candidates (all have streaming), got %d", len(result)) } } func TestEvaluate_CapabilityMatch_NoCapsData(t *testing.T) { e := NewEvaluator() ctx := &Context{ UserID: "u1", Model: "any", HealthStatus: map[string]models.ProviderStatus{}, Capabilities: nil, // no capability data } policies := []Policy{ { ID: "p1", Priority: 1, Type: PolicyCapabilityMatch, IsActive: true, Scope: "global", Config: PolicyConfig{RequiredCaps: []string{"tool_calling"}}, }, } result, _ := e.Evaluate(ctx, policies, baseCandidates()) // No caps data — policy should not match, all candidates kept if len(result) != 3 { t.Fatalf("expected 3 candidates (no caps data), got %d", len(result)) } } func TestEvaluate_CapabilityMatch_EmptyRequiredCaps(t *testing.T) { e := NewEvaluator() ctx := capContext() policies := []Policy{ { ID: "p1", Priority: 1, Type: PolicyCapabilityMatch, IsActive: true, Scope: "global", Config: PolicyConfig{RequiredCaps: []string{}}, }, } result, _ := e.Evaluate(ctx, policies, baseCandidates()) // Empty required caps — no filtering if len(result) != 3 { t.Fatalf("expected 3 candidates (empty caps), got %d", len(result)) } } func TestHasAllCaps(t *testing.T) { caps := &models.ModelCapabilities{ ToolCalling: true, Vision: true, Streaming: true, } if !hasAllCaps(caps, []string{"tool_calling"}) { t.Error("should match tool_calling") } if !hasAllCaps(caps, []string{"tool_calling", "vision"}) { t.Error("should match tool_calling+vision") } if hasAllCaps(caps, []string{"thinking"}) { t.Error("should not match thinking") } if !hasAllCaps(caps, []string{"unknown_future_cap"}) { t.Error("unknown caps should be ignored (forward compat)") } if hasAllCaps(nil, []string{"tool_calling"}) { t.Error("nil caps should return false") } if !hasAllCaps(caps, nil) { t.Error("nil required should return true") } if !hasAllCaps(caps, []string{}) { t.Error("empty required should return true") } }