mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-23 21:22:16 +00:00
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:
@@ -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))
|
||||
}
|
||||
Reference in New Issue
Block a user