feat: Add intelligent carrier selection algorithm with machine learning optimization and performance tracking

- Add SelectionAlgorithm with scoring, weighting, and ML-based carrier selection
- Add SelectionCriteria struct with region, profile type, urgency, and cost/performance weights
- Add CarrierScore with performance, reliability, cost, region, and capability scores
- Add calculateWeightedScore combining all scores with ML-optimized weights
- Add getWeights with urgency-based adjustments
This commit is contained in:
nutcas3
2026-05-01 23:28:10 +03:00
parent 66ebf2c573
commit 699ac9f879
9 changed files with 1748 additions and 0 deletions
@@ -0,0 +1,183 @@
package smdp
import (
"math"
"time"
"github.com/sirupsen/logrus"
)
// SelectionCriteria defines parameters for carrier selection
type SelectionCriteria struct {
Region string `json:"region"`
ProfileType string `json:"profile_type"`
Urgency string `json:"urgency"` // "low", "medium", "high"
CostSensitivity float64 `json:"cost_sensitivity"` // 0.0-1.0
PerformanceWeight float64 `json:"performance_weight"` // 0.0-1.0
ReliabilityWeight float64 `json:"reliability_weight"` // 0.0-1.0
}
// CarrierScore represents the scored evaluation of a carrier
type CarrierScore struct {
CarrierID string `json:"carrier_id"`
Carrier *Carrier `json:"carrier"`
TotalScore float64 `json:"total_score"`
PerformanceScore float64 `json:"performance_score"`
ReliabilityScore float64 `json:"reliability_score"`
CostScore float64 `json:"cost_score"`
RegionScore float64 `json:"region_score"`
CapabilityScore float64 `json:"capability_score"`
SelectedAt time.Time `json:"selected_at"`
Reason string `json:"reason"`
}
// SelectionAlgorithm implements intelligent carrier selection
type SelectionAlgorithm struct {
logger *logrus.Logger
history map[string][]CarrierScore // Selection history per carrier
maxHistory int
learningRate float64
mlModel *LearningModel // Machine learning model
}
// NewSelectionAlgorithm creates a new selection algorithm instance
func NewSelectionAlgorithm() *SelectionAlgorithm {
return &SelectionAlgorithm{
logger: logrus.New(),
history: make(map[string][]CarrierScore),
maxHistory: 100,
learningRate: 0.1,
mlModel: NewLearningModel(),
}
}
// calculateWeightedScore combines all scores with weights
func (sa *SelectionAlgorithm) calculateWeightedScore(score *CarrierScore, criteria *SelectionCriteria) float64 {
weights := sa.getWeights(criteria)
total := score.PerformanceScore*weights.performance +
score.ReliabilityScore*weights.reliability +
score.CostScore*weights.cost +
score.RegionScore*weights.region +
score.CapabilityScore*weights.capability
return math.Min(100.0, total)
}
// getWeights returns selection weights based on criteria and ML optimization
func (sa *SelectionAlgorithm) getWeights(criteria *SelectionCriteria) struct {
performance float64
reliability float64
cost float64
region float64
capability float64
} {
// Get ML-optimized weights as base
mlWeights := sa.mlModel.GetOptimizedWeights()
weights := struct {
performance float64
reliability float64
cost float64
region float64
capability float64
}{
performance: mlWeights.Performance,
reliability: mlWeights.Reliability,
cost: mlWeights.Cost,
region: mlWeights.Region,
capability: mlWeights.Capability,
}
// Adjust based on urgency (override ML weights for specific business rules)
switch criteria.Urgency {
case "high":
weights.performance = math.Max(weights.performance, 0.4)
weights.reliability = math.Max(weights.reliability, 0.4)
weights.cost = math.Min(weights.cost, 0.1)
weights.region = math.Min(weights.region, 0.05)
weights.capability = math.Min(weights.capability, 0.05)
case "low":
weights.cost = math.Max(weights.cost, 0.4)
weights.performance = math.Min(weights.performance, 0.2)
weights.reliability = math.Min(weights.reliability, 0.2)
}
// Apply user-defined weights (business preferences override ML)
if criteria.PerformanceWeight > 0 {
weights.performance = criteria.PerformanceWeight
}
if criteria.ReliabilityWeight > 0 {
weights.reliability = criteria.ReliabilityWeight
}
if criteria.CostSensitivity > 0 {
weights.cost = criteria.CostSensitivity
}
// Normalize weights
total := weights.performance + weights.reliability + weights.cost + weights.region + weights.capability
if total > 0 {
weights.performance /= total
weights.reliability /= total
weights.cost /= total
weights.region /= total
weights.capability /= total
}
return weights
}
// recordSelection records the selection for learning
func (sa *SelectionAlgorithm) recordSelection(score *CarrierScore) {
history := sa.history[score.CarrierID]
history = append(history, *score)
// Limit history size
if len(history) > sa.maxHistory {
history = history[1:]
}
sa.history[score.CarrierID] = history
}
// GetSelectionHistory returns selection history for a carrier
func (sa *SelectionAlgorithm) GetSelectionHistory(carrierID string) []CarrierScore {
return sa.history[carrierID]
}
// UpdateLearning updates the algorithm based on feedback using ML model
func (sa *SelectionAlgorithm) UpdateLearning(carrierID string, actualPerformance float64) {
history := sa.history[carrierID]
if len(history) == 0 {
return
}
// Get the most recent selection to get expected score
lastSelection := history[len(history)-1]
expectedScore := lastSelection.TotalScore
// Update ML model with performance feedback
sa.mlModel.UpdateLearning(carrierID, actualPerformance, expectedScore)
// Legacy learning rate adjustment for backward compatibility
if actualPerformance < 50.0 {
sa.learningRate *= 0.95 // Reduce learning rate for poor performance
} else {
sa.learningRate *= 1.05 // Increase learning rate for good performance
}
}
// GetLearningStats returns statistics about the ML learning model
func (sa *SelectionAlgorithm) GetLearningStats() map[string]any {
return sa.mlModel.GetLearningStats()
}
// PredictPerformance predicts expected performance for a carrier
func (sa *SelectionAlgorithm) PredictPerformance(carrierID string, criteria *SelectionCriteria) float64 {
return sa.mlModel.PredictPerformance(carrierID, criteria)
}
// GetCarrierPerformance returns detailed performance metrics for a carrier
func (sa *SelectionAlgorithm) GetCarrierPerformance(carrierID string) *PerformanceMetrics {
return sa.mlModel.GetCarrierPerformance(carrierID)
}
@@ -0,0 +1,331 @@
package smdp
import (
"math"
"time"
)
// LearningModel represents the machine learning model for carrier selection
type LearningModel struct {
// Performance tracking
carrierPerformance map[string]*PerformanceMetrics
learningRate float64
adaptationThreshold float64
// Weight optimization
baseWeights WeightVector
optimizedWeights WeightVector
// Historical data
performanceHistory map[string][]float64
selectionHistory map[string][]SelectionRecord
// Model parameters
momentum float64
regularization float64
decayRate float64
}
// PerformanceMetrics tracks carrier performance over time
type PerformanceMetrics struct {
SuccessRate float64
AverageResponseTime float64
Reliability float64
CostEfficiency float64
LastUpdated time.Time
SampleCount int
}
// WeightVector represents the selection weights
type WeightVector struct {
Performance float64
Reliability float64
Cost float64
Region float64
Capability float64
}
// SelectionRecord tracks individual selection outcomes
type SelectionRecord struct {
CarrierID string
SelectedAt time.Time
ExpectedScore float64
ActualScore float64
Performance float64
ResponseTime time.Duration
Success bool
SelectionReason string
}
// NewLearningModel creates a new machine learning model
func NewLearningModel() *LearningModel {
return &LearningModel{
carrierPerformance: make(map[string]*PerformanceMetrics),
performanceHistory: make(map[string][]float64),
selectionHistory: make(map[string][]SelectionRecord),
learningRate: 0.01,
adaptationThreshold: 0.05,
momentum: 0.9,
regularization: 0.001,
decayRate: 0.995,
baseWeights: WeightVector{
Performance: 0.3,
Reliability: 0.3,
Cost: 0.2,
Region: 0.1,
Capability: 0.1,
},
optimizedWeights: WeightVector{
Performance: 0.3,
Reliability: 0.3,
Cost: 0.2,
Region: 0.1,
Capability: 0.1,
},
}
}
// UpdateLearning updates the model with performance feedback using ML techniques
func (lm *LearningModel) UpdateLearning(carrierID string, actualPerformance float64, expectedScore float64) {
// Record the performance outcome
record := SelectionRecord{
CarrierID: carrierID,
SelectedAt: time.Now(),
ExpectedScore: expectedScore,
ActualScore: actualPerformance,
Performance: actualPerformance,
Success: actualPerformance >= 70.0, // 70% threshold for success
}
// Add to selection history
history := lm.selectionHistory[carrierID]
history = append(history, record)
// Keep only last 100 records per carrier
if len(history) > 100 {
history = history[1:]
}
lm.selectionHistory[carrierID] = history
// Update carrier performance metrics
lm.updatePerformanceMetrics(carrierID, record)
// Calculate performance prediction error
predictionError := actualPerformance - expectedScore
// If error is significant, trigger weight optimization
if math.Abs(predictionError) > lm.adaptationThreshold {
lm.optimizeWeights(carrierID, predictionError)
}
// Apply learning rate decay
lm.learningRate *= lm.decayRate
if lm.learningRate < 0.001 {
lm.learningRate = 0.001 // Minimum learning rate
}
}
// updatePerformanceMetrics updates the performance metrics for a carrier
func (lm *LearningModel) updatePerformanceMetrics(carrierID string, record SelectionRecord) {
metrics, exists := lm.carrierPerformance[carrierID]
if !exists {
metrics = &PerformanceMetrics{
LastUpdated: time.Now(),
}
lm.carrierPerformance[carrierID] = metrics
}
// Update metrics with exponential moving average
alpha := 0.1 // Smoothing factor
if metrics.SampleCount == 0 {
// First sample
metrics.SuccessRate = 1.0
if !record.Success {
metrics.SuccessRate = 0.0
}
metrics.AverageResponseTime = float64(record.ResponseTime.Milliseconds())
metrics.Reliability = record.Performance / 100.0
} else {
// Exponential moving average update
if record.Success {
metrics.SuccessRate = alpha*1.0 + (1-alpha)*metrics.SuccessRate
} else {
metrics.SuccessRate = alpha*0.0 + (1-alpha)*metrics.SuccessRate
}
responseTimeMs := float64(record.ResponseTime.Milliseconds())
metrics.AverageResponseTime = alpha*responseTimeMs + (1-alpha)*metrics.AverageResponseTime
metrics.Reliability = alpha*(record.Performance/100.0) + (1-alpha)*metrics.Reliability
}
metrics.CostEfficiency = 1.0 - (float64(record.ResponseTime.Milliseconds()) / 1000.0) // Simple cost proxy
metrics.LastUpdated = time.Now()
metrics.SampleCount++
}
// optimizeWeights uses gradient descent to optimize selection weights
func (lm *LearningModel) optimizeWeights(carrierID string, error float64) {
// Calculate gradients based on performance error
gradients := lm.calculateGradients(carrierID, error)
// Apply gradient descent with momentum
learningRate := lm.learningRate
// Update optimized weights
lm.optimizedWeights.Performance -= learningRate * gradients.Performance
lm.optimizedWeights.Reliability -= learningRate * gradients.Reliability
lm.optimizedWeights.Cost -= learningRate * gradients.Cost
lm.optimizedWeights.Region -= learningRate * gradients.Region
lm.optimizedWeights.Capability -= learningRate * gradients.Capability
// Apply regularization to prevent overfitting
lm.applyRegularization()
// Ensure weights sum to 1 and are non-negative
lm.normalizeWeights()
}
// calculateGradients calculates the gradient for each weight based on error
func (lm *LearningModel) calculateGradients(carrierID string, error float64) WeightVector {
metrics := lm.carrierPerformance[carrierID]
if metrics == nil {
return WeightVector{}
}
// Calculate gradients based on how each factor contributed to the error
gradients := WeightVector{}
// Performance gradient: higher error suggests need to adjust performance weight
gradients.Performance = error * (metrics.Reliability - 0.5) * 2.0
// Reliability gradient: adjust based on success rate
gradients.Reliability = error * (metrics.SuccessRate - 0.5) * 2.0
// Cost gradient: adjust based on cost efficiency
gradients.Cost = error * (metrics.CostEfficiency - 0.5) * 2.0
// Region and capability gradients (simplified)
gradients.Region = error * 0.1
gradients.Capability = error * 0.1
return gradients
}
// applyRegularization applies L2 regularization to prevent overfitting
func (lm *LearningModel) applyRegularization() {
regFactor := lm.regularization
lm.optimizedWeights.Performance *= (1.0 - regFactor)
lm.optimizedWeights.Reliability *= (1.0 - regFactor)
lm.optimizedWeights.Cost *= (1.0 - regFactor)
lm.optimizedWeights.Region *= (1.0 - regFactor)
lm.optimizedWeights.Capability *= (1.0 - regFactor)
}
// normalizeWeights ensures weights sum to 1 and are non-negative
func (lm *LearningModel) normalizeWeights() {
// Ensure non-negative
if lm.optimizedWeights.Performance < 0 {
lm.optimizedWeights.Performance = 0
}
if lm.optimizedWeights.Reliability < 0 {
lm.optimizedWeights.Reliability = 0
}
if lm.optimizedWeights.Cost < 0 {
lm.optimizedWeights.Cost = 0
}
if lm.optimizedWeights.Region < 0 {
lm.optimizedWeights.Region = 0
}
if lm.optimizedWeights.Capability < 0 {
lm.optimizedWeights.Capability = 0
}
// Normalize to sum to 1
total := lm.optimizedWeights.Performance + lm.optimizedWeights.Reliability +
lm.optimizedWeights.Cost + lm.optimizedWeights.Region + lm.optimizedWeights.Capability
if total > 0 {
lm.optimizedWeights.Performance /= total
lm.optimizedWeights.Reliability /= total
lm.optimizedWeights.Cost /= total
lm.optimizedWeights.Region /= total
lm.optimizedWeights.Capability /= total
} else {
// Fallback to base weights if all weights become zero
lm.optimizedWeights = lm.baseWeights
}
}
// GetOptimizedWeights returns the current optimized weights for selection
func (lm *LearningModel) GetOptimizedWeights() WeightVector {
return lm.optimizedWeights
}
// GetCarrierPerformance returns performance metrics for a carrier
func (lm *LearningModel) GetCarrierPerformance(carrierID string) *PerformanceMetrics {
return lm.carrierPerformance[carrierID]
}
// PredictPerformance predicts the expected performance for a carrier
func (lm *LearningModel) PredictPerformance(carrierID string, criteria *SelectionCriteria) float64 {
metrics := lm.carrierPerformance[carrierID]
if metrics == nil {
return 75.0 // Default prediction for new carriers
}
// Use weighted combination of performance metrics
weights := lm.optimizedWeights
prediction := metrics.SuccessRate*weights.Reliability*100 +
(1.0-metrics.AverageResponseTime/1000.0)*weights.Performance*100 +
metrics.CostEfficiency*weights.Cost*100 +
0.5*weights.Region*100 + // Simplified region prediction
0.5*weights.Capability*100 // Simplified capability prediction
return math.Min(100.0, math.Max(0.0, prediction))
}
// GetLearningStats returns statistics about the learning model
func (lm *LearningModel) GetLearningStats() map[string]any {
stats := make(map[string]any)
stats["learning_rate"] = lm.learningRate
stats["total_carriers_tracked"] = len(lm.carrierPerformance)
stats["optimized_weights"] = lm.optimizedWeights
stats["base_weights"] = lm.baseWeights
// Calculate overall performance improvement
totalImprovement := 0.0
sampleCount := 0
for _, history := range lm.selectionHistory {
if len(history) > 10 { // Only consider carriers with sufficient data
firstHalf := history[:len(history)/2]
secondHalf := history[len(history)/2:]
firstAvg := 0.0
for _, record := range firstHalf {
firstAvg += record.ActualScore
}
firstAvg /= float64(len(firstHalf))
secondAvg := 0.0
for _, record := range secondHalf {
secondAvg += record.ActualScore
}
secondAvg /= float64(len(secondHalf))
improvement := secondAvg - firstAvg
totalImprovement += improvement
sampleCount++
}
}
if sampleCount > 0 {
stats["average_performance_improvement"] = totalImprovement / float64(sampleCount)
}
return stats
}
@@ -0,0 +1,94 @@
package smdp
import (
"context"
"fmt"
"sort"
"time"
"github.com/sirupsen/logrus"
)
// SelectOptimalCarrier selects the best carrier based on multiple criteria
func (sa *SelectionAlgorithm) SelectOptimalCarrier(
ctx context.Context,
carriers []*Carrier,
criteria *SelectionCriteria,
) (*CarrierScore, error) {
if len(carriers) == 0 {
return nil, fmt.Errorf("no carriers available for selection")
}
sa.logger.WithFields(logrus.Fields{
"region": criteria.Region,
"profile_type": criteria.ProfileType,
"urgency": criteria.Urgency,
"carrier_count": len(carriers),
}).Info("Starting optimal carrier selection")
// Score all carriers
scores := make([]*CarrierScore, 0, len(carriers))
for _, carrier := range carriers {
if !carrier.IsActive || carrier.HealthStatus != CarrierStatusHealthy {
continue
}
score := sa.scoreCarrier(carrier, criteria)
scores = append(scores, score)
}
if len(scores) == 0 {
return nil, fmt.Errorf("no healthy carriers available")
}
// Sort by total score (descending)
sort.Slice(scores, func(i, j int) bool {
return scores[i].TotalScore > scores[j].TotalScore
})
// Select the best carrier
bestScore := scores[0]
sa.recordSelection(bestScore)
sa.logger.WithFields(logrus.Fields{
"selected_carrier": bestScore.CarrierID,
"total_score": bestScore.TotalScore,
"performance": bestScore.PerformanceScore,
"reliability": bestScore.ReliabilityScore,
"cost": bestScore.CostScore,
}).Info("Optimal carrier selected")
return bestScore, nil
}
// scoreCarrier calculates a comprehensive score for a carrier
func (sa *SelectionAlgorithm) scoreCarrier(carrier *Carrier, criteria *SelectionCriteria) *CarrierScore {
score := &CarrierScore{
Carrier: carrier,
CarrierID: carrier.ID,
SelectedAt: time.Now(),
}
// Performance score (0-100)
score.PerformanceScore = sa.calculatePerformanceScore(carrier)
// Reliability score (0-100)
score.ReliabilityScore = sa.calculateReliabilityScore(carrier)
// Cost score (0-100, higher is better/cheaper)
score.CostScore = sa.calculateCostScore(carrier, criteria)
// Region compatibility score (0-100)
score.RegionScore = sa.calculateRegionScore(carrier, criteria.Region)
// Capability score (0-100)
score.CapabilityScore = sa.calculateCapabilityScore(carrier, criteria.ProfileType)
// Calculate weighted total score
score.TotalScore = sa.calculateWeightedScore(score, criteria)
// Generate selection reason
score.Reason = sa.generateReason(score, criteria)
return score
}
@@ -0,0 +1,168 @@
package smdp
import (
"fmt"
"math"
"slices"
)
// calculatePerformanceScore evaluates carrier performance metrics
func (sa *SelectionAlgorithm) calculatePerformanceScore(carrier *Carrier) float64 {
metrics := carrier.Metrics
if metrics.TotalRequests == 0 {
return 50.0 // Neutral score for new carriers
}
// Success rate (40% weight)
successRate := float64(metrics.SuccessfulRequests) / float64(metrics.TotalRequests)
successScore := successRate * 40.0
// Response time score (30% weight) - lower is better
responseScore := 30.0
if metrics.AverageResponseTime > 0 {
// Normalize: 1s = 0 points, 100ms = 30 points
responseMs := metrics.AverageResponseTime.Milliseconds()
if responseMs <= 100 {
responseScore = 30.0
} else if responseMs >= 1000 {
responseScore = 0.0
} else {
responseScore = 30.0 * (1.0 - float64(responseMs-100)/900.0)
}
}
// Request rate score (30% weight) - moderate rate is optimal
requestScore := 15.0 // Base score
if metrics.RequestRate > 0 && metrics.RequestRate <= 100 {
requestScore = 30.0
} else if metrics.RequestRate > 100 {
// Penalize very high rates (potential overload)
requestScore = 30.0 * math.Max(0, 1.0-metrics.RequestRate/1000.0)
}
return successScore + responseScore + requestScore
}
// calculateReliabilityScore evaluates carrier reliability
func (sa *SelectionAlgorithm) calculateReliabilityScore(carrier *Carrier) float64 {
// Health status (40% weight)
healthScore := 0.0
switch carrier.HealthStatus {
case CarrierStatusHealthy:
healthScore = 40.0
case CarrierStatusDegraded:
healthScore = 20.0
case CarrierStatusUnhealthy:
healthScore = 0.0
default:
healthScore = 10.0
}
// Uptime based on recent performance (30% weight)
uptimeScore := 30.0
if carrier.Metrics.TotalRequests > 0 {
successRate := float64(carrier.Metrics.SuccessfulRequests) / float64(carrier.Metrics.TotalRequests)
uptimeScore = successRate * 30.0
}
// Priority score (30% weight) - higher priority = more reliable
priorityScore := float64(carrier.Priority) / 100.0 * 30.0
if priorityScore > 30.0 {
priorityScore = 30.0
}
return healthScore + uptimeScore + priorityScore
}
// calculateCostScore evaluates cost effectiveness
func (sa *SelectionAlgorithm) calculateCostScore(carrier *Carrier, criteria *SelectionCriteria) float64 {
// This would integrate with actual pricing data
// For now, use priority as a proxy (higher priority = potentially more expensive)
costScore := 100.0 - float64(carrier.Priority)
if costScore < 0 {
costScore = 0
}
// Apply cost sensitivity
if criteria.CostSensitivity > 0.5 {
costScore *= 1.5 // Boost cost score for cost-sensitive requests
}
return costScore
}
// calculateRegionScore evaluates regional compatibility
func (sa *SelectionAlgorithm) calculateRegionScore(carrier *Carrier, region string) float64 {
if region == "" {
return 50.0 // Neutral score if no region specified
}
// Check if carrier supports the region
if carrier.CountryCode == region {
return 100.0
}
// Check regional compatibility through MCC
// This would require a region-to-MCC mapping
// For now, return neutral score
return 50.0
}
// calculateCapabilityScore evaluates carrier capabilities
func (sa *SelectionAlgorithm) calculateCapabilityScore(carrier *Carrier, profileType string) float64 {
capabilities := carrier.Capabilities
score := 50.0 // Base score
// Check if profile type is supported
if slices.Contains(capabilities.SupportedProfileTypes, profileType) {
score += 30.0
}
// Check for advanced features
hasBulkDownload := false
hasRemoteProvisioning := false
for _, feature := range capabilities.Features {
if feature == "bulk_download" {
hasBulkDownload = true
}
if feature == "remote_provisioning" {
hasRemoteProvisioning = true
}
}
if hasBulkDownload {
score += 10.0
}
if hasRemoteProvisioning {
score += 10.0
}
return math.Min(100.0, score)
}
// generateReason creates a human-readable selection reason
func (sa *SelectionAlgorithm) generateReason(score *CarrierScore, criteria *SelectionCriteria) string {
reasons := []string{}
if score.PerformanceScore > 80 {
reasons = append(reasons, "excellent performance")
}
if score.ReliabilityScore > 80 {
reasons = append(reasons, "high reliability")
}
if score.CostScore > 80 {
reasons = append(reasons, "cost-effective")
}
if score.RegionScore > 80 {
reasons = append(reasons, "region-compatible")
}
if score.CapabilityScore > 80 {
reasons = append(reasons, "full capability support")
}
if len(reasons) == 0 {
return "best available option"
}
return fmt.Sprintf("selected for %s", reasons[0])
}
@@ -0,0 +1,166 @@
package smdp
import (
"context"
"testing"
)
// TestEdgeCases tests edge cases and boundary conditions
func TestEdgeCases(t *testing.T) {
selector := NewSelectionAlgorithm()
// Test empty carrier list
t.Run("EmptyCarrierList", func(t *testing.T) {
criteria := &SelectionCriteria{
Region: "US",
ProfileType: "operational",
Urgency: "medium",
CostSensitivity: 0.5,
PerformanceWeight: 0.4,
ReliabilityWeight: 0.4,
}
_, err := selector.SelectOptimalCarrier(context.Background(), []*Carrier{}, criteria)
if err == nil {
t.Error("Expected error for empty carrier list")
}
})
// Test all unhealthy carriers
t.Run("AllUnhealthyCarriers", func(t *testing.T) {
unhealthyCarriers := []*Carrier{
createUnhealthyCarrier("broken1", "Broken 1", "US"),
createUnhealthyCarrier("broken2", "Broken 2", "US"),
}
criteria := &SelectionCriteria{
Region: "US",
ProfileType: "operational",
Urgency: "medium",
CostSensitivity: 0.5,
PerformanceWeight: 0.4,
ReliabilityWeight: 0.4,
}
_, err := selector.SelectOptimalCarrier(context.Background(), unhealthyCarriers, criteria)
if err == nil {
t.Error("Expected error when all carriers are unhealthy")
}
})
// Test inactive carriers
t.Run("InactiveCarriers", func(t *testing.T) {
inactiveCarriers := []*Carrier{
createInactiveCarrier("inactive1", "Inactive 1", "US"),
createInactiveCarrier("inactive2", "Inactive 2", "US"),
}
criteria := &SelectionCriteria{
Region: "US",
ProfileType: "operational",
Urgency: "medium",
CostSensitivity: 0.5,
PerformanceWeight: 0.4,
ReliabilityWeight: 0.4,
}
_, err := selector.SelectOptimalCarrier(context.Background(), inactiveCarriers, criteria)
if err == nil {
t.Error("Expected error when all carriers are inactive")
}
})
}
// TestMultipleSelections tests behavior with multiple selections
func TestMultipleSelections(t *testing.T) {
selector := NewSelectionAlgorithm()
carriers := []*Carrier{
createTestCarrier("carrier1", "Carrier 1", "US", 90, 95, 100, 5.0),
createTestCarrier("carrier2", "Carrier 2", "US", 85, 98, 120, 4.0),
createTestCarrier("carrier3", "Carrier 3", "US", 80, 97, 110, 6.0),
}
criteria := &SelectionCriteria{
Region: "US",
ProfileType: "operational",
Urgency: "medium",
CostSensitivity: 0.5,
PerformanceWeight: 0.4,
ReliabilityWeight: 0.4,
}
// Perform multiple selections
selections := make(map[string]int)
for i := range 20 {
score, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria)
if err != nil {
t.Fatalf("Selection %d failed: %v", i, err)
}
selections[score.CarrierID]++
}
// Check that we have some distribution
totalSelections := 0
for _, count := range selections {
totalSelections += count
}
if totalSelections != 20 {
t.Errorf("Expected 20 total selections, got %d", totalSelections)
}
t.Logf("Selection distribution: %+v", selections)
}
// TestLearningFeedback tests the learning feedback mechanism
func TestLearningFeedback(t *testing.T) {
selector := NewSelectionAlgorithm()
carriers := []*Carrier{
createTestCarrier("learning-carrier", "Learning Carrier", "US", 80, 90, 150, 5.0),
}
criteria := &SelectionCriteria{
Region: "US",
ProfileType: "operational",
Urgency: "medium",
CostSensitivity: 0.5,
PerformanceWeight: 0.4,
ReliabilityWeight: 0.4,
}
// Initial selection
_, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria)
if err != nil {
t.Fatalf("Initial selection failed: %v", err)
}
initialLearningRate := selector.learningRate
// Update with poor performance
selector.UpdateLearning("learning-carrier", 30.0)
learningRateAfterPoor := selector.learningRate
// Update with good performance
selector.UpdateLearning("learning-carrier", 90.0)
learningRateAfterGood := selector.learningRate
// Verify learning rate changes
if learningRateAfterPoor >= initialLearningRate {
t.Error("Learning rate should decrease after poor performance")
}
if learningRateAfterGood <= learningRateAfterPoor {
t.Error("Learning rate should increase after good performance")
}
t.Logf("Learning rate progression: %.4f -> %.4f -> %.4f",
initialLearningRate, learningRateAfterPoor, learningRateAfterGood)
}
// Helper function to create inactive carrier
func createInactiveCarrier(id, name, country string) *Carrier {
carrier := createTestCarrier(id, name, country, 50, 50, 500, 1.0)
carrier.IsActive = false
return carrier
}
@@ -0,0 +1,95 @@
package smdp
import (
"context"
"testing"
)
// BenchmarkSelectionAlgorithm benchmarks the selection algorithm performance
func BenchmarkSelectionAlgorithm(b *testing.B) {
selector := NewSelectionAlgorithm()
carriers := make([]*Carrier, 100)
for i := range 100 {
carriers[i] = createTestCarrier(
"carrier-"+string(rune(i)),
"Carrier "+string(rune(i)),
"US",
50+i%50,
80+i%20,
100+int64(i*10),
1.0+float64(i)*0.1,
)
}
criteria := &SelectionCriteria{
Region: "US",
ProfileType: "operational",
Urgency: "medium",
CostSensitivity: 0.5,
PerformanceWeight: 0.4,
ReliabilityWeight: 0.4,
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria)
if err != nil {
b.Fatalf("Selection failed: %v", err)
}
}
}
// BenchmarkScoring benchmarks individual scoring functions
func BenchmarkScoring(b *testing.B) {
selector := NewSelectionAlgorithm()
carrier := createTestCarrier("test", "Test", "US", 80, 95, 150, 5.0)
criteria := &SelectionCriteria{CostSensitivity: 0.5}
b.Run("PerformanceScore", func(b *testing.B) {
for i := 0; i < b.N; i++ {
selector.calculatePerformanceScore(carrier)
}
})
b.Run("ReliabilityScore", func(b *testing.B) {
for i := 0; i < b.N; i++ {
selector.calculateReliabilityScore(carrier)
}
})
b.Run("CostScore", func(b *testing.B) {
for i := 0; i < b.N; i++ {
selector.calculateCostScore(carrier, criteria)
}
})
b.Run("RegionScore", func(b *testing.B) {
for i := 0; i < b.N; i++ {
selector.calculateRegionScore(carrier, "US")
}
})
b.Run("CapabilityScore", func(b *testing.B) {
for i := 0; i < b.N; i++ {
selector.calculateCapabilityScore(carrier, "operational")
}
})
}
// BenchmarkWeightCalculation benchmarks weight calculation
func BenchmarkWeightCalculation(b *testing.B) {
selector := NewSelectionAlgorithm()
criteria := &SelectionCriteria{
Urgency: "medium",
PerformanceWeight: 0.4,
ReliabilityWeight: 0.3,
CostSensitivity: 0.3,
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
selector.getWeights(criteria)
}
}
@@ -0,0 +1,204 @@
package smdp
import (
"context"
"testing"
"time"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/config"
)
// TestSelectionAlgorithm tests the intelligent carrier selection algorithm
func TestSelectionAlgorithm(t *testing.T) {
// Create selection algorithm
selector := NewSelectionAlgorithm()
// Create test carriers with different characteristics
carriers := []*Carrier{
createTestCarrier("att-us", "AT&T US", "US", 90, 98, 150, 10.5),
createTestCarrier("verizon-us", "Verizon US", "US", 85, 99, 120, 8.2),
createTestCarrier("tmobile-de", "T-Mobile DE", "DE", 75, 95, 200, 6.8),
createTestCarrier("orange-fr", "Orange FR", "FR", 70, 95, 180, 4.5),
}
// Test case 1: High priority US request
t.Run("HighPriorityUSRequest", func(t *testing.T) {
criteria := &SelectionCriteria{
Region: "US",
ProfileType: "operational",
Urgency: "high",
CostSensitivity: 0.2,
PerformanceWeight: 0.6,
ReliabilityWeight: 0.6,
}
score, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria)
if err != nil {
t.Fatalf("Failed to select optimal carrier: %v", err)
}
// Should select a US carrier with high performance
if score.CarrierID != "att-us" && score.CarrierID != "verizon-us" {
t.Errorf("Expected US carrier, got %s", score.CarrierID)
}
if score.TotalScore < 80 {
t.Errorf("Expected high score for high priority request, got %.2f", score.TotalScore)
}
t.Logf("Selected carrier: %s with score %.2f", score.CarrierID, score.TotalScore)
})
// Test case 2: Cost-optimized European request
t.Run("CostOptimizedEURequest", func(t *testing.T) {
criteria := &SelectionCriteria{
Region: "DE",
ProfileType: "operational",
Urgency: "low",
CostSensitivity: 0.8,
PerformanceWeight: 0.2,
ReliabilityWeight: 0.3,
}
score, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria)
if err != nil {
t.Fatalf("Failed to select optimal carrier: %v", err)
}
// Should prefer European carrier for better region compatibility
if score.CarrierID != "tmobile-de" && score.CarrierID != "orange-fr" {
t.Logf("Note: Selected non-EU carrier %s for DE region", score.CarrierID)
}
t.Logf("Selected carrier: %s with score %.2f", score.CarrierID, score.TotalScore)
})
// Test case 3: Balanced global request
t.Run("BalancedGlobalRequest", func(t *testing.T) {
criteria := &SelectionCriteria{
Region: "",
ProfileType: "operational",
Urgency: "medium",
CostSensitivity: 0.5,
PerformanceWeight: 0.4,
ReliabilityWeight: 0.4,
}
score, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria)
if err != nil {
t.Fatalf("Failed to select optimal carrier: %v", err)
}
// Should select the best overall performer
if score.TotalScore < 70 {
t.Errorf("Expected reasonable score for balanced request, got %.2f", score.TotalScore)
}
t.Logf("Selected carrier: %s with score %.2f", score.CarrierID, score.TotalScore)
})
// Test case 4: No healthy carriers available
t.Run("NoHealthyCarriers", func(t *testing.T) {
unhealthyCarriers := []*Carrier{
createUnhealthyCarrier("broken-carrier", "Broken Carrier", "US"),
}
criteria := &SelectionCriteria{
Region: "US",
ProfileType: "operational",
Urgency: "medium",
CostSensitivity: 0.5,
PerformanceWeight: 0.4,
ReliabilityWeight: 0.4,
}
_, err := selector.SelectOptimalCarrier(context.Background(), unhealthyCarriers, criteria)
if err == nil {
t.Error("Expected error when no healthy carriers available")
}
t.Logf("Correctly returned error: %v", err)
})
}
// TestSelectionLearning tests the learning capabilities
func TestSelectionLearning(t *testing.T) {
selector := NewSelectionAlgorithm()
carriers := []*Carrier{
createTestCarrier("test-carrier", "Test Carrier", "US", 80, 90, 150, 5.0),
}
criteria := &SelectionCriteria{
Region: "US",
ProfileType: "operational",
Urgency: "medium",
CostSensitivity: 0.5,
PerformanceWeight: 0.4,
ReliabilityWeight: 0.4,
}
// Perform selection
score, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria)
if err != nil {
t.Fatalf("Failed to select optimal carrier: %v", err)
}
// Check selection history
history := selector.GetSelectionHistory("test-carrier")
if len(history) != 1 {
t.Errorf("Expected 1 selection in history, got %d", len(history))
}
// Update learning with performance feedback
selector.UpdateLearning("test-carrier", 85.0)
// Perform another selection to see if learning affects results
score2, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria)
if err != nil {
t.Fatalf("Failed to select optimal carrier on second attempt: %v", err)
}
t.Logf("First selection score: %.2f, Second selection score: %.2f",
score.TotalScore, score2.TotalScore)
}
// Helper functions
func createTestCarrier(id, name, country string, priority, successRate int, responseTime int64, requestRate float64) *Carrier {
return &Carrier{
ID: id,
Name: name,
CountryCode: country,
MCC: "310",
MNC: "410",
IsActive: true,
Priority: priority,
HealthStatus: CarrierStatusHealthy,
LastHealthCheck: time.Now(),
ES2Config: &config.ES2Config{
BaseURL: "https://example.com",
APIKey: "test-key",
InsecureSkipVerify: false,
FunctionalityRequesterID: "telecom-platform",
},
Capabilities: &CarrierCapabilities{
SupportedProfileTypes: []string{"operational", "testing"},
Features: []string{"bulk_download"},
MaxConcurrentRequests: 100,
},
Metrics: &CarrierMetrics{
TotalRequests: uint64(1000 * requestRate),
SuccessfulRequests: uint64(float64(1000*requestRate) * float64(successRate) / 100),
FailedRequests: uint64(float64(1000*requestRate) * float64(100-successRate) / 100),
AverageResponseTime: time.Duration(responseTime) * time.Millisecond,
RequestRate: requestRate,
},
}
}
func createUnhealthyCarrier(id, name, country string) *Carrier {
carrier := createTestCarrier(id, name, country, 50, 50, 500, 1.0)
carrier.HealthStatus = CarrierStatusUnhealthy
return carrier
}
@@ -0,0 +1,308 @@
package smdp
import (
"context"
"testing"
)
// TestMachineLearningModel tests the ML learning capabilities
func TestMachineLearningModel(t *testing.T) {
mlModel := NewLearningModel()
// Test initial state
t.Run("InitialState", func(t *testing.T) {
weights := mlModel.GetOptimizedWeights()
// Should start with base weights
if weights.Performance < 0.29 || weights.Performance > 0.31 {
t.Errorf("Expected performance weight around 0.3, got %.3f", weights.Performance)
}
stats := mlModel.GetLearningStats()
if stats["learning_rate"].(float64) <= 0 {
t.Error("Learning rate should be positive")
}
})
// Test learning updates
t.Run("LearningUpdates", func(t *testing.T) {
carrierID := "test-ml-carrier"
// Simulate multiple selections with feedback
for i := range 10 {
expectedScore := float64(70 + i*2) // Improving expected scores
actualScore := float64(65 + i*3) // Actual performance
mlModel.UpdateLearning(carrierID, actualScore, expectedScore)
}
// Check that weights have been optimized
optimizedWeights := mlModel.GetOptimizedWeights()
originalWeights := WeightVector{Performance: 0.3, Reliability: 0.3, Cost: 0.2, Region: 0.1, Capability: 0.1}
// Weights should have changed from original
if optimizedWeights.Performance == originalWeights.Performance &&
optimizedWeights.Reliability == originalWeights.Reliability {
t.Error("Expected weights to change after learning")
}
// Weights should still sum to 1
total := optimizedWeights.Performance + optimizedWeights.Reliability +
optimizedWeights.Cost + optimizedWeights.Region + optimizedWeights.Capability
if total < 0.99 || total > 1.01 {
t.Errorf("Weights should sum to 1.0, got %.3f", total)
}
})
// Test performance metrics tracking
t.Run("PerformanceMetrics", func(t *testing.T) {
carrierID := "metrics-carrier"
// Add some performance data
mlModel.UpdateLearning(carrierID, 85.0, 80.0)
mlModel.UpdateLearning(carrierID, 90.0, 85.0)
mlModel.UpdateLearning(carrierID, 78.0, 82.0)
metrics := mlModel.GetCarrierPerformance(carrierID)
if metrics == nil {
t.Fatal("Expected performance metrics for carrier")
}
if metrics.SampleCount != 3 {
t.Errorf("Expected 3 samples, got %d", metrics.SampleCount)
}
if metrics.SuccessRate < 0 || metrics.SuccessRate > 1 {
t.Errorf("Success rate should be between 0 and 1, got %.3f", metrics.SuccessRate)
}
})
// Test performance prediction
t.Run("PerformancePrediction", func(t *testing.T) {
carrierID := "prediction-carrier"
criteria := &SelectionCriteria{
Region: "US",
ProfileType: "operational",
Urgency: "medium",
CostSensitivity: 0.5,
PerformanceWeight: 0.4,
ReliabilityWeight: 0.4,
}
// Add training data
for i := range 5 {
mlModel.UpdateLearning(carrierID, 80.0+float64(i*2), 75.0+float64(i))
}
prediction := mlModel.PredictPerformance(carrierID, criteria)
if prediction < 0 || prediction > 100 {
t.Errorf("Prediction should be between 0 and 100, got %.2f", prediction)
}
// Prediction for unknown carrier should return default
unknownPrediction := mlModel.PredictPerformance("unknown-carrier", criteria)
if unknownPrediction != 75.0 {
t.Errorf("Expected default prediction for unknown carrier, got %.2f", unknownPrediction)
}
})
}
// TestMLIntegration tests the integration of ML with SelectionAlgorithm
func TestMLIntegration(t *testing.T) {
selector := NewSelectionAlgorithm()
// Create test carriers
carriers := []*Carrier{
createTestCarrier("ml-test-1", "ML Test 1", "US", 80, 85, 150, 5.0),
createTestCarrier("ml-test-2", "ML Test 2", "US", 75, 90, 120, 4.0),
}
criteria := &SelectionCriteria{
Region: "US",
ProfileType: "operational",
Urgency: "medium",
CostSensitivity: 0.5,
PerformanceWeight: 0.4,
ReliabilityWeight: 0.4,
}
// Test initial selection
t.Run("InitialSelection", func(t *testing.T) {
score, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria)
if err != nil {
t.Fatalf("Selection failed: %v", err)
}
// Provide learning feedback
selector.UpdateLearning(score.CarrierID, 85.0)
// Check learning stats
stats := selector.GetLearningStats()
if stats["total_carriers_tracked"].(int) == 0 {
t.Error("Expected carriers to be tracked in ML model")
}
})
// Test learning over multiple iterations
t.Run("IterativeLearning", func(t *testing.T) {
weightsBefore := selector.mlModel.GetOptimizedWeights()
// Perform multiple selections with feedback
for i := range 5 {
score, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria)
if err != nil {
t.Fatalf("Selection %d failed: %v", i, err)
}
// Simulate performance feedback (varying performance)
actualPerformance := 70.0 + float64(i*3)
selector.UpdateLearning(score.CarrierID, actualPerformance)
}
weightsAfter := selector.mlModel.GetOptimizedWeights()
// Weights should have been optimized
if weightsAfter.Performance == weightsBefore.Performance &&
weightsAfter.Reliability == weightsBefore.Reliability {
t.Error("Expected weights to change after learning iterations")
}
// Check performance prediction
prediction := selector.PredictPerformance("ml-test-1", criteria)
if prediction < 0 || prediction > 100 {
t.Errorf("Prediction should be between 0 and 100, got %.2f", prediction)
}
})
// Test carrier performance metrics
t.Run("CarrierPerformanceMetrics", func(t *testing.T) {
// Get performance metrics for a carrier
metrics := selector.GetCarrierPerformance("ml-test-1")
if metrics == nil {
t.Error("Expected performance metrics for carrier")
}
if metrics.SampleCount == 0 {
t.Error("Expected sample count > 0 after learning")
}
})
}
// TestMLWeightOptimization tests the weight optimization logic
func TestMLWeightOptimization(t *testing.T) {
mlModel := NewLearningModel()
t.Run("GradientDescent", func(t *testing.T) {
carrierID := "gradient-test"
// Simulate consistent poor performance to trigger weight optimization
for range 10 {
expectedScore := 80.0
actualScore := 50.0 // Consistently poor performance
mlModel.UpdateLearning(carrierID, actualScore, expectedScore)
}
weights := mlModel.GetOptimizedWeights()
// After consistent poor performance, weights should be adjusted
// This is a simplified test - in practice, the optimization is more complex
total := weights.Performance + weights.Reliability + weights.Cost + weights.Region + weights.Capability
if total < 0.99 || total > 1.01 {
t.Errorf("Weights should still sum to 1.0 after optimization, got %.3f", total)
}
// All weights should be non-negative
if weights.Performance < 0 || weights.Reliability < 0 || weights.Cost < 0 ||
weights.Region < 0 || weights.Capability < 0 {
t.Error("All weights should be non-negative")
}
})
t.Run("Regularization", func(t *testing.T) {
// Test that regularization prevents extreme weight values
carrierID := "regularization-test"
// Simulate extreme performance variations
for i := range 20 {
expectedScore := 80.0
actualScore := 20.0 + float64(i*3) // Wide range of performance
mlModel.UpdateLearning(carrierID, actualScore, expectedScore)
}
weights := mlModel.GetOptimizedWeights()
// Weights should be reasonable (not too extreme)
for _, weight := range []float64{
weights.Performance, weights.Reliability, weights.Cost,
weights.Region, weights.Capability} {
if weight < 0.05 || weight > 0.7 {
t.Errorf("Weight %.3f seems too extreme (should be between 0.05 and 0.7)", weight)
}
}
})
}
// TestMLLearningRate tests the learning rate adaptation
func TestMLLearningRate(t *testing.T) {
mlModel := NewLearningModel()
initialLearningRate := mlModel.GetLearningStats()["learning_rate"].(float64)
// Add some learning data
for range 10 {
mlModel.UpdateLearning("rate-test", 75.0, 70.0)
}
finalLearningRate := mlModel.GetLearningStats()["learning_rate"].(float64)
// Learning rate should have decayed
if finalLearningRate >= initialLearningRate {
t.Error("Learning rate should decay over time")
}
// Learning rate should not go below minimum
if finalLearningRate < 0.001 {
t.Error("Learning rate should not go below minimum threshold")
}
}
// BenchmarkMLModel benchmarks the ML model performance
func BenchmarkMLModel(b *testing.B) {
mlModel := NewLearningModel()
carrierID := "benchmark-carrier"
b.ResetTimer()
for i := 0; i < b.N; i++ {
mlModel.UpdateLearning(carrierID, 75.0, 70.0)
}
}
// BenchmarkSelectionWithML benchmarks selection with ML integration
func BenchmarkSelectionWithML(b *testing.B) {
selector := NewSelectionAlgorithm()
carriers := []*Carrier{
createTestCarrier("benchmark-1", "Benchmark 1", "US", 80, 85, 150, 5.0),
createTestCarrier("benchmark-2", "Benchmark 2", "US", 75, 90, 120, 4.0),
}
criteria := &SelectionCriteria{
Region: "US",
ProfileType: "operational",
Urgency: "medium",
CostSensitivity: 0.5,
PerformanceWeight: 0.4,
ReliabilityWeight: 0.4,
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria)
if err != nil {
b.Fatalf("Selection failed: %v", err)
}
selector.UpdateLearning("benchmark-1", 75.0)
}
}
@@ -0,0 +1,199 @@
package smdp
import (
"context"
"testing"
)
// TestScoringComponents tests individual scoring components
func TestScoringComponents(t *testing.T) {
selector := NewSelectionAlgorithm()
// Test performance scoring
t.Run("PerformanceScoring", func(t *testing.T) {
carrier := createTestCarrier("test", "Test", "US", 80, 95, 150, 5.0)
score := selector.calculatePerformanceScore(carrier)
if score < 0 || score > 100 {
t.Errorf("Performance score should be between 0 and 100, got %.2f", score)
}
t.Logf("Performance score: %.2f", score)
})
// Test reliability scoring
t.Run("ReliabilityScoring", func(t *testing.T) {
carrier := createTestCarrier("test", "Test", "US", 80, 95, 150, 5.0)
score := selector.calculateReliabilityScore(carrier)
if score < 0 || score > 100 {
t.Errorf("Reliability score should be between 0 and 100, got %.2f", score)
}
t.Logf("Reliability score: %.2f", score)
})
// Test cost scoring
t.Run("CostScoring", func(t *testing.T) {
carrier := createTestCarrier("test", "Test", "US", 80, 95, 150, 5.0)
criteria := &SelectionCriteria{CostSensitivity: 0.5}
score := selector.calculateCostScore(carrier, criteria)
if score < 0 || score > 100 {
t.Errorf("Cost score should be between 0 and 100, got %.2f", score)
}
t.Logf("Cost score: %.2f", score)
})
// Test region scoring
t.Run("RegionScoring", func(t *testing.T) {
carrier := createTestCarrier("test", "Test", "US", 80, 95, 150, 5.0)
// Test matching region
score1 := selector.calculateRegionScore(carrier, "US")
if score1 != 100.0 {
t.Errorf("Expected perfect score for matching region, got %.2f", score1)
}
// Test non-matching region
score2 := selector.calculateRegionScore(carrier, "DE")
if score2 != 50.0 {
t.Errorf("Expected neutral score for non-matching region, got %.2f", score2)
}
// Test empty region
score3 := selector.calculateRegionScore(carrier, "")
if score3 != 50.0 {
t.Errorf("Expected neutral score for empty region, got %.2f", score3)
}
t.Logf("Region scores - Match: %.2f, Non-match: %.2f, Empty: %.2f", score1, score2, score3)
})
// Test capability scoring
t.Run("CapabilityScoring", func(t *testing.T) {
carrier := createTestCarrier("test", "Test", "US", 80, 95, 150, 5.0)
// Test supported profile type
score1 := selector.calculateCapabilityScore(carrier, "operational")
if score1 < 80.0 {
t.Errorf("Expected high score for supported profile type, got %.2f", score1)
}
// Test unsupported profile type
score2 := selector.calculateCapabilityScore(carrier, "unsupported")
if score2 < 50.0 {
t.Errorf("Expected base score for unsupported profile type, got %.2f", score2)
}
t.Logf("Capability scores - Supported: %.2f, Unsupported: %.2f", score1, score2)
})
}
// TestWeightCalculation tests the weight calculation logic
func TestWeightCalculation(t *testing.T) {
selector := NewSelectionAlgorithm()
// Test default weights
t.Run("DefaultWeights", func(t *testing.T) {
criteria := &SelectionCriteria{
Urgency: "medium",
}
weights := selector.getWeights(criteria)
total := weights.performance + weights.reliability + weights.cost + weights.region + weights.capability
if total < 0.99 || total > 1.01 { // Allow for floating point precision
t.Errorf("Weights should sum to 1.0, got %.2f", total)
}
t.Logf("Default weights - P: %.2f, R: %.2f, C: %.2f, Reg: %.2f, Cap: %.2f",
weights.performance, weights.reliability, weights.cost, weights.region, weights.capability)
})
// Test high urgency weights
t.Run("HighUrgencyWeights", func(t *testing.T) {
criteria := &SelectionCriteria{
Urgency: "high",
}
weights := selector.getWeights(criteria)
if weights.performance < 0.39 || weights.reliability < 0.39 {
t.Errorf("High urgency should prioritize performance and reliability")
}
t.Logf("High urgency weights - P: %.2f, R: %.2f, C: %.2f, Reg: %.2f, Cap: %.2f",
weights.performance, weights.reliability, weights.cost, weights.region, weights.capability)
})
// Test low urgency weights
t.Run("LowUrgencyWeights", func(t *testing.T) {
criteria := &SelectionCriteria{
Urgency: "low",
}
weights := selector.getWeights(criteria)
if weights.cost < 0.39 {
t.Errorf("Low urgency should prioritize cost")
}
t.Logf("Low urgency weights - P: %.2f, R: %.2f, C: %.2f, Reg: %.2f, Cap: %.2f",
weights.performance, weights.reliability, weights.cost, weights.region, weights.capability)
})
// Test custom weights
t.Run("CustomWeights", func(t *testing.T) {
criteria := &SelectionCriteria{
Urgency: "medium",
PerformanceWeight: 0.7,
ReliabilityWeight: 0.2,
CostSensitivity: 0.1,
}
weights := selector.getWeights(criteria)
total := weights.performance + weights.reliability + weights.cost + weights.region + weights.capability
if total < 0.99 || total > 1.01 {
t.Errorf("Custom weights should sum to 1.0, got %.2f", total)
}
t.Logf("Custom weights - P: %.2f, R: %.2f, C: %.2f, Reg: %.2f, Cap: %.2f",
weights.performance, weights.reliability, weights.cost, weights.region, weights.capability)
})
}
// TestSelectionHistory tests the selection history functionality
func TestSelectionHistory(t *testing.T) {
selector := NewSelectionAlgorithm()
carriers := []*Carrier{
createTestCarrier("test-carrier", "Test Carrier", "US", 80, 90, 150, 5.0),
}
criteria := &SelectionCriteria{
Region: "US",
ProfileType: "operational",
Urgency: "medium",
CostSensitivity: 0.5,
PerformanceWeight: 0.4,
ReliabilityWeight: 0.4,
}
// Perform multiple selections
for i := range 5 {
_, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria)
if err != nil {
t.Fatalf("Failed to select optimal carrier on iteration %d: %v", i, err)
}
}
// Check selection history
history := selector.GetSelectionHistory("test-carrier")
if len(history) != 5 {
t.Errorf("Expected 5 selections in history, got %d", len(history))
}
// Test learning feedback
selector.UpdateLearning("test-carrier", 85.0)
t.Logf("Selection history contains %d entries", len(history))
}