4c3c2037fd
49 Hungarian values on the debug (RESET prompt), storage, network-storage, security, system, deploy, restore and remote-backup copy moved from the formal („ön") to the product's te-form („írd be", „add meg", „hozz létre", „biztosan eltávolítod", „engedélyezd", „próbáld", ...). i18n_missing_gate.py's stem list widened by 33 stems (+ one listed third-person exception, „sora adja meg"); it counted 61 on the previous bundle and counts 0 now, ceiling stays 0. Decoys: a widened-stem form convicts, its te-form twin and the third-person phrase pass. 38 parity fixtures changed by exactly those bytes. Seven Go-side keys listed as REWORDED in the go-parity map. Pinned by TestR516_WidenedFormalFormsAreGone; two tests that quoted the old words follow them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
198 lines
9.2 KiB
Go
198 lines
9.2 KiB
Go
package web
|
|
|
|
import (
|
|
"io"
|
|
"log"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/i18n"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/monitor"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
|
)
|
|
|
|
// R-516 item 8 — a disconnected drive showed TWO banners on every page: its own
|
|
// (alert.storage.disconnected, in the household's language) and the health check's Hungarian
|
|
// „Meghajtó leválasztva" warning for the same drive. The dedicated banner is the one kept.
|
|
//
|
|
// COMPANION RED-PROOF: drop the WarnKindStorageDisconnected `continue` in AlertManager.Refresh — the
|
|
// first half fails with two banners.
|
|
func TestR516_DisconnectedDriveHasOneBanner(t *testing.T) {
|
|
const warn = "Meghajtó leválasztva: Külső (/mnt/usb)"
|
|
hr := &monitor.HealthReport{Status: "warn", Warnings: []string{warn, "CPU usage high: 91%"},
|
|
WarningKinds: []string{monitor.WarnKindStorageDisconnected, ""}}
|
|
cfg := &config.Config{}
|
|
cfg.Hub.Enabled = false
|
|
sp := []settings.StoragePath{{Path: "/mnt/usb", Label: "Külső", Disconnected: true}}
|
|
|
|
count := func(alerts []Alert) (dedicated, health, cpu int) {
|
|
for _, a := range alerts {
|
|
switch {
|
|
case strings.HasPrefix(a.ID, "storage-disconnected-"):
|
|
dedicated++
|
|
case a.Message == warn:
|
|
health++
|
|
case a.Message == "CPU usage high: 91%":
|
|
cpu++
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
am := NewAlertManager(log.New(io.Discard, "", 0))
|
|
am.Refresh(hr, cfg, nil, false, "", sp)
|
|
d, h, c := count(am.GetAlerts("hu"))
|
|
if d != 1 || h != 0 {
|
|
t.Errorf("a disconnected drive must have exactly one banner: dedicated=%d health-warning=%d", d, h)
|
|
}
|
|
if c != 1 {
|
|
t.Errorf("positive control: another health warning must still reach the banners (got %d)", c)
|
|
}
|
|
|
|
// Without the storage list there is no dedicated banner, so the health warning is the only one
|
|
// that can say it — it must stay.
|
|
am2 := NewAlertManager(log.New(io.Discard, "", 0))
|
|
am2.Refresh(hr, cfg, nil, false, "")
|
|
if d, h, _ := count(am2.GetAlerts("hu")); d != 0 || h != 1 {
|
|
t.Errorf("without the storage list the health warning is the only banner: dedicated=%d health-warning=%d", d, h)
|
|
}
|
|
}
|
|
|
|
// R-516 item 7 — the storage page printed the disconnect time as the raw stored RFC3339 UTC string
|
|
// („Leválasztva: 2026-09-14T19:58:02Z"). It now goes through fmtTimeStr, the page's local-time format,
|
|
// in both languages.
|
|
//
|
|
// COMPANION RED-PROOF: put `{{.DisconnectedAt}}` back in storage.levalasztva_2 (hu.json) — the hu half
|
|
// fails with the raw string.
|
|
func TestR516_DisconnectTimeIsLocalNotRawUTC(t *testing.T) {
|
|
const raw = "2026-09-14T19:58:02Z"
|
|
for _, tc := range []struct{ lang, prefix string }{{"hu", "Leválasztva: "}, {"en", "Disconnected: "}} {
|
|
d := i18nLayoutData("storage", "Tárhely")
|
|
paths := i18nStoragePaths()
|
|
paths[0]["DisconnectedAt"] = raw
|
|
d["StoragePaths"] = paths
|
|
h := renderBackupPageLang(t, tc.lang, "storage", d)
|
|
if strings.Contains(h, raw) {
|
|
t.Errorf("%s: the raw UTC timestamp reached the page", tc.lang)
|
|
}
|
|
if want := tc.prefix + fmtRFC3339Local(raw); !strings.Contains(h, want) {
|
|
t.Errorf("%s: the page must carry %q", tc.lang, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// R-516 item 10 — a health warning with a dashboard sentence shows it in the household's language;
|
|
// a warning without one shows its wire text verbatim (the fail-open fallback).
|
|
func TestR516_HealthBannersFollowTheHousehold(t *testing.T) {
|
|
hr := &monitor.HealthReport{Status: "fail",
|
|
Issues: []string{"SSD disk usage critical: 95%"},
|
|
IssueMsgs: []monitor.MsgRef{{Key: "health.system_disk_critical", Args: []interface{}{95.0}}},
|
|
Warnings: []string{"SSD disk usage high: 90%", "Docker: something"}, WarningKinds: []string{"", ""},
|
|
WarningMsgs: []monitor.MsgRef{{Key: "health.system_disk_high", Args: []interface{}{90.0}}, {}}}
|
|
cfg := &config.Config{}
|
|
cfg.Hub.Enabled = false
|
|
am := NewAlertManager(log.New(io.Discard, "", 0))
|
|
am.Refresh(hr, cfg, nil, false, "")
|
|
got := map[string][]string{}
|
|
for _, lang := range []string{"hu", "en"} {
|
|
for _, a := range am.GetAlerts(lang) {
|
|
if strings.HasPrefix(a.ID, "health-") {
|
|
got[lang] = append(got[lang], a.Message)
|
|
}
|
|
}
|
|
}
|
|
want := map[string][]string{
|
|
"hu": {"A rendszer tárhelye majdnem megtelt: 95% foglalt", "A rendszer tárhelye fogy: 90% foglalt", "Docker: something"},
|
|
"en": {"System storage is almost full: 95% used", "System storage is filling up: 90% used", "Docker: something"},
|
|
}
|
|
for lang, w := range want {
|
|
if strings.Join(got[lang], "|") != strings.Join(w, "|") {
|
|
t.Errorf("%s banners:\n got %q\n want %q", lang, got[lang], w)
|
|
}
|
|
}
|
|
}
|
|
|
|
// R-516 item 12 — the last formal („ön") forms in the dashboard copy are in the product's te-form.
|
|
// The i18n gate's ratchet (HU_FORMAL_CEILING = 0) keeps new ones out; this pins the reworded bytes.
|
|
func TestR516_FormalFormsAreGone(t *testing.T) {
|
|
b, err := i18n.Shared()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := map[string]string{
|
|
"alert.deadapp.group": "%d telepített alkalmazás nem fut — nézd meg a rendszermonitort",
|
|
"flash.login.password_changed": "Jelszó sikeresen módosítva. Jelentkezz be az új jelszóval.",
|
|
"launcher.biztosan_kikapcsolja_a_megosztast_a": "Biztosan kikapcsolod a megosztást? A link azonnal érvénytelenné válik.",
|
|
"launcher.olvassa_be_telefonnal_a_gyors": "Olvasd be telefonnal a gyors megnyitáshoz.",
|
|
"deploy.a_kontener_leallt_ellenorizze_a": "A konténer leállt. Ellenőrizd a naplókat.",
|
|
"debug.kattintson_a_gombra_a_telemetria": "Kattints a gombra a telemetria futtatásához.",
|
|
"app_import.a_feltoltes_megszakadt_ellenorizze_a": "A feltöltés megszakadt — ellenőrizd a kapcsolatot, majd próbáld újra.",
|
|
}
|
|
for key, w := range want {
|
|
got, fellBack, ok := b.Text("hu", key)
|
|
if !ok || fellBack || got != w {
|
|
t.Errorf("%s: %q, want %q", key, got, w)
|
|
}
|
|
}
|
|
// Negative control over EVERY Hungarian value: the formal stems this row removed do not return.
|
|
// ASCII-folded, so an accent difference cannot hide one.
|
|
fold := strings.NewReplacer("á", "a", "é", "e", "í", "i", "ó", "o", "ö", "o", "ő", "o", "ú", "u", "ü", "u", "ű", "u")
|
|
stems := []string{"kerjuk, jelentkezzen", "nezze meg", "olvassa be", "kattintson a", "ellenorizze", "ujratelepiti az", "vegye fel a kapcsolatot", "huzza ide"}
|
|
for _, key := range b.Keys("hu") {
|
|
v, _, _ := b.Text("hu", key)
|
|
lv := fold.Replace(strings.ToLower(v))
|
|
for _, st := range stems {
|
|
if strings.Contains(lv, st) {
|
|
t.Errorf("%s still carries the formal %q: %q", key, st, v)
|
|
}
|
|
}
|
|
}
|
|
// Positive control: the fold must see a te-form stem, or the loop above judged nothing.
|
|
if lv := fold.Replace(strings.ToLower(want["deploy.a_kontener_leallt_ellenorizze_a"])); !strings.Contains(lv, "ellenorizd") {
|
|
t.Fatal("the ASCII fold does not work — the negative control proves nothing")
|
|
}
|
|
}
|
|
|
|
// R-516 (2026-10-06) — the formal („ön") forms the gate's first stem list did not see: the debug RESET
|
|
// prompt, the storage and network-storage confirms and guides, the security and system pages, the
|
|
// deploy and restore copy. They are in the te-form now; the i18n gate's widened stems keep new ones out.
|
|
func TestR516_WidenedFormalFormsAreGone(t *testing.T) {
|
|
b, err := i18n.Shared()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := map[string]string{
|
|
"debug.irja_be_reset": "Írd be: RESET",
|
|
"debug.api.reset_confirm_invalid": "Érvénytelen megerősítés — írd be: RESET",
|
|
"settings_security.adja_meg_a_jelenlegi_jelszavat": "Add meg a jelenlegi jelszavad",
|
|
"storage.biztosan_eltavolitja_adattarolot": "Biztosan eltávolítod a(z) {{.Path}} adattárolót?",
|
|
"storage_network.engedelyezze_a_felhom_gep_ip": "Engedélyezd a Felhom gép IP-címét a megosztáson. Proxmox-fürt esetén minden csomópont IP-címét.",
|
|
"backups_remote.a_letetben_levo_helyreallitasi_csomag": "A letétben lévő helyreállítási csomag nem fedi a jelenlegi távoli mentési jelszót. Hozz létre új helyreállítási kódot.",
|
|
"err.selfupdate.mentes_fut_probalja_kesobb": "Mentés fut, próbáld később",
|
|
"flash.backup.window_invalid_time": "Érvénytelen időpont. Használd az ÓÓ:PP formátumot (például 02:30).",
|
|
}
|
|
for key, w := range want {
|
|
got, fellBack, ok := b.Text("hu", key)
|
|
if !ok || fellBack || got != w {
|
|
t.Errorf("%s: %q, want %q", key, got, w)
|
|
}
|
|
}
|
|
fold := strings.NewReplacer("á", "a", "é", "e", "í", "i", "ó", "o", "ö", "o", "ő", "o", "ú", "u", "ü", "u", "ű", "u")
|
|
stems := []string{"irja be", "hozzon letre", "biztosan eltavolitja", "engedelyezze", "probalja", "valasszon",
|
|
"jelezze", "biztosan ujrainditja", "folytatja?", "szabaditson", "mentse el", "tavolitsa el"}
|
|
for _, key := range b.Keys("hu") {
|
|
v, _, _ := b.Text("hu", key)
|
|
lv := fold.Replace(strings.ToLower(v))
|
|
for _, st := range stems {
|
|
if strings.Contains(lv, st) {
|
|
t.Errorf("%s still carries the formal %q: %q", key, st, v)
|
|
}
|
|
}
|
|
}
|
|
// Positive control: the fold must turn a formal sentence into a matching stem, or the loop judged nothing.
|
|
if lv := fold.Replace(strings.ToLower("Írja be a nevet")); !strings.Contains(lv, "irja be") {
|
|
t.Fatal("the ASCII fold does not work — the negative control proves nothing")
|
|
}
|
|
}
|