| @@ -30,6 +30,26 @@ func TestHoldPolicyDigitalBiasesDecode(t *testing.T) { | |||||
| } | } | ||||
| } | } | ||||
| func TestHoldPolicyIntentOverrides(t *testing.T) { | |||||
| policy := Policy{DecisionHoldMs: 1000, Intent: "archive-and-triage"} | |||||
| hold := HoldPolicyFromPolicy(policy) | |||||
| if hold.RecordMs <= hold.BaseMs { | |||||
| t.Fatalf("expected archive intent to extend record hold, got %d vs %d", hold.RecordMs, hold.BaseMs) | |||||
| } | |||||
| if !containsReason(hold.Reasons, HoldReasonIntentArchive) { | |||||
| t.Fatalf("expected intent archive reason, got %+v", hold.Reasons) | |||||
| } | |||||
| policy = Policy{DecisionHoldMs: 1000, Intent: "decode-digital"} | |||||
| hold = HoldPolicyFromPolicy(policy) | |||||
| if hold.DecodeMs <= hold.BaseMs { | |||||
| t.Fatalf("expected decode intent to extend decode hold, got %d vs %d", hold.DecodeMs, hold.BaseMs) | |||||
| } | |||||
| if !containsReason(hold.Reasons, HoldReasonIntentDecode) { | |||||
| t.Fatalf("expected intent decode reason, got %+v", hold.Reasons) | |||||
| } | |||||
| } | |||||
| func TestAdmitRefinementPlanNoCandidatesReason(t *testing.T) { | func TestAdmitRefinementPlanNoCandidatesReason(t *testing.T) { | ||||
| res := AdmitRefinementPlan(RefinementPlan{}, Policy{}, time.Now(), &RefinementHold{Active: map[int64]time.Time{}}) | res := AdmitRefinementPlan(RefinementPlan{}, Policy{}, time.Now(), &RefinementHold{Active: map[int64]time.Time{}}) | ||||
| if res.Admission.Reason != ReasonAdmissionNoCandidates { | if res.Admission.Reason != ReasonAdmissionNoCandidates { | ||||
| @@ -138,6 +138,39 @@ func TestDecisionQueueHighTierHoldProtected(t *testing.T) { | |||||
| } | } | ||||
| } | } | ||||
| func TestDecisionQueueFamilyPriorityProtectsHold(t *testing.T) { | |||||
| arbiter := NewArbiter() | |||||
| policy := Policy{DecisionHoldMs: 500, SignalPriorities: []string{"digital"}} | |||||
| budget := BudgetModel{Record: BudgetQueue{Max: 1}} | |||||
| now := time.Now() | |||||
| decisions := []SignalDecision{ | |||||
| {Candidate: Candidate{ID: 1, SNRDb: 5, Hint: "digital"}, ShouldRecord: true}, | |||||
| } | |||||
| arbiter.ApplyDecisions(decisions, budget, now, policy) | |||||
| if !decisions[0].ShouldRecord { | |||||
| t.Fatalf("expected candidate 1 to be selected initially") | |||||
| } | |||||
| decisions = []SignalDecision{ | |||||
| {Candidate: Candidate{ID: 1, SNRDb: 5, Hint: "digital"}, ShouldRecord: true}, | |||||
| {Candidate: Candidate{ID: 2, SNRDb: 35, Hint: "voice"}, ShouldRecord: true}, | |||||
| } | |||||
| arbiter.ApplyDecisions(decisions, budget, now.Add(100*time.Millisecond), policy) | |||||
| if !decisions[0].ShouldRecord { | |||||
| t.Fatalf("expected family-priority hold to keep candidate 1") | |||||
| } | |||||
| if decisions[1].ShouldRecord { | |||||
| t.Fatalf("expected candidate 2 to remain deferred behind family hold") | |||||
| } | |||||
| if decisions[0].RecordAdmission == nil || decisions[0].RecordAdmission.FamilyRank != 1 { | |||||
| t.Fatalf("expected family rank on admission, got %+v", decisions[0].RecordAdmission) | |||||
| } | |||||
| if decisions[0].RecordAdmission == nil || decisions[0].RecordAdmission.TierFloor != PriorityTierHigh { | |||||
| t.Fatalf("expected tier floor on admission, got %+v", decisions[0].RecordAdmission) | |||||
| } | |||||
| } | |||||
| func TestDecisionQueueOpportunisticDisplacement(t *testing.T) { | func TestDecisionQueueOpportunisticDisplacement(t *testing.T) { | ||||
| arbiter := NewArbiter() | arbiter := NewArbiter() | ||||
| policy := Policy{DecisionHoldMs: 500} | policy := Policy{DecisionHoldMs: 500} | ||||
| @@ -145,6 +145,28 @@ func TestScheduleCandidatesPriorityBoost(t *testing.T) { | |||||
| } | } | ||||
| } | } | ||||
| func TestScheduleCandidatesFamilyTierFloor(t *testing.T) { | |||||
| policy := Policy{MaxRefinementJobs: 2, MinCandidateSNRDb: 0, SignalPriorities: []string{"digital", "wfm"}} | |||||
| cands := []Candidate{ | |||||
| {ID: 1, SNRDb: 1, Hint: "digital-burst"}, | |||||
| {ID: 2, SNRDb: 20, Hint: "voice"}, | |||||
| } | |||||
| plan := BuildRefinementPlan(cands, policy) | |||||
| item := findScheduled(plan.Ranked, 1) | |||||
| if item == nil { | |||||
| t.Fatalf("expected ranked candidate 1") | |||||
| } | |||||
| if item.Family != "digital" || item.FamilyRank != 1 { | |||||
| t.Fatalf("expected digital family rank 1, got family=%s rank=%d", item.Family, item.FamilyRank) | |||||
| } | |||||
| if item.TierFloor != PriorityTierHigh { | |||||
| t.Fatalf("expected tier floor high, got %s", item.TierFloor) | |||||
| } | |||||
| if priorityTierRank(item.Tier) < priorityTierRank(PriorityTierHigh) { | |||||
| t.Fatalf("expected tier to be raised by family floor, got %s", item.Tier) | |||||
| } | |||||
| } | |||||
| func TestScheduleCandidatesEvidenceBoost(t *testing.T) { | func TestScheduleCandidatesEvidenceBoost(t *testing.T) { | ||||
| policy := Policy{MaxRefinementJobs: 2, MinCandidateSNRDb: 0} | policy := Policy{MaxRefinementJobs: 2, MinCandidateSNRDb: 0} | ||||
| single := Candidate{ | single := Candidate{ | ||||
| @@ -386,3 +408,12 @@ func findWorkItem(items []RefinementWorkItem, id int64) *RefinementWorkItem { | |||||
| } | } | ||||
| return nil | return nil | ||||
| } | } | ||||
| func findScheduled(items []ScheduledCandidate, id int64) *ScheduledCandidate { | |||||
| for i := range items { | |||||
| if items[i].Candidate.ID == id { | |||||
| return &items[i] | |||||
| } | |||||
| } | |||||
| return nil | |||||
| } | |||||