package main import ( "testing" "time" ) // The panel must not become one station repeated. PSK Reporter re-reports a // calling operator every cycle, and dozens of receivers report the same // transmission, so a station arrives many times a minute. func TestChaseNewStoreDeduplicates(t *testing.T) { s := newChaseNewStore() t0 := time.Date(2026, 8, 14, 12, 0, 0, 0, time.UTC) row := ChaseNewSpot{Call: "VK9XX", Band: "20m", Mode: "FT8", At: t0.Format(time.RFC3339)} if !s.put(row, t0) { t.Fatal("the first sighting was rejected") } if s.put(row, t0.Add(2*time.Minute)) { t.Error("the same station on the same band and mode was listed twice") } // A different band is a different opportunity — a new-band slot is exactly // what an operator is watching for. other := row other.Band = "15m" if !s.put(other, t0.Add(2*time.Minute)) { t.Error("the same station on another band was suppressed") } // Once the window has passed it is worth showing again: the station is still // there, and the row it had has aged out of the list. if !s.put(row, t0.Add(chaseSeenTTL+time.Minute)) { t.Error("the station never came back after the de-duplication window") } } // The list is aged with the operator's own spot lifetime, so a station heard an // hour ago is not offered as something to chase now. func TestChaseNewStoreAgesOut(t *testing.T) { s := newChaseNewStore() t0 := time.Date(2026, 8, 14, 12, 0, 0, 0, time.UTC) s.put(ChaseNewSpot{Call: "OLD", Band: "20m", Mode: "FT8", At: t0.Format(time.RFC3339)}, t0) s.put(ChaseNewSpot{Call: "NEW", Band: "20m", Mode: "FT8", At: t0.Add(20 * time.Minute).Format(time.RFC3339)}, t0.Add(20*time.Minute)) got := s.list(15*time.Minute, t0.Add(21*time.Minute)) if len(got) != 1 || got[0].Call != "NEW" { t.Fatalf("got %+v, want only NEW", got) } // Newest first: an operator reads the top of this list and nothing else. s.put(ChaseNewSpot{Call: "NEWEST", Band: "20m", Mode: "FT8", At: t0.Add(25 * time.Minute).Format(time.RFC3339)}, t0.Add(25*time.Minute)) got = s.list(15*time.Minute, t0.Add(26*time.Minute)) if len(got) != 2 || got[0].Call != "NEWEST" { t.Fatalf("got %+v, want NEWEST first", got) } } // The list is bounded. A widget that grows without limit under a 6 m opening // costs memory for rows nobody will ever scroll to. func TestChaseNewStoreIsBounded(t *testing.T) { s := newChaseNewStore() t0 := time.Date(2026, 8, 14, 12, 0, 0, 0, time.UTC) for i := 0; i < chaseNewMax+50; i++ { at := t0.Add(time.Duration(i) * time.Second) s.put(ChaseNewSpot{ Call: "S" + time.Duration(i).String(), Band: "20m", Mode: "FT8", At: at.Format(time.RFC3339), }, at) } if got := len(s.list(0, t0.Add(time.Hour))); got != chaseNewMax { t.Errorf("kept %d rows, want the %d cap", got, chaseNewMax) } }