package main import ( "testing" "sdr-wideband-suite/internal/pipeline" ) func TestBuildMonitorWindowSummaryCountsCandidates(t *testing.T) { windows := []pipeline.MonitorWindow{ {Index: 0, Label: "primary", StartHz: 100, EndHz: 200, CenterHz: 150, SpanHz: 100}, {Index: 1, Label: "secondary", StartHz: 300, EndHz: 400, CenterHz: 350, SpanHz: 100}, } candidates := []pipeline.Candidate{ {ID: 1, CenterHz: 150, BandwidthHz: 20}, {ID: 2, CenterHz: 320, BandwidthHz: 10}, } summary := buildMonitorWindowSummary(windows, nil, candidates) if len(summary) != 2 { t.Fatalf("expected 2 window summaries, got %d", len(summary)) } if summary[0].Candidates != 1 || summary[1].Candidates != 1 { t.Fatalf("unexpected candidate counts: %+v", summary) } } func TestBuildMonitorWindowSummaryPreservesStatsCounts(t *testing.T) { stats := []pipeline.MonitorWindowStats{ {Index: 0, Label: "primary", StartHz: 100, EndHz: 200, CenterHz: 150, SpanHz: 100, Candidates: 2, Planned: 1}, } windows := []pipeline.MonitorWindow{ {Index: 0, Label: "primary", StartHz: 100, EndHz: 200, CenterHz: 150, SpanHz: 100}, } candidates := []pipeline.Candidate{ {ID: 1, CenterHz: 150, BandwidthHz: 20}, } summary := buildMonitorWindowSummary(windows, stats, candidates) if len(summary) != 1 { t.Fatalf("expected 1 window summary, got %d", len(summary)) } if summary[0].Candidates != 2 { t.Fatalf("expected candidates to stay at 2, got %d", summary[0].Candidates) } if summary[0].Planned != 1 { t.Fatalf("expected planned to stay at 1, got %d", summary[0].Planned) } } func TestBuildWindowOutcomeSummaryTracksPressureByWindowAndZone(t *testing.T) { windows := []pipeline.MonitorWindow{ {Index: 0, Label: "alpha", Zone: "north", StartHz: 100, EndHz: 200}, {Index: 1, Label: "beta", Zone: "south", StartHz: 300, EndHz: 400}, } match0 := pipeline.MonitorWindowMatch{Index: 0, Label: "alpha", Zone: "north"} match1 := pipeline.MonitorWindowMatch{Index: 1, Label: "beta", Zone: "south"} workItems := []pipeline.RefinementWorkItem{ { Candidate: pipeline.Candidate{ID: 1, MonitorMatches: []pipeline.MonitorWindowMatch{match0}}, Admission: &pipeline.PriorityAdmission{Class: pipeline.AdmissionClassAdmit}, }, { Candidate: pipeline.Candidate{ID: 2, MonitorMatches: []pipeline.MonitorWindowMatch{match0}}, Admission: &pipeline.PriorityAdmission{Class: pipeline.AdmissionClassHold}, }, { Candidate: pipeline.Candidate{ID: 3, MonitorMatches: []pipeline.MonitorWindowMatch{match1}}, Admission: &pipeline.PriorityAdmission{Class: pipeline.AdmissionClassDisplace}, }, { Candidate: pipeline.Candidate{ID: 4, MonitorMatches: []pipeline.MonitorWindowMatch{match1}}, Admission: &pipeline.PriorityAdmission{Class: pipeline.AdmissionClassDefer}, }, } decisions := []pipeline.SignalDecision{ { Candidate: pipeline.Candidate{ID: 1}, ShouldRecord: true, RecordWindow: &match0, RecordAdmission: &pipeline.PriorityAdmission{Class: pipeline.AdmissionClassAdmit}, }, { Candidate: pipeline.Candidate{ID: 2}, ShouldRecord: false, RecordWindow: &match0, RecordAdmission: &pipeline.PriorityAdmission{Class: pipeline.AdmissionClassDisplace}, }, { Candidate: pipeline.Candidate{ID: 3}, ShouldAutoDecode: true, DecodeWindow: &match1, DecodeAdmission: &pipeline.PriorityAdmission{Class: pipeline.AdmissionClassHold}, }, { Candidate: pipeline.Candidate{ID: 4}, ShouldAutoDecode: false, DecodeWindow: &match1, DecodeAdmission: &pipeline.PriorityAdmission{Class: pipeline.AdmissionClassDefer}, }, } summary := buildWindowSummary(pipeline.RefinementPlan{MonitorWindows: windows}, nil, nil, workItems, decisions) if summary == nil || summary.Outcomes == nil { t.Fatalf("expected outcome summary to be populated") } if len(summary.Outcomes.Windows) != 2 { t.Fatalf("expected 2 window outcomes, got %d", len(summary.Outcomes.Windows)) } win0 := summary.Outcomes.Windows[0] win1 := summary.Outcomes.Windows[1] if win0.Refinement.Admit != 1 || win0.Refinement.Hold != 1 { t.Fatalf("unexpected refinement outcomes for window 0: %+v", win0.Refinement) } if win0.Record.Admit != 1 || win0.Record.Displace != 1 || win0.Record.Enabled != 1 { t.Fatalf("unexpected record outcomes for window 0: %+v", win0.Record) } if win1.Refinement.Displace != 1 || win1.Refinement.Defer != 1 { t.Fatalf("unexpected refinement outcomes for window 1: %+v", win1.Refinement) } if win1.Decode.Hold != 1 || win1.Decode.Defer != 1 || win1.Decode.Enabled != 1 { t.Fatalf("unexpected decode outcomes for window 1: %+v", win1.Decode) } if len(summary.Outcomes.Zones) != 2 { t.Fatalf("expected 2 zone outcomes, got %d", len(summary.Outcomes.Zones)) } for _, zone := range summary.Outcomes.Zones { switch zone.Zone { case "north": if zone.Refinement.Admit != 1 || zone.Refinement.Hold != 1 { t.Fatalf("unexpected north zone refinement: %+v", zone.Refinement) } case "south": if zone.Refinement.Displace != 1 || zone.Refinement.Defer != 1 { t.Fatalf("unexpected south zone refinement: %+v", zone.Refinement) } default: t.Fatalf("unexpected zone %q", zone.Zone) } } }