Files
felhom-controller/controller/internal/fillwatch/r329_severity_test.go
T
admin 9832760027
gates / gates (push) Successful in 11s
v0.223.0: the app-down alarm reached nobody (R-329), and the stop nobody heard (R-386)
R-329. NotifyAppStartFailures emitted severity "warn". The hub accepts exactly
{info, warning, error, critical} and silently coerces anything else to "info",
which severityNotifies then drops BEFORE both legs. Banner shown, event stored,
POST 200, no mail sent. One word.

This is the second time: DiskAlertKind.Severity emitted "warn" until v0.215.0
and its own comment records that every warning-level disk alert went to nobody.
A comment recorded the lesson and nothing enforced it. The guard is now an AST
walk over the whole controller - grep cannot work here, since "warn" appears
legitimately nine times as a healthcheck status vocabulary.

The sweep found exactly one bad severity. Its limits are stated: the walk cannot
follow a variable, so all six dynamic call sites are registered by name with the
values each can take, and a new one fails the test. Two of the six were found by
the guard, not by the hand sweep before it.

Also pinned: fillwatch.Band.Severity() returns "" for BandOK, which would vanish
the same way. It is unreachable because Check() notifies only on escalation -
but that safety lives in a different function from the one that looks unsafe, so
the test asserts the consequence rather than the mapping.

app_start_failed gains a customer toggle, DEFAULT OFF, per operator ruling. The
operator is mailed either way: processOperator never consults customer prefs.
It is deliberately NOT in operatorOnlyEvents, which would make the toggle a lie.

R-386. classifyRunStates decided "the customer stopped this" from the STATE, so
every stopped stack was assumed deliberate. Measured on demo-hp: privatebin
stopped out of band, nine scans, zero events, zero banner - while the comment
beside it claimed an out-of-band stop still alerts.

DesiredState already records the answer and has exactly one writer. Stopped ->
no alarm; Running -> alarm; absent -> UNKNOWN, keep today's behaviour AND say
so. Absent stays silent deliberately: reading it as "nobody asked" would email
about every app anyone ever stopped, fleet-wide, on the first cycle after
upgrade. The gap is bounded not silent - IntentUnknown is set and the names are
logged at INFO on the heartbeat cadence. failedRestart still lifts a Stopped
intent, or F-CRIT-1 re-opens. No new DesiredState writer.

Two settings toggles each governed two alarms. "Lemez figyelmeztetes (90%+)"
also wrote disk_critical, the drive-is-FAILING alarm. Now four honest toggles;
12 became 15. A no-op save stores the existing slice verbatim, so byte identity
is by construction - without that guard the defaults case reorders, which the
red-proof caught.

Test count 1504 -> 1522. Five red-proofs, five seen failing; one passed first
time and is reported - that mutation was inert, not the test weak.
2026-08-23 11:21:06 +02:00

78 lines
3.8 KiB
Go

package fillwatch
import "testing"
// R-329 — `Band.Severity()` returns "" for BandOK, and "" is NOT in the hub's vocabulary
// {info, warning, error, critical}. The hub would coerce it to "info" and mail nobody.
//
// **This is safe today, and this test is what keeps it safe**, because the safety is not in
// `Severity()` at all — it is in `Check()`: the notify seam fires ONLY on an escalation
// (`if next <= prev { continue }`), and BandOK is the lowest band, so a notification can never carry
// it. That is an invariant held in one function about the behaviour of another, which is precisely
// the shape this project has shipped wrong nine times.
//
// THE LAYER. The first test pins the mapping (a unit fact). The second pins the CONSEQUENCE — that
// no notification can carry a band whose severity is empty — because the mapping being right is not
// what makes the product correct, and asserting only the mapping is case #9's mistake.
//
// RED-PROOF (observed, see REPORT.md): REMOVE the de-escalation `continue` block from Check() —
// every band change then notifies, and TestR329_FillwatchNeverEmitsTheEmptySeverity fails with
// `notified with band ok → severity "" (event type "")`.
//
// **A weaker mutation is INERT here, and it is worth knowing which:** changing `if next <= prev` to
// `if next < prev` does nothing, because an earlier `if next == prev { continue }` already removed
// the equal case. That mutation was tried first, the test passed, and the test was right to pass —
// the code had not changed behaviour. **A red-proof that passes is not automatically a weak test;
// check the mutation actually applied before believing either verdict.**
func TestR329_BandSeverityMapping(t *testing.T) {
for _, tc := range []struct {
band Band
want string
}{
{BandWarning, "warning"},
{BandCritical, "critical"},
} {
if got := tc.band.Severity(); got != tc.want {
t.Errorf("Band(%v).Severity() = %q, want %q", tc.band, got, tc.want)
}
}
// Documented and deliberate: BandOK has no severity because it has no event. The next test is
// what proves that cannot leak.
if got := BandOK.Severity(); got != "" {
t.Errorf("BandOK.Severity() = %q — if this ever becomes a real severity, an all-clear starts "+
"emailing customers; change it deliberately, not by accident", got)
}
}
// The consequence: drive a real Watcher up and back down and assert that every notification carries
// a severity the hub will actually route.
func TestR329_FillwatchNeverEmitsTheEmptySeverity(t *testing.T) {
// Reuses the package's existing harness (REUSE.md §4) rather than inventing a second fixture.
h := newHarness(t, Target{Path: "/mnt/data", Label: "Adatok"})
// up to warning, up to critical, back down, back to ok — the full round trip, so a
// de-escalation notification would be caught if one ever started firing.
for _, pct := range []float64{50, 91, 97, 91, 50} {
h.set("/mnt/data", pct, 100-pct)
h.check(t)
}
if len(h.events) == 0 {
// Positive control: a test that observes nothing proves nothing. If the fixture stopped
// crossing bands this would pass forever while checking air.
t.Fatal("no notifications at all — the fixture never crossed a band, so this test is checking " +
"nothing; fix the fixture before trusting a green")
}
for _, e := range h.events {
if e.Band.Severity() == "" || e.Band.EventType() == "" {
t.Errorf("notified with band %v → severity %q (event type %q): the hub coerces an unknown "+
"severity to \"info\" and then drops it, so this alert would reach NOBODY",
e.Band, e.Band.Severity(), e.Band.EventType())
}
if !map[string]bool{"info": true, "warning": true, "error": true, "critical": true}[e.Band.Severity()] {
t.Errorf("notified with severity %q, outside the hub vocabulary", e.Band.Severity())
}
}
t.Logf("%d crossings notified, every one with a routable severity", len(h.events))
}