package mrqueue import ( "testing" "time" ) func TestScoreMR_BaseScore(t *testing.T) { now := time.Now() config := DefaultScoreConfig() input := ScoreInput{ Priority: 2, // P2 (medium) MRCreatedAt: now, RetryCount: 0, Now: now, } score := ScoreMR(input, config) // BaseScore(1000) + Priority(2 gives 4-2=2, so 2*100=200) = 1200 expected := 1200.0 if score != expected { t.Errorf("expected score %f, got %f", expected, score) } } func TestScoreMR_PriorityOrdering(t *testing.T) { now := time.Now() tests := []struct { priority int expected float64 }{ {0, 1400.0}, // P0: base(1000) + (4-0)*100 = 1400 {1, 1300.0}, // P1: base(1000) + (4-1)*100 = 1300 {2, 1200.0}, // P2: base(1000) + (4-2)*100 = 1200 {3, 1100.0}, // P3: base(1000) + (4-3)*100 = 1100 {4, 1000.0}, // P4: base(1000) + (4-4)*100 = 1000 } for _, tt := range tests { t.Run("P"+string(rune('0'+tt.priority)), func(t *testing.T) { input := ScoreInput{ Priority: tt.priority, MRCreatedAt: now, Now: now, } score := ScoreMRWithDefaults(input) if score != tt.expected { t.Errorf("P%d: expected %f, got %f", tt.priority, tt.expected, score) } }) } // Verify ordering: P0 > P1 > P2 > P3 > P4 for i := 0; i < 4; i++ { input1 := ScoreInput{Priority: i, MRCreatedAt: now, Now: now} input2 := ScoreInput{Priority: i + 1, MRCreatedAt: now, Now: now} score1 := ScoreMRWithDefaults(input1) score2 := ScoreMRWithDefaults(input2) if score1 <= score2 { t.Errorf("P%d (%f) should score higher than P%d (%f)", i, score1, i+1, score2) } } } func TestScoreMR_ConvoyAgeEscalation(t *testing.T) { now := time.Now() config := DefaultScoreConfig() // MR without convoy noConvoy := ScoreInput{ Priority: 2, MRCreatedAt: now, Now: now, } scoreNoConvoy := ScoreMR(noConvoy, config) // MR with 24-hour old convoy convoyTime := now.Add(-24 * time.Hour) withConvoy := ScoreInput{ Priority: 2, MRCreatedAt: now, ConvoyCreatedAt: &convoyTime, Now: now, } scoreWithConvoy := ScoreMR(withConvoy, config) // 24 hours * 10 pts/hour = 240 extra points expectedDiff := 240.0 actualDiff := scoreWithConvoy - scoreNoConvoy if actualDiff != expectedDiff { t.Errorf("expected convoy age to add %f pts, got %f", expectedDiff, actualDiff) } } func TestScoreMR_ConvoyStarvationPrevention(t *testing.T) { now := time.Now() // P4 issue in 48-hour old convoy vs P0 issue with no convoy oldConvoy := now.Add(-48 * time.Hour) lowPriorityOldConvoy := ScoreInput{ Priority: 4, // P4 (lowest) MRCreatedAt: now, ConvoyCreatedAt: &oldConvoy, Now: now, } highPriorityNoConvoy := ScoreInput{ Priority: 0, // P0 (highest) MRCreatedAt: now, Now: now, } scoreOldConvoy := ScoreMRWithDefaults(lowPriorityOldConvoy) scoreHighPriority := ScoreMRWithDefaults(highPriorityNoConvoy) // P4 with 48h convoy: 1000 + 0 + 480 = 1480 // P0 with no convoy: 1000 + 400 + 0 = 1400 // Old convoy should win (starvation prevention) if scoreOldConvoy <= scoreHighPriority { t.Errorf("48h old P4 convoy (%f) should beat P0 no convoy (%f) for starvation prevention", scoreOldConvoy, scoreHighPriority) } } func TestScoreMR_RetryPenalty(t *testing.T) { now := time.Now() config := DefaultScoreConfig() // No retries noRetry := ScoreInput{ Priority: 2, MRCreatedAt: now, RetryCount: 0, Now: now, } scoreNoRetry := ScoreMR(noRetry, config) // 3 retries threeRetries := ScoreInput{ Priority: 2, MRCreatedAt: now, RetryCount: 3, Now: now, } scoreThreeRetries := ScoreMR(threeRetries, config) // 3 retries * 50 pts penalty = 150 pts less expectedDiff := 150.0 actualDiff := scoreNoRetry - scoreThreeRetries if actualDiff != expectedDiff { t.Errorf("expected 3 retries to lose %f pts, lost %f", expectedDiff, actualDiff) } } func TestScoreMR_RetryPenaltyCapped(t *testing.T) { now := time.Now() config := DefaultScoreConfig() // Max penalty is 300, so 10 retries should be same as 6 sixRetries := ScoreInput{ Priority: 2, MRCreatedAt: now, RetryCount: 6, Now: now, } tenRetries := ScoreInput{ Priority: 2, MRCreatedAt: now, RetryCount: 10, Now: now, } scoreSix := ScoreMR(sixRetries, config) scoreTen := ScoreMR(tenRetries, config) if scoreSix != scoreTen { t.Errorf("penalty should be capped: 6 retries (%f) should equal 10 retries (%f)", scoreSix, scoreTen) } // Both should be base(1000) + priority(200) - maxPenalty(300) = 900 expected := 900.0 if scoreSix != expected { t.Errorf("expected capped score %f, got %f", expected, scoreSix) } } func TestScoreMR_MRAgeAsTiebreaker(t *testing.T) { now := time.Now() // Two MRs with same priority, one submitted 10 hours ago oldMR := ScoreInput{ Priority: 2, MRCreatedAt: now.Add(-10 * time.Hour), Now: now, } newMR := ScoreInput{ Priority: 2, MRCreatedAt: now, Now: now, } scoreOld := ScoreMRWithDefaults(oldMR) scoreNew := ScoreMRWithDefaults(newMR) // Old MR should have 10 pts more (1 pt/hour) expectedDiff := 10.0 actualDiff := scoreOld - scoreNew if actualDiff != expectedDiff { t.Errorf("older MR should score %f more, got %f", expectedDiff, actualDiff) } } func TestScoreMR_Deterministic(t *testing.T) { fixedNow := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC) convoyTime := time.Date(2024, 12, 31, 12, 0, 0, 0, time.UTC) mrTime := time.Date(2025, 1, 1, 10, 0, 0, 0, time.UTC) input := ScoreInput{ Priority: 1, MRCreatedAt: mrTime, ConvoyCreatedAt: &convoyTime, RetryCount: 2, Now: fixedNow, } // Run 100 times, should always be same first := ScoreMRWithDefaults(input) for i := 0; i < 100; i++ { score := ScoreMRWithDefaults(input) if score != first { t.Errorf("score not deterministic: iteration %d got %f, expected %f", i, score, first) } } } func TestScoreMR_InvalidPriorityClamped(t *testing.T) { now := time.Now() // Negative priority should clamp to 0 bonus (priority=4) negativePriority := ScoreInput{ Priority: -1, MRCreatedAt: now, Now: now, } scoreNegative := ScoreMRWithDefaults(negativePriority) // Very high priority should clamp to max bonus (priority=0) highPriority := ScoreInput{ Priority: 10, MRCreatedAt: now, Now: now, } scoreHigh := ScoreMRWithDefaults(highPriority) // Negative priority gets clamped to max bonus (4*100=400) if scoreNegative != 1400.0 { t.Errorf("negative priority should clamp to P0 bonus, got %f", scoreNegative) } // High priority (10) gives 4-10=-6, clamped to 0 if scoreHigh != 1000.0 { t.Errorf("priority>4 should give 0 bonus, got %f", scoreHigh) } } func TestMR_Score(t *testing.T) { now := time.Now() convoyTime := now.Add(-12 * time.Hour) mr := &MR{ Priority: 1, CreatedAt: now.Add(-2 * time.Hour), ConvoyCreatedAt: &convoyTime, RetryCount: 1, } score := mr.ScoreAt(now) // base(1000) + convoy(12*10=120) + priority(3*100=300) - retry(1*50=50) + mrAge(2*1=2) expected := 1000.0 + 120.0 + 300.0 - 50.0 + 2.0 if score != expected { t.Errorf("MR.ScoreAt expected %f, got %f", expected, score) } } func TestScoreMR_EdgeCases(t *testing.T) { now := time.Now() tests := []struct { name string input ScoreInput }{ { name: "zero time MR", input: ScoreInput{ Priority: 2, MRCreatedAt: time.Time{}, Now: now, }, }, { name: "future MR", input: ScoreInput{ Priority: 2, MRCreatedAt: now.Add(24 * time.Hour), Now: now, }, }, { name: "future convoy", input: ScoreInput{ Priority: 2, MRCreatedAt: now, ConvoyCreatedAt: func() *time.Time { t := now.Add(24 * time.Hour); return &t }(), Now: now, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Should not panic score := ScoreMRWithDefaults(tt.input) // Score should still be reasonable (>= base - maxPenalty) if score < 700 { t.Errorf("score %f unexpectedly low for edge case", score) } }) } }