From 1f5d4ab370676e9ce16e69b12e65961c8a37adc3 Mon Sep 17 00:00:00 2001 From: Jan Svabenik Date: Sun, 22 Mar 2026 11:33:44 +0100 Subject: [PATCH] pipeline: add intent and family priority tests --- internal/pipeline/arbitration_test.go | 20 ++++++++++++++ internal/pipeline/decision_queue_test.go | 33 ++++++++++++++++++++++++ internal/pipeline/scheduler_test.go | 31 ++++++++++++++++++++++ 3 files changed, 84 insertions(+) diff --git a/internal/pipeline/arbitration_test.go b/internal/pipeline/arbitration_test.go index adaf1cf..b0925b0 100644 --- a/internal/pipeline/arbitration_test.go +++ b/internal/pipeline/arbitration_test.go @@ -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) { res := AdmitRefinementPlan(RefinementPlan{}, Policy{}, time.Now(), &RefinementHold{Active: map[int64]time.Time{}}) if res.Admission.Reason != ReasonAdmissionNoCandidates { diff --git a/internal/pipeline/decision_queue_test.go b/internal/pipeline/decision_queue_test.go index deaa154..5df5fe2 100644 --- a/internal/pipeline/decision_queue_test.go +++ b/internal/pipeline/decision_queue_test.go @@ -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) { arbiter := NewArbiter() policy := Policy{DecisionHoldMs: 500} diff --git a/internal/pipeline/scheduler_test.go b/internal/pipeline/scheduler_test.go index 88e8d41..5b66346 100644 --- a/internal/pipeline/scheduler_test.go +++ b/internal/pipeline/scheduler_test.go @@ -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) { policy := Policy{MaxRefinementJobs: 2, MinCandidateSNRDb: 0} single := Candidate{ @@ -386,3 +408,12 @@ func findWorkItem(items []RefinementWorkItem, id int64) *RefinementWorkItem { } return nil } + +func findScheduled(items []ScheduledCandidate, id int64) *ScheduledCandidate { + for i := range items { + if items[i].Candidate.ID == id { + return &items[i] + } + } + return nil +}