Files
felhom-controller/controller/internal/notify/wire_golden_test.go
T
admin bef39598d0
gates / gates (push) Successful in 24s
v0.256.0: the box sends its own sentence in the household's language (R-558 Part B)
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
2026-09-18 16:54:20 +02:00

123 lines
4.8 KiB
Go

package notify
import (
"strings"
"testing"
)
// TestEventMessageWireTextIsFrozen — localisation slice 2 (R-557), scenario S6.
//
// An event's `message` is ON THE WIRE and, unlike most wire text, a CUSTOMER READS IT. The hub's
// FormatCustomerEmail (felhom.eu/hub/internal/notify/templates.go) uses the per-type Hungarian
// `customerMessages` entry when it has one and FALLS BACK TO THIS MESSAGE when it has none — and
// appends it as „- Üzenet: %s" whenever it differs from the entry. Several event types deliberately
// have no entry precisely so the controller's dynamic sentence is what the household is sent
// (hub/internal/api/handler.go, the notes around offbox_enlarge_blocked and its neighbours).
//
// So these sentences are NOT the dashboard's to translate. They follow the household's language in
// slice 3 (R-558), where the hub learns which language the box reports and composes the mail. Until
// then they stay Hungarian in every language, and this test is what makes "stay" measurable: the
// golden strings below were captured from the producers at 736f54b49610, the base commit of
// v0.252.0, before any conversion.
//
// Not every producer is here — the table covers one representative of each SHAPE (fixed sentence,
// one parameter, two parameters, a numeric parameter, a branch per case), which is what a conversion
// would break. A producer converted by accident would have to escape every shape to escape this.
func TestEventMessageWireTextIsFrozen(t *testing.T) {
cases := []struct {
name string
call func(n *Notifier)
want string
}{
{
name: "fixed sentence",
call: func(n *Notifier) { n.NotifyDBDumpCompleted(DBDumpDetails{}) },
want: "Adatbázis mentés elkészült",
},
{
name: "one parameter",
call: func(n *Notifier) { n.NotifyAppDeployed("privatebin", "PrivateBin") },
want: "Alkalmazás telepítve: PrivateBin",
},
{
name: "two parameters",
call: func(n *Notifier) { n.NotifyBackupTargetAbsent("Kulso HDD", "/mnt/hdd_1") },
want: "A rendszermentés meghajtója nem érhető el: Kulso HDD (/mnt/hdd_1)",
},
{
name: "numeric parameter",
call: func(n *Notifier) { n.NotifyDRStarted(3) },
want: "Katasztrófa helyreállítás elindítva (3 alkalmazás)",
},
{
name: "two numeric parameters",
call: func(n *Notifier) { n.NotifyDRCompleted(7, 2) },
want: "Katasztrófa helyreállítás befejezve (7 sikeres, 2 sikertelen)",
},
{
name: "branch: update succeeded",
call: func(n *Notifier) { n.NotifyControllerUpdated("0.251.0", "0.252.0", true) },
want: "Controller frissítve: 0.251.0 → 0.252.0",
},
{
name: "branch: update failed",
call: func(n *Notifier) { n.NotifyControllerUpdated("0.251.0", "0.252.0", false) },
want: "Controller frissítés sikertelen: 0.251.0 → 0.252.0",
},
{
name: "disk health, sector count",
call: func(n *Notifier) {
n.NotifyDiskHealthDegraded(DiskAlert{Label: "Kulso HDD", Kind: DiskAlertFailSectors, Sectors: 352})
},
want: "Lemez hiba: Kulso HDD — a meghajtón 352 olvashatatlan szektor van. " +
"Mentse az adatait, és keressen meg minket a meghajtó cseréjéhez.",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
var got string
var seen int
n := &Notifier{}
n.pushFn = func(_, _, message, _ string, _ interface{}) {
got, seen = message, seen+1
}
tc.call(n)
// A producer that stopped pushing would otherwise leave `got` empty and compare unequal
// with a confusing message; say which it was.
if seen == 0 {
t.Fatalf("the producer pushed no event at all — the golden below was never compared")
}
if got != tc.want {
t.Errorf("event message moved (slice 2 may not translate what the hub mails)\n"+
" got %q\n want %q", got, tc.want)
}
})
}
}
// TestEventMessagesCarryNoBundleKey is the other half: a converted producer would push a KEY, or a
// sentence assembled from one. Neither can look Hungarian, so this catches the accident the golden
// table above would miss for a producer it does not list.
func TestEventMessagesCarryNoBundleKey(t *testing.T) {
var msgs []string
n := &Notifier{}
n.pushFn = func(_, _, message, _ string, _ interface{}) { msgs = append(msgs, message) }
n.NotifyAppDeployed("privatebin", "PrivateBin")
n.NotifyAppRemoved("privatebin", "PrivateBin")
n.NotifyStorageReconnected("Kulso HDD")
n.NotifyDRStarted(1)
n.NotifyCrossDriveCompleted(CrossDriveDetails{StackName: "privatebin"})
if len(msgs) == 0 {
t.Fatal("no producer pushed — nothing was checked")
}
for _, m := range msgs {
// A bundle key is lower-case, dotted and has no space: "event.app_deployed".
if !strings.Contains(m, " ") && strings.Contains(m, ".") && m == strings.ToLower(m) {
t.Errorf("an event message looks like a bundle key, not a sentence: %q", m)
}
}
}