package main import ( "testing" "time" "hamlog/internal/bandopen" "hamlog/internal/cluster" ) // The "Watch for band openings" switch has to gate the DETECTOR, not just the // extra data sources. // // It originally governed only the RBN nodes and the PSK Reporter feed, while // the detector itself ran on every ordinary cluster spot — so an operator who // had never enabled the watch still got opening banners for a feature he had // deliberately left off. That is what this pins. func TestBandOpenWatchGatesTheDetector(t *testing.T) { spot := func() cluster.Spot { return cluster.Spot{ DXCall: "EA1ABC", Band: "6m", DistanceKm: 1400, ShortPath: 210, ReceivedAt: time.Now(), } } // Switched off: the spot must not even reach the detector. off := &App{opSet: true, opLat: 48.0, opLon: 2.0} off.detectBandOpening(spot()) if off.bandOpen.det != nil { t.Error("the detector ran with the watch switched off") } // Switched on: the same spot is accepted (one spot is not an opening, so // nothing is announced — but the detector now exists and is collecting). on := &App{opSet: true, opLat: 48.0, opLon: 2.0} on.bandOpen.on.Store(true) on.detectBandOpening(spot()) if on.bandOpen.det == nil { t.Error("the detector did not run with the watch switched on") } } // Switching the watch off must put the badges out. They fade on a timer fed by // spots the detector no longer looks at, so left alone they would stay lit // until the next restart. func TestClearBandOpeningsPutsTheBadgesOut(t *testing.T) { a := &App{} a.bandOpen.live = map[string]bandopen.Opening{"6m": {Band: "6m", Calls: 9}} a.bandOpen.aliveUntil = map[string]time.Time{"6m": time.Now().Add(time.Hour)} a.bandOpen.det = bandopen.New(bandopen.DefaultConfig()) a.bandOpen.last = []bandopen.Opening{{Band: "6m", Calls: 9}} a.clearBandOpenings() if got := a.GetLiveOpenings(); len(got) != 0 { t.Errorf("a badge stayed lit after the watch was switched off: %v", got) } if a.bandOpen.det != nil { t.Error("the accumulated spot window survived — switching back on would start from a stale burst") } // The history is NOT cleared: those openings really happened. if len(a.GetBandOpenings()) != 1 { t.Error("the remembered openings were thrown away") } } // Unticking a band in Settings must actually stop its announcements. // // Nothing checked the operator's selection on either feed path: bandopen.Watched // only says which bands the detector is CAPABLE of. So a station with just 6 m // ticked still got 10 m and 2 m badges, from the cluster path and — once grid // chasing widened the subscription to every band — from PSK Reporter too. func TestBandOpenHonoursTheSelectedBands(t *testing.T) { a := &App{opSet: true, opLat: 48, opLon: 2} a.bandOpen.on.Store(true) a.setBandOpenBands([]string{"6m"}) if !a.bandOpenWanted("6m") { t.Error("6m is ticked and was refused") } for _, b := range []string{"10m", "2m", "4m"} { if a.bandOpenWanted(b) { t.Errorf("%s is not ticked and was accepted", b) } } // A band the detector cannot watch at all stays out whatever is stored. if a.bandOpenWanted("20m") { t.Error("20m is not a watched band and was accepted") } // And the cluster path must obey it too. a.detectBandOpening(cluster.Spot{ DXCall: "K1ABC", Band: "10m", DistanceKm: 6000, ShortPath: 280, ReceivedAt: time.Now(), }) if a.bandOpen.det != nil { t.Error("a spot on an unticked band reached the detector") } } // Unticking a band puts its badge out. They fade on a timer fed by spots the // detector no longer looks at, so it would otherwise stay lit until a restart. func TestUntickingABandClearsItsBadge(t *testing.T) { a := &App{} a.bandOpen.live = map[string]bandopen.Opening{ "6m": {Band: "6m", Calls: 9}, "10m": {Band: "10m", Calls: 5}, } a.bandOpen.aliveUntil = map[string]time.Time{ "6m": time.Now().Add(time.Hour), "10m": time.Now().Add(time.Hour), } a.setBandOpenBands([]string{"6m"}) live := a.GetLiveOpenings() if len(live) != 1 || live[0].Band != "6m" { t.Errorf("live openings = %v, want only 6m", live) } }