Files
admin 0fe04bb6d5
gates / gates (push) Successful in 28s
v0.259.0 — the claim page and the backup warnings answer in the reader's language (R-596, R-598)
The 2026-09-20 English drill ended one screen short: the claim page was English
and its answers were Hungarian, so a household who mistyped the code from their
e-mail could not tell a typo from a dead code. Fourteen call sites carrying nine
messages now go through s.msg; the backup page's two protection warnings — which
are promises about whether the customer's files are safe — follow the same route.

Hungarian is byte-identical, proved structurally by the go-parity gate against the
frozen base capture and red-proofed on a single added full stop.

data["Title"] was DEAD (claim.html is standalone; .Title is layout.html's) and is
deleted rather than translated — a translated dead field is a permanent false
signal about where the page's title comes from.

Six existing copy-contract tests were kept, not weakened: each now resolves its key
through the real bundle, so it still convicts on a reworded Hungarian sentence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-21 07:45:56 +02:00

166 lines
6.8 KiB
Go

package web
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"gitea.dooplex.hu/admin/felhom-controller/internal/agentapi"
"gitea.dooplex.hu/admin/felhom-controller/internal/i18n"
)
// R-598 — THE TWO SENTENCES THAT SAY WHETHER THE CUSTOMER'S FILES ARE SAFE.
//
// The 2026-09-20 English drill found the Backup page's protection warnings still Hungarian on an
// otherwise English page. They are not decoration: they say "only one copy is being made" and "the
// system backup is on the same disk as the system, so it protects against bad files but not against
// a disk failure". A household who cannot read them believes they are protected when they are not.
//
// These tests render the REAL /backups page through the real handler in both languages, because
// that is the only instrument that can see this defect — the sentences arrive as struct fields, so
// the template parity fixtures render them faithfully whatever language they are in.
// renderBackupsPageLang is renderBackupsPage with a language cookie. Same production handler.
func renderBackupsPageLang(t *testing.T, lang, primary string, disks []agentapi.DiskInfo, registerPath string) string {
t.Helper()
s := absentHarness(t, primary, disks, registerPath)
s.loadTemplates()
req := httptest.NewRequest("GET", "/backups", nil)
if lang != "" {
req.AddCookie(&http.Cookie{Name: langCookieName, Value: lang})
}
rr := httptest.NewRecorder()
s.backupsHandler(rr, req)
if rr.Code != 200 {
t.Fatalf("backups page returned %d, want 200", rr.Code)
}
return rr.Body.String()
}
func enText(t *testing.T, key string) string {
t.Helper()
b, err := i18n.Shared()
if err != nil {
t.Fatal(err)
}
got := b.Msg("en", key)
if got == key {
t.Fatalf("en.json does not know %q", key)
}
return got
}
// S2 — the degraded warning and the offer follow the reader, in the state the drill actually saw:
// never configured, backup on the system disk, one drive attached.
func TestBackupWarningsFollowLanguage(t *testing.T) {
disks := []agentapi.DiskInfo{{
Name: "hdd1", MountPath: "/mnt/hdd1", GuestPath: "/mnt/felhom-drives/hdd1", Role: "user-data",
}}
const reg = "/mnt/felhom-drives/hdd1"
huHTML := renderBackupsPageLang(t, "hu", builtinLocalTarget, disks, reg)
for _, key := range []string{backupTargetDegradedKey, backupTargetOfferKey} {
want := huText(t, key)
if !strings.Contains(huHTML, want) {
t.Errorf("the HUNGARIAN backups page lost %q — the pre-v0.259.0 wording must be byte-identical", want)
}
}
enHTML := renderBackupsPageLang(t, "en", builtinLocalTarget, disks, reg)
for _, key := range []string{backupTargetDegradedKey, backupTargetOfferKey} {
if want := enText(t, key); !strings.Contains(enHTML, want) {
t.Errorf("the ENGLISH backups page is missing %q", want)
}
// The decisive half: the Hungarian sentence must be GONE, not merely accompanied.
if hu := huText(t, key); strings.Contains(enHTML, hu) {
t.Errorf("the HUNGARIAN sentence %q is still on the ENGLISH backups page", hu)
}
}
}
// The absent-drive state has the OPPOSITE remedy, and its English must promise exactly what the
// Hungarian promises: the FACT, the CONSEQUENCE, the REMEDY, in that order (§8's retrieval-promise
// rule applied to the English side).
func TestAbsentDriveWarningFollowsLanguageAndKeepsThePromise(t *testing.T) {
html := renderBackupsPageLang(t, "en", "felhom-backup",
[]agentapi.DiskInfo{theVanishedDrive}, "/mnt/felhom-drives/mentes2")
en := enText(t, backupTargetAbsentKey)
if !strings.Contains(html, en) {
t.Fatalf("the English absent-drive copy never reached the page; want %q", en)
}
if strings.Contains(html, huText(t, backupTargetAbsentKey)) {
t.Error("the Hungarian absent-drive copy is still on the English page")
}
for _, want := range []struct{ frag, why string }{
{"cannot be reached", "the FACT — the drive cannot be reached"},
{"is not made", "the CONSEQUENCE — the full system backup does not happen"},
{"reconnect it", "the REMEDY — reconnect THAT drive"},
} {
if !strings.Contains(en, want.frag) {
t.Errorf("the English absent copy is missing %s (%q); got: %s", want.why, want.frag, en)
}
}
// The English must not accidentally offer a different drive either.
if strings.Contains(html, `id="backup-target-assign"`) {
t.Error("an offer control rendered on the English page while the configured target is absent")
}
}
// The degraded sentence is a PROMISE about protection. Its English must draw the same distinction
// the Hungarian draws — corrupted files yes, disk failure no. An English text that promised
// protection against a disk failure would be worse than no translation at all.
func TestEnglishDegradedWarningPromisesExactlyWhatTheHungarianPromises(t *testing.T) {
en := enText(t, backupTargetDegradedKey)
for _, want := range []string{"same disk as the system", "corrupted files", "not against a disk failure", "second drive"} {
if !strings.Contains(en, want) {
t.Errorf("the English degraded warning is missing %q; got: %s", want, en)
}
}
// Hungarian says „hibás fájlok ellen véd, lemezhiba ellen nem" — protection is CONDITIONAL. A
// bare "protects against a disk failure" would invert it, so pin the negation's presence.
if !strings.Contains(en, "but not against a disk failure") {
t.Errorf("the English warning does not NEGATE disk-failure protection; got: %s", en)
}
}
// The tier labels on the same page: the builder is language-free now, and the words come from the
// reader's bundle. Asserted through buildTierViews rather than the page so a box with no agent
// still exercises it.
func TestTierLabelsFollowLanguage(t *testing.T) {
tiers := []agentapi.TierBackupState{
{Target: "local", Primary: true, Storage: "present"},
{Target: "felhom-pbs", Storage: "present"},
}
b, err := i18n.Shared()
if err != nil {
t.Fatal(err)
}
for _, lang := range []string{"hu", "en"} {
v := &guestBackupView{}
msg := func(key string, a ...interface{}) string {
if len(a) == 0 {
return b.Msg(lang, key)
}
return b.Msgf(lang, key, a...)
}
buildTierViews(v, tiers, map[string]int64{"local": 86400, "felhom-pbs": 604800}, tvNow, msg)
if len(v.Tiers) != 2 {
t.Fatalf("[%s] want 2 tier rows, got %d", lang, len(v.Tiers))
}
wantLocal, wantPBS := b.Msgf(lang, "backup.tier.local", "local"), b.Msg(lang, "backup.tier.pbs")
if v.Tiers[0].Label != wantLocal {
t.Errorf("[%s] local tier label = %q, want %q", lang, v.Tiers[0].Label, wantLocal)
}
if v.Tiers[1].Label != wantPBS {
t.Errorf("[%s] PBS tier label = %q, want %q", lang, v.Tiers[1].Label, wantPBS)
}
}
// And the two languages must actually differ — otherwise the loop above passes on an
// untranslated bundle and proves nothing.
if b.Msg("hu", "backup.tier.pbs") == b.Msg("en", "backup.tier.pbs") {
t.Error("backup.tier.pbs is identical in both languages — untranslated")
}
}