watcher: send the inconsistency alert once per disagreement (closes #158)
check / check (push) Successful in 57s

detectInconsistencies alerted for every neighbouring pair of nameservers
whose records differed, on every DNS check, for as long as they differed.
It now also takes the previous hostname state and alerts for a pair only
when both nameservers were in that state with equal records. The state
loaded at startup is the previous state for the first check, so a
disagreement saved before a restart is not reported again. The choice of
pairs is in newlyDisagreeingPairs, tested on record data built in the
test. The README describes the new behaviour.

Model: opus-5-5
This commit is contained in:
2026-09-28 23:06:23 +00:00
parent 95b017eb3e
commit 8fd2fadbd8
5 changed files with 170 additions and 17 deletions
+109
View File
@@ -0,0 +1,109 @@
package watcher_test
import (
"slices"
"testing"
"sneak.berlin/go/dnswatcher/internal/state"
"sneak.berlin/go/dnswatcher/internal/watcher"
)
// hostnameState builds the state a check with these records leaves behind.
func hostnameState(
records map[string]map[string][]string,
) *state.HostnameState {
hs := &state.HostnameState{
RecordsByNameserver: make(map[string]*state.NameserverRecordState),
}
for ns, recs := range records {
hs.RecordsByNameserver[ns] = &state.NameserverRecordState{
Records: recs,
Status: "ok",
}
}
return hs
}
func TestNewlyDisagreeingPairs(t *testing.T) {
t.Parallel()
const (
nsA = "a.ns.example.net."
nsB = "b.ns.example.net."
ip1 = "192.0.2.1"
ip2 = "192.0.2.2"
)
onlyA := map[string]map[string][]string{nsA: {"A": {ip1}}}
agree := map[string]map[string][]string{
nsA: {"A": {ip1}},
nsB: {"A": {ip1}},
}
disagree := map[string]map[string][]string{
nsA: {"A": {ip1}},
nsB: {"A": {ip2}},
}
alert := [][2]string{{nsA, nsB}}
// Each case starts from the state loaded at startup and runs the
// checks in order; want[i] is what check i alerts for.
tests := []struct {
name string
loaded map[string]map[string][]string
checks []map[string]map[string][]string
want [][][2]string
}{
{
name: "disagreement persisting across checks alerts once",
loaded: agree,
checks: []map[string]map[string][]string{disagree, disagree, disagree},
want: [][][2]string{alert, nil, nil},
},
{
name: "disagreement starting on a later check alerts on it",
loaded: agree,
checks: []map[string]map[string][]string{agree, agree, disagree},
want: [][][2]string{nil, nil, alert},
},
{
name: "disagreement in the loaded state does not alert",
loaded: disagree,
checks: []map[string]map[string][]string{disagree, disagree},
want: [][][2]string{nil, nil},
},
{
name: "nameserver new on this check does not alert",
loaded: onlyA,
checks: []map[string]map[string][]string{disagree},
want: [][][2]string{nil},
},
{
name: "disagreement after agreeing again alerts again",
loaded: agree,
checks: []map[string]map[string][]string{disagree, agree, disagree},
want: [][][2]string{alert, nil, alert},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
prev := hostnameState(tt.loaded)
for i, current := range tt.checks {
got := watcher.NewlyDisagreeingPairs(prev, current)
if !slices.Equal(got, tt.want[i]) {
t.Errorf(
"check %d: alerted for %v, want %v",
i, got, tt.want[i],
)
}
prev = hostnameState(current)
}
})
}
}