Files
OpsLog/internal/alerts/bandplan_test.go
T
rouggy 9c7983b13e fix(alerts): one band plan, not two that disagree
A spot at 10131.5 showed as DATA in the cluster list and raised an alert
announcing SSB. Two tables answered the same question: the frontend knew
10.130-10.150 is the 30 m data segment, while the alert engine knew a CW
range that stopped at 10.130 and ended with a bare 'return SSB' for
everything else.

That fall-through was the fault. A band plan that answers 'SSB' for
anything it does not recognise is not a band plan, it is a default wearing
one — and it fed the alert text, the FlexRadio spot colours and the mode
the radio is told to switch to.

The Go table is now transcribed from the frontend's, segment for segment,
and returns nothing where it knows nothing. The order matters and is now
load-bearing — the FT8 and FT4 watering holes are listed BEFORE the wide
data blocks they sit inside — so there is a test that fails if anybody
sorts the table by frequency and quietly turns every FT8 hole into DATA.

WPM counts as CW while we are here: nothing else is reported in words per
minute, and RBN puts it on every line.
2026-08-26 21:09:52 +02:00

72 lines
2.5 KiB
Go

package alerts
import "testing"
// The spot that started it: RI1FJL on 10131.5, an FT8 station in the 30 m data
// segment. The cluster list showed DATA and the alert announced SSB, because
// this side's table stopped its CW range at 10.130 and called everything above
// it phone.
func TestInferModeDoesNotCallThirtyMetresSSB(t *testing.T) {
if got := InferMode("", 10_131_500); got != "DATA" {
t.Errorf("10131.5 kHz → %q, want DATA (there is no SSB on 30 m)", got)
}
// And the CW half of the same band still reads as CW.
if got := InferMode("", 10_110_000); got != "CW" {
t.Errorf("10110 kHz → %q, want CW", got)
}
}
// The comment beats the band plan. A spotter who names the mode knows something
// no table does — an FT8 station calling outside the usual watering hole, a CW
// operation in a data segment during a contest.
func TestInferModeTrustsTheCommentFirst(t *testing.T) {
cases := map[string]struct {
comment string
freqHz int64
want string
}{
"FT8 said outright": {"-14 dB FT8", 14_200_000, "FT8"},
"CW in a data segment": {"CW 599", 14_080_000, "CW"},
"RBN reports WPM not CW": {"18 dB 25 WPM CQ", 14_080_000, "CW"},
"phone below the split": {"SSB net", 7_010_000, "SSB"},
}
for name, c := range cases {
t.Run(name, func(t *testing.T) {
if got := InferMode(c.comment, c.freqHz); got != c.want {
t.Errorf("InferMode(%q, %d) = %q, want %q", c.comment, c.freqHz, got, c.want)
}
})
}
}
// The watering holes are listed before the wide data blocks they sit inside, and
// the table is scanned in order — so sorting it by frequency would quietly turn
// every FT8 hole into "DATA". This is what would fail if somebody did.
func TestBandPlanKeepsTheWateringHolesAheadOfTheDataBlocks(t *testing.T) {
holes := map[int64]string{
3_573_500: "FT8",
7_074_500: "FT8",
7_048_000: "FT4",
14_074_500: "FT8",
14_080_500: "FT4",
21_074_500: "FT8",
28_074_500: "FT8",
50_313_500: "FT8",
}
for hz, want := range holes {
if got := modeFromFrequency(hz); got != want {
t.Errorf("%d Hz → %q, want %q", hz, got, want)
}
}
}
// Nothing is claimed for a frequency the plan does not cover. Answering "SSB"
// for whatever fell through is the exact fault this file exists to fix.
func TestModeFromFrequencyIsSilentWhenItDoesNotKnow(t *testing.T) {
for _, hz := range []int64{500_000, 6_000_000, 70_200_000, 432_100_000} {
if got := modeFromFrequency(hz); got != "" {
t.Errorf("%d Hz → %q, want \"\" (outside the plan)", hz, got)
}
}
}