bef39598d0
gates / gates (push) Successful in 24s
MinAgent: 0.131.0 (unchanged). Needs hub v0.118.0+, which shipped first and tolerates a box that sends none of this - every box in the fleet is that box until this release reaches it. The hub writes a household's e-mails in their language now, but about a third of those mails carry a sentence the BOX composed, naming a drive, an app or a number. The hub cannot translate one. So the box sends it twice. - message_customer on POST /api/v1/event, omitempty. A HUNGARIAN household sends nothing extra at all, so its payload stays byte-for-byte what every box sends today and the hub's fallback path keeps being the one production exercises rather than a branch nobody takes. - 19 producers render both sentences from ONE bundle key. `message` stays Hungarian always: it is what the operator is mailed and what the hub logs. - customer.language bootstraps a new box - stored choice, then config, then Hungarian. The config value is NEVER written into settings.json: that would record a choice the household never made. The Hungarian did not move, measured twice: the wire golden from the slice-2 base commit, and the Go parity gate over all 19 new keys. Three guards had to learn the change and one caught me: the test seam now carries the new field; the R-329 severity register reported two dynamic sites as no longer existing the moment they moved off PushEvent (the walk now checks 36 severity literals, up from 20); and TestConfigLanguageIsWiredInMain reads main.go, because cmd/ is gitignored and ripgrep does not. A mistake, named: the first pass dropped displayName from three producers, which would have mailed customers "Alkalmazás telepítve: %!s(MISSING)". Caught reading the diff; now pinned by a test that refuses %!/MISSING/%s/%d in either language. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
172 lines
7.2 KiB
Go
172 lines
7.2 KiB
Go
package notify
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// hubAcceptedSeverities is the hub's EXACT severity vocabulary, transcribed from
|
|
// felhom.eu/hub/internal/api/handler.go — the event-ingest handler's `switch payload.Severity` —
|
|
// where anything outside this set is silently coerced to "info".
|
|
//
|
|
// Of these, only warning/error/critical actually reach an email:
|
|
// felhom.eu/hub/internal/notify/dispatcher.go, func severityNotifies.
|
|
//
|
|
// This set is duplicated here ON PURPOSE. A test asserting only `== "warning"` would keep passing if
|
|
// the hub's vocabulary changed underneath it; pinning the set means the assertion is about the WIRE
|
|
// CONTRACT, and a reader can check both named locations rather than take this on trust.
|
|
var hubAcceptedSeverities = map[string]bool{"info": true, "warning": true, "error": true, "critical": true}
|
|
|
|
// hubSeverityNotifies mirrors felhom.eu/hub/internal/notify/dispatcher.go:severityNotifies.
|
|
func hubSeverityNotifies(sev string) bool {
|
|
switch sev {
|
|
case "warning", "error", "critical":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Group K — Scenario K: the severity actually routes.
|
|
//
|
|
// This is the highest-value assertion in the change. Before v0.215.0 the Figyelmeztetés-level disk
|
|
// alert carried severity "warn", which is NOT in the hub's accepted set, so the hub coerced it to
|
|
// "info" and severityNotifies dropped it — the alert reached no email on either the customer or the
|
|
// operator leg.
|
|
//
|
|
// Red-proof: restore `return "warn"` in DiskAlertKind.severity() → the exact-match assertion, the
|
|
// accepted-set assertion AND the routes-to-email assertion all fail.
|
|
func TestNotifyDiskHealthDegraded_SeverityRoutes(t *testing.T) {
|
|
n := &Notifier{}
|
|
var gotSev string
|
|
n.pushFn = func(eventType, severity, message, _ string, details interface{}) { gotSev = severity }
|
|
|
|
n.NotifyDiskHealthDegraded(DiskAlert{Label: "sdb", Kind: DiskAlertWarn,
|
|
Attributes: []string{"függőben lévő szektorok"}})
|
|
|
|
if gotSev != "warning" {
|
|
t.Errorf("Figyelmeztetés severity = %q, want %q", gotSev, "warning")
|
|
}
|
|
if !hubAcceptedSeverities[gotSev] {
|
|
t.Errorf("severity %q is NOT in the hub's accepted set — the hub coerces it to \"info\"", gotSev)
|
|
}
|
|
if !hubSeverityNotifies(gotSev) {
|
|
t.Errorf("severity %q does not route to email — the alert would reach nobody", gotSev)
|
|
}
|
|
|
|
// Every Hiba kind must also carry a routing severity.
|
|
for _, k := range []DiskAlertKind{DiskAlertFailSelfReported, DiskAlertFailSectors, DiskAlertFailTemperature, DiskAlertFailWorsened} {
|
|
n.NotifyDiskHealthDegraded(DiskAlert{Label: "sdc", Kind: k, Sectors: 352, TemperatureC: 61})
|
|
if gotSev != "critical" {
|
|
t.Errorf("kind %d severity = %q, want critical", k, gotSev)
|
|
}
|
|
if !hubAcceptedSeverities[gotSev] || !hubSeverityNotifies(gotSev) {
|
|
t.Errorf("kind %d severity %q does not route", k, gotSev)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The event type + details payload + the Figyelmeztetés copy (unchanged wording, now delivered).
|
|
func TestNotifyDiskHealthDegraded_WarnShape(t *testing.T) {
|
|
n := &Notifier{}
|
|
var gotType, gotMsg string
|
|
var gotDetails interface{}
|
|
n.pushFn = func(eventType, severity, message, _ string, details interface{}) {
|
|
gotType, gotMsg, gotDetails = eventType, message, details
|
|
}
|
|
|
|
n.NotifyDiskHealthDegraded(DiskAlert{Label: "sdb (lassú)", Kind: DiskAlertWarn,
|
|
Attributes: []string{"függőben lévő szektorok"}})
|
|
|
|
if gotType != "disk_health_degraded" {
|
|
t.Errorf("event type = %q, want disk_health_degraded", gotType)
|
|
}
|
|
if !strings.Contains(gotMsg, "Lemez állapot romlás: sdb (lassú)") {
|
|
t.Errorf("message missing the required subject phrase: %q", gotMsg)
|
|
}
|
|
if !strings.Contains(gotMsg, "függőben lévő szektorok") {
|
|
t.Errorf("message does not name the triggering attribute: %q", gotMsg)
|
|
}
|
|
if d, ok := gotDetails.(DiskHealthDetails); !ok || d.Disk != "sdb (lassú)" || d.Critical {
|
|
t.Errorf("details = %+v, want DiskHealthDetails{Disk:sdb (lassú), Critical:false}", gotDetails)
|
|
}
|
|
}
|
|
|
|
// Each Hiba kind produces its OWN message, and each names the fact the customer needs to act on.
|
|
// A customer told "the drive overheated" must not be told to arrange a replacement, and vice versa.
|
|
//
|
|
// Red-proof: collapse the switch so every Fail kind emits the self-reported sentence → the sector
|
|
// count and the temperature disappear from their messages and the substring assertions fail.
|
|
func TestNotifyDiskHealthDegraded_FailShapes(t *testing.T) {
|
|
n := &Notifier{}
|
|
var gotMsg string
|
|
var gotDetails interface{}
|
|
n.pushFn = func(eventType, severity, message, _ string, details interface{}) {
|
|
gotMsg, gotDetails = message, details
|
|
}
|
|
|
|
cases := []struct {
|
|
name string
|
|
alert DiskAlert
|
|
wantSubstrs []string
|
|
notSubstrs []string
|
|
}{
|
|
{
|
|
name: "drive self-reports FAILING",
|
|
alert: DiskAlert{Label: "sdc", Kind: DiskAlertFailSelfReported},
|
|
wantSubstrs: []string{"Lemez állapot romlás: sdc", "SMART önellenőrzése hibát jelez"},
|
|
},
|
|
{
|
|
name: "reached from sector counters",
|
|
alert: DiskAlert{Label: "ST3000VX010-2E3166", Kind: DiskAlertFailSectors, Sectors: 352},
|
|
wantSubstrs: []string{"Lemez hiba: ST3000VX010-2E3166", "352 olvashatatlan szektor", "meghajtó cseréjéhez"},
|
|
notSubstrs: []string{"SMART önellenőrzése"}, // the drive said PASSED — must not claim otherwise
|
|
},
|
|
{
|
|
name: "reached from heat",
|
|
alert: DiskAlert{Label: "sdd", Kind: DiskAlertFailTemperature, TemperatureC: 61},
|
|
wantSubstrs: []string{"Lemez hiba: sdd", "túlmelegedett (61 °C)", "szellőzését"},
|
|
notSubstrs: []string{"olvashatatlan szektor"},
|
|
},
|
|
{
|
|
name: "already reported, still worsening",
|
|
alert: DiskAlert{Label: "sdg", Kind: DiskAlertFailWorsened, Sectors: 352},
|
|
wantSubstrs: []string{"Lemez hiba: sdg", "tovább romlott", "352 olvashatatlan szektor"},
|
|
},
|
|
}
|
|
for _, c := range cases {
|
|
n.NotifyDiskHealthDegraded(c.alert)
|
|
for _, w := range c.wantSubstrs {
|
|
if !strings.Contains(gotMsg, w) {
|
|
t.Errorf("%s: message %q missing %q", c.name, gotMsg, w)
|
|
}
|
|
}
|
|
for _, bad := range c.notSubstrs {
|
|
if strings.Contains(gotMsg, bad) {
|
|
t.Errorf("%s: message %q must not contain %q", c.name, gotMsg, bad)
|
|
}
|
|
}
|
|
if d, ok := gotDetails.(DiskHealthDetails); !ok || !d.Critical {
|
|
t.Errorf("%s: details %+v, want Critical:true", c.name, gotDetails)
|
|
}
|
|
}
|
|
}
|
|
|
|
// No customer-facing disk copy may carry an emoji (design-system hard rule) and every shape must
|
|
// name the disk. Cheap, and it covers shapes added later by someone who did not read the rule.
|
|
func TestNotifyDiskHealthDegraded_CopyDiscipline(t *testing.T) {
|
|
n := &Notifier{}
|
|
var gotMsg string
|
|
n.pushFn = func(eventType, severity, message, _ string, details interface{}) { gotMsg = message }
|
|
for _, k := range []DiskAlertKind{DiskAlertWarn, DiskAlertFailSelfReported, DiskAlertFailSectors, DiskAlertFailTemperature, DiskAlertFailWorsened} {
|
|
n.NotifyDiskHealthDegraded(DiskAlert{Label: "TESTDISK", Kind: k, Sectors: 8, TemperatureC: 61})
|
|
if !strings.Contains(gotMsg, "TESTDISK") {
|
|
t.Errorf("kind %d: message does not name the disk: %q", k, gotMsg)
|
|
}
|
|
for _, r := range gotMsg {
|
|
if r > 0x2100 { // emoji / symbol block — Hungarian accents are all well below this
|
|
t.Errorf("kind %d: message contains a non-text rune %q: %s", k, r, gotMsg)
|
|
}
|
|
}
|
|
}
|
|
}
|