Files
felhom.eu/hub/internal/notify/language_test.go
T
admin 9167cf53af
gates / gates (push) Successful in 23s
hub v0.118.0: the household's e-mails follow the household's language (R-558 Part A)
The hub has written every customer e-mail in Hungarian whatever the box was set
to. The box has published its language since controller v0.247.0; nothing read
it. Now it does.

Nothing an operator reads changes. The Hungarian mails are byte-identical, and
that is a diff rather than a reading: 56 goldens per language captured from
v0.117.0 BEFORE any string moved, and all 56 Hungarian ones pass unchanged after
every sentence was routed through the new bundle.

- internal/i18n: flat bundle, 79 keys, hu authoritative + hu fallback, ceiling 0.
- customerMessages/severityLabels are DERIVED from the bundle, so a sentence is
  written in one place and all 40+ tests that read those maps still work.
- Language order: last reported -> created-with -> hu. reports.language defaults
  to EMPTY, never hu: "never told us" is not "chose Hungarian".
- message_customer on POST /api/v1/event, additive and optional forever, for the
  sentences the box composes and the hub cannot translate.
- The bind page is per-language, and its `expired` state stays Hungarian: it is
  the state an unknown token lands in, so rendering a real English customer's
  token in English would make the LANGUAGE answer what the TEXT refuses to.

Two defects found inside the release:
- R-581: the newest report was picked by received_at, which has SECOND
  granularity, so same-second reports tied and the winner was arbitrary. Ordered
  by the autoincrement id now. GetCustomers() still has the shape - row open.
- R-582: the English copy-guard stems, ported word for word from Hungarian,
  convicted 141 honest sentences. The English claim is a phrase with a modal.

R-555 closed: the language allowlist entry is out of wire_contract_gate.py.
hub_copy_gate.py follows the sentences into the bundle - without that it would
have scanned four files that no longer hold any customer text and reported
success. Three new decoys incl. an innocent control.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-18 16:20:11 +02:00

126 lines
5.5 KiB
Go

package notify
import (
"strings"
"testing"
"gitea.dooplex.hu/admin/felhom-hub/internal/i18n"
)
// Localisation slice 3 (R-558) — the behaviour the bundle exists to produce.
//
// The goldens next door prove WHAT each mail says. These prove WHICH ONE a household gets, and that
// the operator's copy never moves.
// S3 — the box's own sentence.
//
// Roughly a third of the customer mails carry a sentence the BOX composed, naming a drive or an app
// or a number. The hub cannot translate one, so the box sends a second copy in the household's
// language. This is the test that the second copy is actually preferred — and that its absence
// leaves the mail exactly as it was, which is the state of every box in the fleet until the
// controller half ships.
func TestMessageCustomerPreferredForTheHousehold(t *testing.T) {
const hunSentence = "A /mnt/adat lemez 91%-ig megtelt (12,4 GB szabad)."
const engSentence = "The /mnt/adat disk is 91% full (12.4 GB free)."
// disk_warning has NO bundle entry on purpose, so the box's sentence IS the mail.
subject, body := FormatCustomerEmail("en", "c1", "disk_warning", "warning", hunSentence, engSentence, "")
if !strings.Contains(body, engSentence) {
t.Errorf("the English household's mail does not carry the English sentence:\n%s", body)
}
if strings.Contains(body, hunSentence) {
t.Errorf("the English household's mail still carries the Hungarian sentence:\n%s", body)
}
if !strings.Contains(subject, engSentence) {
t.Errorf("the subject is not the English sentence: %q", subject)
}
// The OPERATOR's mail is built from `message` and is untouched by any of this.
opSubject, opBody := FormatOperatorEmail("c1", "disk_warning", "warning", hunSentence, "")
if !strings.Contains(opBody, hunSentence) {
t.Errorf("the operator's mail lost the Hungarian sentence:\n%s", opBody)
}
if strings.Contains(opBody, engSentence) || strings.Contains(opSubject, engSentence) {
t.Error("the English sentence leaked into the operator's mail")
}
}
// S3, the other half: an old controller sends no second sentence, and the mail must be byte-for-byte
// what it is today. Compared against the RENDER WITH NO NEW FIELD AT ALL, not against a retyped
// expectation — a retyped one would pass even if both sides changed together.
func TestMessageCustomerAbsentIsExactlyToday(t *testing.T) {
const hunSentence = "A /mnt/adat lemez 91%-ig megtelt (12,4 GB szabad)."
subject, body := FormatCustomerEmail(i18n.Default, "c1", "disk_warning", "warning", hunSentence, "", "")
if !strings.Contains(body, hunSentence) || !strings.Contains(subject, hunSentence) {
t.Fatalf("an absent message_customer lost the box's sentence:\n%s\n%s", subject, body)
}
// And a Hungarian household that DOES get a second sentence gets the Hungarian one, because the
// box sends the same text twice. The point is that nothing about the hu path depends on it.
s2, b2 := FormatCustomerEmail(i18n.Default, "c1", "disk_warning", "warning", hunSentence, hunSentence, "")
if s2 != subject || b2 != body {
t.Error("a Hungarian household's mail changed when the box sent the sentence twice")
}
}
// The entry still WINS over the box's sentence, in both languages — the property that keeps the
// dynamic-message types (disk_warning and friends) working, stated from the other direction.
func TestEntryWinsAndTheBoxSentenceBecomesItsOwnLine(t *testing.T) {
const hun = "A cel nincs beallitva."
const eng = "The target is not set."
_, body := FormatCustomerEmail("en", "c1", "backup_failed", "error", hun, eng, "")
headline := i18n.Shared().Msg("en", "mail.event.backup_failed")
if !strings.Contains(body, headline) {
t.Errorf("the English entry is not the headline:\n%s", body)
}
if !strings.Contains(body, eng) {
t.Errorf("the box's English sentence is not on its own line:\n%s", body)
}
if strings.Contains(body, hun) {
t.Errorf("the Hungarian sentence reached an English household:\n%s", body)
}
// The line label is translated too — an English mail must not say "Üzenet".
if strings.Contains(body, "Üzenet") {
t.Errorf("the English mail carries the Hungarian field label:\n%s", body)
}
}
// Every mail kind actually differs between the two languages. Without this, a key that silently fell
// back to Hungarian would pass every other test here: the mail sends, it just sends in the wrong
// language, which is the exact defect this slice exists to remove.
func TestEveryCustomerMailDiffersBetweenLanguages(t *testing.T) {
for _, c := range customerMailCases("hu") {
if strings.HasPrefix(c.name, "operator_") {
continue // the operator's mails are English already and are not localised
}
name := c.name
huSubject, huBody := c.render()
var enSubject, enBody string
for _, e := range customerMailCases("en") {
if e.name == name {
enSubject, enBody = e.render()
}
}
if huBody == enBody {
t.Errorf("%s renders identically in both languages — it is not localised", name)
}
if huSubject == enSubject && !strings.Contains(huSubject, "[Felhom] Információ") {
t.Errorf("%s has the same subject in both languages: %q", name, huSubject)
}
}
}
// An unsupported or absent language renders Hungarian rather than a blank or a key.
func TestUnknownLanguageRendersHungarian(t *testing.T) {
want, wantBody := FormatCustomerEmail("hu", "c1", "backup_completed", "info", "", "", "")
for _, lang := range []string{"", "de", "en-GB", " "} {
got, gotBody := FormatCustomerEmail(lang, "c1", "backup_completed", "info", "", "", "")
if got != want || gotBody != wantBody {
t.Errorf("language %q did not fall back to Hungarian", lang)
}
}
}