hub v0.118.1: the test mail follows the language too (R-558)
gates / gates (push) Successful in 23s

Found during v0.118.0's own live proof, which is the only reason it was found:
I went to press "send test notification" for the English demo box and read
sendTestEmail first.

It had its own hardcoded Hungarian subject and body and never went through
FormatCustomerEmail, so it was the one customer mail v0.118.0 did not localise
— and it is the only customer mail an operator can trigger on demand, which
makes it the one most likely to be used to check whether the localisation
works. Pressing the button for an English household would have answered that
question wrongly, and convincingly.

The two sentences are extracted byte-for-byte into the bundle, so the Hungarian
test mail is unchanged. Red-proofed against the hardcoded version.

The general form worth keeping: the surface you would use to CHECK a feature is
the one most worth checking first. A broken instrument that reports success is
worse than a broken feature.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-18 16:25:20 +02:00
parent 9167cf53af
commit a2c52ebf2a
6 changed files with 82 additions and 5 deletions
+18
View File
@@ -1,3 +1,21 @@
## v0.118.1 — the test mail follows the language too (2026-09-18, R-558)
Found during v0.118.0's own live proof, which is the only reason it was found: I went to press "send
test notification" for the English demo box and read `sendTestEmail` first.
- **`sendTestEmail` had its own hardcoded Hungarian subject and body** and never went through
`FormatCustomerEmail`, so it was the one customer mail v0.118.0 did not localise. It is also **the
only customer mail an operator can trigger on demand** — which makes it the one most likely to be
used to check whether the localisation works. Left alone, pressing the button for an English
household would have answered that question wrongly, and convincingly.
- Its two sentences are now `mail.test.subject` / `mail.test.body` in the bundle, extracted
byte-for-byte from the literals so the Hungarian test mail is unchanged.
- `TestTestMailFollowsTheHouseholdLanguage`, red-proofed against the hardcoded version ("en test mail
subject = \"[Felhom] Teszt értesítés\", want it to contain \"Test notification\"").
**The general form worth keeping: the surface you would use to CHECK a feature is the one most worth
checking first.** A broken instrument that reports success is worse than a broken feature.
## v0.118.0 — the household's e-mails follow the household's language (2026-09-18, R-558 / R-555) ## v0.118.0 — the household's e-mails follow the household's language (2026-09-18, R-558 / R-555)
**Localisation slice 3, Part A.** The hub has written every customer e-mail in Hungarian whatever the **Localisation slice 3, Part A.** The hub has written every customer e-mail in Hungarian whatever the
+3 -1
View File
@@ -77,5 +77,7 @@
"bind.locked.body": "There were too many failed attempts. For safety the link has locked — contact support to link the box.", "bind.locked.body": "There were too many failed attempts. For safety the link has locked — contact support to link the box.",
"bind.invalid.lead": "This link is invalid or has expired.", "bind.invalid.lead": "This link is invalid or has expired.",
"bind.invalid.body": "The link is valid for 7 days. If it has expired, ask support for a new one, or your operator can do the linking.", "bind.invalid.body": "The link is valid for 7 days. If it has expired, ask support for a new one, or your operator can do the linking.",
"bind.htmllang": "en" "bind.htmllang": "en",
"mail.test.subject": "[Felhom] Test notification",
"mail.test.body": "Dear Customer,\n\nThis is a test notification from the Felhom monitoring system.\nNotifications are working correctly.\n\nBest regards,\nFelhom.eu monitoring"
} }
+3 -1
View File
@@ -77,5 +77,7 @@
"bind.locked.body": "Túl sok sikertelen próbálkozás történt. Biztonsági okból a hivatkozás zárolódott — kérjük, vedd fel a kapcsolatot az ügyfélszolgálattal a doboz összekötéséhez.", "bind.locked.body": "Túl sok sikertelen próbálkozás történt. Biztonsági okból a hivatkozás zárolódott — kérjük, vedd fel a kapcsolatot az ügyfélszolgálattal a doboz összekötéséhez.",
"bind.invalid.lead": "Ez a hivatkozás érvénytelen vagy lejárt.", "bind.invalid.lead": "Ez a hivatkozás érvénytelen vagy lejárt.",
"bind.invalid.body": "A hivatkozás 7 napig érvényes. Ha lejárt, kérj újat az ügyfélszolgálattól, vagy az összekötést az üzemeltető is elvégezheti.", "bind.invalid.body": "A hivatkozás 7 napig érvényes. Ha lejárt, kérj újat az ügyfélszolgálattól, vagy az összekötést az üzemeltető is elvégezheti.",
"bind.htmllang": "hu" "bind.htmllang": "hu",
"mail.test.subject": "[Felhom] Teszt értesítés",
"mail.test.body": "Kedves Ügyfél!\n\nEz egy teszt értesítés a Felhom monitoring rendszerből.\nAz értesítések megfelelően működnek.\n\nÜdvözlettel,\nFelhom.eu monitoring"
} }
+13 -2
View File
@@ -12,6 +12,8 @@ import (
"time" "time"
"gitea.dooplex.hu/admin/felhom-hub/internal/store" "gitea.dooplex.hu/admin/felhom-hub/internal/store"
"gitea.dooplex.hu/admin/felhom-hub/internal/i18n"
) )
// Dispatcher routes events to operator and/or customer email channels. // Dispatcher routes events to operator and/or customer email channels.
@@ -199,8 +201,17 @@ func (d *Dispatcher) sendTestEmail(customerID string) {
if err != nil || prefs == nil || prefs.Email == "" { if err != nil || prefs == nil || prefs.Email == "" {
d.logger.Printf("[WARN] Test email: no email configured for %s", customerID) d.logger.Printf("[WARN] Test email: no email configured for %s", customerID)
} else { } else {
subject := "[Felhom] Teszt értesítés" // The test mail follows the household's language like every other customer mail (R-558).
body := "Kedves Ügyfél!\n\nEz egy teszt értesítés a Felhom monitoring rendszerből.\nAz értesítések megfelelően működnek.\n\nÜdvözlettel,\nFelhom.eu monitoring" //
// It was the ONE that would not have: it is the only customer mail with its own hardcoded
// text, because it never goes through FormatCustomerEmail — and it is also the only one an
// operator can trigger on demand, so it is the one most likely to be used to CHECK whether
// the localisation works. Left alone, pressing "send test" for an English household would
// have answered that question wrongly.
b := i18n.Shared()
lang := d.store.CustomerLanguage(customerID)
subject := b.Msg(lang, "mail.test.subject")
body := b.Msg(lang, "mail.test.body")
if err := d.sendEmailFn(prefs.Email, subject, body, nil); err != nil { if err := d.sendEmailFn(prefs.Email, subject, body, nil); err != nil {
d.logger.Printf("[ERROR] Test email to %s failed: %v", prefs.Email, err) d.logger.Printf("[ERROR] Test email to %s failed: %v", prefs.Email, err)
+44
View File
@@ -1,10 +1,13 @@
package notify package notify
import ( import (
"io"
"log"
"strings" "strings"
"testing" "testing"
"gitea.dooplex.hu/admin/felhom-hub/internal/i18n" "gitea.dooplex.hu/admin/felhom-hub/internal/i18n"
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
) )
// Localisation slice 3 (R-558) — the behaviour the bundle exists to produce. // Localisation slice 3 (R-558) — the behaviour the bundle exists to produce.
@@ -123,3 +126,44 @@ func TestUnknownLanguageRendersHungarian(t *testing.T) {
} }
} }
} }
// The TEST mail follows the household's language too.
//
// It is the one customer mail with its own text — it never goes through FormatCustomerEmail — and
// it is also the only one an operator can trigger on demand, so it is the one most likely to be
// used to CHECK whether the localisation works. It was still hardcoded Hungarian when the rest of
// this release was finished; pressing "send test" for an English household would have answered that
// question wrongly, in the most convincing possible way.
func TestTestMailFollowsTheHouseholdLanguage(t *testing.T) {
for _, tc := range []struct{ lang, want, notWant string }{
{"en", "Test notification", "Teszt"},
{"hu", "Teszt értesítés", "Test notification"},
} {
st := newDispStore(t)
if err := st.SaveCustomerConfig(&store.CustomerConfig{
CustomerID: "c1", APIKey: "k", RetrievalPassword: "p", Language: tc.lang,
}); err != nil {
t.Fatal(err)
}
if err := st.SaveNotificationPrefs("c1", "fam@example.com", []string{"test"}, 6); err != nil {
t.Fatal(err)
}
d := NewDispatcher(st, "test-key", "from@felhom.eu", "", false, log.New(io.Discard, "", 0))
var subject, body string
d.sendEmailFn = func(_, s, b string, _ map[string]string) error {
subject, body = s, b
return nil
}
d.ProcessEvent("c1", "test", "info", "", "{}", "hub")
if !strings.Contains(subject, tc.want) {
t.Errorf("%s test mail subject = %q, want it to contain %q", tc.lang, subject, tc.want)
}
if strings.Contains(subject, tc.notWant) || strings.Contains(body, tc.notWant) {
t.Errorf("%s test mail carries the other language (%q):\n%s\n%s", tc.lang, tc.notWant, subject, body)
}
if body == "" {
t.Fatalf("%s test mail has an empty body — the seam did not fire", tc.lang)
}
}
}
+1 -1
View File
@@ -128,7 +128,7 @@ spec:
spec: spec:
containers: containers:
- name: hub - name: hub
image: gitea.dooplex.hu/admin/felhom-hub:0.118.0 image: gitea.dooplex.hu/admin/felhom-hub:0.118.1
ports: ports:
- containerPort: 8080 - containerPort: 8080
name: http name: http