Parcourir la source

Tighten arbitration snapshot and add arbitration tests

master
Jan Svabenik il y a 13 heures
Parent
révision
956d34d4f3
6 fichiers modifiés avec 85 ajouts et 2 suppressions
  1. +0
    -1
      cmd/sdrd/arbitration_snapshot.go
  2. +0
    -1
      cmd/sdrd/types.go
  3. +16
    -0
      internal/pipeline/arbitration_test.go
  4. +26
    -0
      internal/pipeline/budget_test.go
  5. +28
    -0
      internal/pipeline/decision_queue_test.go
  6. +15
    -0
      internal/pipeline/scheduler_test.go

+ 0
- 1
cmd/sdrd/arbitration_snapshot.go Voir le fichier

@@ -6,7 +6,6 @@ func buildArbitrationSnapshot(step pipeline.RefinementStep, arb pipeline.Arbitra
return &ArbitrationSnapshot{
Budgets: &arb.Budgets,
HoldPolicy: &arb.HoldPolicy,
RefinementPlan: &step.Input.Plan,
RefinementAdmission: &arb.Refinement,
Queue: arb.Queue,
DecisionSummary: summarizeDecisions(step.Result.Decisions),


+ 0
- 1
cmd/sdrd/types.go Voir le fichier

@@ -47,7 +47,6 @@ type DecisionDebug struct {
type ArbitrationSnapshot struct {
Budgets *pipeline.BudgetModel `json:"budgets,omitempty"`
HoldPolicy *pipeline.HoldPolicy `json:"hold_policy,omitempty"`
RefinementPlan *pipeline.RefinementPlan `json:"refinement_plan,omitempty"`
RefinementAdmission *pipeline.RefinementAdmission `json:"refinement_admission,omitempty"`
Queue pipeline.DecisionQueueStats `json:"queue,omitempty"`
DecisionSummary decisionSummary `json:"decision_summary,omitempty"`


+ 16
- 0
internal/pipeline/arbitration_test.go Voir le fichier

@@ -37,6 +37,22 @@ func TestAdmitRefinementPlanNoCandidatesReason(t *testing.T) {
}
}

func TestAdmitRefinementPlanUnlimitedBudget(t *testing.T) {
policy := Policy{MaxRefinementJobs: 0, MinCandidateSNRDb: 0}
cands := []Candidate{
{ID: 1, CenterHz: 100, SNRDb: 5},
{ID: 2, CenterHz: 200, SNRDb: 6},
}
plan := BuildRefinementPlan(cands, policy)
res := AdmitRefinementPlan(plan, policy, time.Now(), &RefinementHold{Active: map[int64]time.Time{}})
if len(res.Plan.Selected) != len(cands) {
t.Fatalf("expected all candidates admitted, got %d", len(res.Plan.Selected))
}
if res.Admission.Skipped != 0 || res.Plan.DroppedByBudget != 0 {
t.Fatalf("expected no budget drops, got admission=%+v plan=%+v", res.Admission, res.Plan)
}
}

func containsReason(reasons []string, target string) bool {
for _, r := range reasons {
if r == target {


+ 26
- 0
internal/pipeline/budget_test.go Voir le fichier

@@ -0,0 +1,26 @@
package pipeline

import "testing"

func TestBudgetModelRefinementSource(t *testing.T) {
t.Run("uses refinement max concurrent when tighter", func(t *testing.T) {
policy := Policy{MaxRefinementJobs: 12, RefinementMaxConcurrent: 4}
budget := BudgetModelFromPolicy(policy)
if budget.Refinement.Max != 4 {
t.Fatalf("expected refinement max 4, got %d", budget.Refinement.Max)
}
if budget.Refinement.Source != "refinement.max_concurrent" {
t.Fatalf("expected refinement source refinement.max_concurrent, got %s", budget.Refinement.Source)
}
})
t.Run("keeps resources budget when smaller", func(t *testing.T) {
policy := Policy{MaxRefinementJobs: 3, RefinementMaxConcurrent: 8}
budget := BudgetModelFromPolicy(policy)
if budget.Refinement.Max != 3 {
t.Fatalf("expected refinement max 3, got %d", budget.Refinement.Max)
}
if budget.Refinement.Source != "resources.max_refinement_jobs" {
t.Fatalf("expected refinement source resources.max_refinement_jobs, got %s", budget.Refinement.Source)
}
})
}

+ 28
- 0
internal/pipeline/decision_queue_test.go Voir le fichier

@@ -57,3 +57,31 @@ func TestDecisionQueueEnforcesBudgets(t *testing.T) {
t.Fatalf("expected mid SNR decision to be budgeted off by record budget")
}
}

func TestDecisionQueueHoldKeepsSelection(t *testing.T) {
arbiter := NewArbiter()
policy := Policy{DecisionHoldMs: 500}
budget := BudgetModel{Record: BudgetQueue{Max: 1}, Decode: BudgetQueue{Max: 1}}
now := time.Now()

decisions := []SignalDecision{
{Candidate: Candidate{ID: 1, SNRDb: 5}, ShouldRecord: true, ShouldAutoDecode: true},
{Candidate: Candidate{ID: 2, SNRDb: 15}, ShouldRecord: true, ShouldAutoDecode: true},
}
arbiter.ApplyDecisions(decisions, budget, now, policy)
if !decisions[1].ShouldRecord || !decisions[1].ShouldAutoDecode {
t.Fatalf("expected candidate 2 to be selected initially")
}

decisions = []SignalDecision{
{Candidate: Candidate{ID: 1, SNRDb: 25}, ShouldRecord: true, ShouldAutoDecode: true},
{Candidate: Candidate{ID: 2, SNRDb: 2}, ShouldRecord: true, ShouldAutoDecode: true},
}
arbiter.ApplyDecisions(decisions, budget, now.Add(100*time.Millisecond), policy)
if !decisions[1].ShouldRecord || !decisions[1].ShouldAutoDecode {
t.Fatalf("expected held candidate 2 to remain selected")
}
if decisions[0].ShouldRecord || decisions[0].ShouldAutoDecode {
t.Fatalf("expected candidate 1 to remain queued behind hold")
}
}

+ 15
- 0
internal/pipeline/scheduler_test.go Voir le fichier

@@ -237,6 +237,21 @@ func TestRefinementStrategyUsesProfile(t *testing.T) {
}
}

func TestRefinementStrategyUsesIntentAndSurveillance(t *testing.T) {
strategy, reason := refinementStrategy(Policy{Intent: "decode-digital"})
if strategy != "digital-hunting" || reason != "intent" {
t.Fatalf("expected intent to set digital strategy, got %s (%s)", strategy, reason)
}
strategy, reason = refinementStrategy(Policy{Intent: "archive-and-triage"})
if strategy != "archive-oriented" || reason != "intent" {
t.Fatalf("expected intent to set archive strategy, got %s (%s)", strategy, reason)
}
strategy, reason = refinementStrategy(Policy{SurveillanceStrategy: "multi-resolution"})
if strategy != "multi-resolution" || reason != "surveillance-strategy" {
t.Fatalf("expected surveillance strategy to set multi-resolution, got %s (%s)", strategy, reason)
}
}

func findWorkItem(items []RefinementWorkItem, id int64) *RefinementWorkItem {
for i := range items {
if items[i].Candidate.ID == id {


Chargement…
Annuler
Enregistrer