Files
felhom-controller/controller/internal/web/backup_target_absent_test.go
T
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

218 lines
10 KiB
Go

package web
import (
"context"
"strings"
"testing"
"gitea.dooplex.hu/admin/felhom-controller/internal/agentapi"
"gitea.dooplex.hu/admin/felhom-controller/internal/i18n"
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
)
// R-114 — the third state: CONFIGURED, and its drive is GONE.
//
// THE BUG THESE PIN, measured live in E-2d (felhom.eu audits/E2D-fresh-vm-2026-07-29.md §5.3). With
// the assigned target's drive detached, the endpoint returned:
//
// {"degraded":true,"target":"felhom-backup",
// "message":"A rendszermentés jelenleg ugyanazon a lemezen van, mint a rendszer …",
// "offer_path":"/mnt/felhom-drives/mentes2","offer_label":"Mentés meghajtó"}
//
// Two falsehoods in one payload. The backup was NOT on the system disk — it was on a drive that had
// vanished. And the remedy offered was THE DRIVE THAT JUST DISAPPEARED. resolveBackupTargetState had
// only two outcomes (a disk claims the target, or nothing does) so the third state fell into the
// second and inherited its message and its offer.
//
// The fixtures below are the observed shapes, not invented ones: the drive is still listed with a
// MountPath (the registry keeps the configured path), Role user-data, but BackupTarget is false —
// which is exactly why the healthy branch missed it and the offer branch matched it.
// absentHarness wires a Server whose agent reports `primary` as the tier target and `disks` as the
// drive list, with one registered storage path so firstOfferableDrive has something to find. Both
// halves come through the production seams (tiersFn / disksFn), so the resolver under test is the
// real one.
func absentHarness(t *testing.T, primary string, disks []agentapi.DiskInfo, registerPath string) *Server {
t.Helper()
s := testServer(t)
if registerPath != "" {
if err := s.settings.AddStoragePath(settings.StoragePath{Path: registerPath, Label: "Mentés meghajtó"}); err != nil {
t.Fatalf("register storage path: %v", err)
}
}
s.tiersFn = func(context.Context) (agentapi.TiersResponse, error) {
return agentapi.TiersResponse{Tiers: []agentapi.BackupTierInfo{{Target: primary, Primary: true}}}, nil
}
s.disksFn = func(context.Context) (agentapi.DisksResponse, error) {
return agentapi.DisksResponse{Disks: disks}, nil
}
return s
}
// huText renders a bundle KEY into the Hungarian a household actually reads.
//
// v0.259.0 (R-598) turned the three backup-protection constants into bundle keys, so every
// assertion below would otherwise have quietly become an assertion about key SPELLING — the exact
// hollowing this repo has paid for before. Resolving through the real embedded bundle keeps them
// assertions about the WORDS, and fails loudly if a key is ever dropped from hu.json.
func huText(t *testing.T, key string) string {
t.Helper()
if key == "" {
return ""
}
b, err := i18n.Shared()
if err != nil {
t.Fatalf("i18n bundle: %v", err)
}
got := b.Msg("hu", key)
if got == key {
t.Fatalf("the bundle does not know %q — it would render the KEY onto the page", key)
}
return got
}
// theVanishedDrive is the E-2d shape: registered, still carrying its configured MountPath, user-data,
// but no longer claiming the backup target because its device is gone.
var theVanishedDrive = agentapi.DiskInfo{
Name: "mentes2",
MountPath: "/mnt/mentes2",
GuestPath: "/mnt/felhom-drives/mentes2",
Role: "user-data",
BackupTarget: false,
}
// ── Scenario C — configured, drive absent ───────────────────────────────────────────────────────
// RED-PROOF: delete the `if targetIsConfiguredDrive(primary)` branch from resolveBackupTargetState
// and this fails twice — the message becomes the system-disk copy and OfferPath becomes non-empty.
func TestConfiguredButAbsentTargetDoesNotClaimTheSystemDisk(t *testing.T) {
s := absentHarness(t, "felhom-backup", []agentapi.DiskInfo{theVanishedDrive}, "/mnt/felhom-drives/mentes2")
st := s.resolveBackupTargetState(context.Background())
if !st.Known {
t.Fatal("state is unknown — the fixture answers both agent calls")
}
if !st.TargetAbsent {
t.Fatalf("TargetAbsent is false for target %q with no disk claiming it — the configured-but-gone "+
"state fell back into never-configured, which is the R-114 bug", st.TargetID)
}
msg := huText(t, degradedMessageFor(st))
if strings.Contains(msg, "ugyanazon a lemezen") {
t.Errorf("the customer is told the backup is on the SYSTEM DISK, which is false — the target is "+
"%q, a drive that has vanished. Got: %s", st.TargetID, msg)
}
if !strings.Contains(msg, "nem érhető el") {
t.Errorf("the absent-drive copy is missing; got: %s", msg)
}
}
// The remedy must be "reconnect THAT drive", never "pick a different one" — and above all never the
// drive that just disappeared, which is what E-2d actually observed being offered.
func TestConfiguredButAbsentTargetOffersNothing(t *testing.T) {
s := absentHarness(t, "felhom-backup", []agentapi.DiskInfo{theVanishedDrive}, "/mnt/felhom-drives/mentes2")
st := s.resolveBackupTargetState(context.Background())
if st.OfferPath != "" {
t.Errorf("offered %q while the configured target is absent — E-2d observed this exact payload "+
"offering the drive that had just vanished", st.OfferPath)
}
if st.OfferLabel != "" {
t.Errorf("offer label %q leaked with no offer path", st.OfferLabel)
}
}
// The suppression must be the branch's own doing, NOT a side effect of the drive being flagged
// Disconnected — that flag is set by the agent-side drive gate in another repo (R-113), and this
// state has to be correct before, during and independently of that landing.
func TestAbsentTargetSuppressesOfferWithoutRelyingOnTheDisconnectedFlag(t *testing.T) {
s := absentHarness(t, "felhom-backup", []agentapi.DiskInfo{theVanishedDrive}, "/mnt/felhom-drives/mentes2")
for _, sp := range s.settings.GetStoragePaths() {
if sp.Disconnected {
t.Fatalf("fixture invalid: %s is already marked Disconnected, so this test would pass "+
"for the wrong reason", sp.Path)
}
}
if st := s.resolveBackupTargetState(context.Background()); st.OfferPath != "" {
t.Errorf("offer %q survived with Disconnected UNSET — the suppression is leaning on the "+
"other repo's flag instead of on this state", st.OfferPath)
}
}
// ── Scenario A — never configured (must be unchanged) ───────────────────────────────────────────
func TestNeverConfiguredStillSaysSystemDiskAndStillOffers(t *testing.T) {
s := absentHarness(t, builtinLocalTarget, []agentapi.DiskInfo{{
Name: "hdd1", MountPath: "/mnt/hdd1", GuestPath: "/mnt/felhom-drives/hdd1", Role: "user-data",
}}, "/mnt/felhom-drives/hdd1")
st := s.resolveBackupTargetState(context.Background())
if st.TargetAbsent {
t.Fatal("target `local` marked absent — the builtin root-fs storage is the NEVER-CONFIGURED " +
"state, not a drive that went missing")
}
if !st.Degraded {
t.Fatal("a backup on the system disk must still read degraded")
}
if msg := huText(t, degradedMessageFor(st)); !strings.Contains(msg, "ugyanazon a lemezen") {
t.Errorf("the never-configured copy changed; got: %s", msg)
}
if st.OfferPath == "" {
t.Error("no offer in the never-configured state — this is exactly where an offer belongs")
}
}
// An unset tier is the same customer situation as `local`: nothing chosen yet, so it must not be
// reported as a drive that vanished.
func TestUnsetTierIsNeverConfiguredNotAbsent(t *testing.T) {
if targetIsConfiguredDrive("") {
t.Error("an empty target id was treated as a configured drive")
}
if targetIsConfiguredDrive(builtinLocalTarget) {
t.Error("the builtin `local` was treated as a configured drive")
}
if !targetIsConfiguredDrive("felhom-backup") {
t.Error("a real storage id was not treated as a configured drive")
}
}
// ── Scenario B — healthy still renders nothing ──────────────────────────────────────────────────
func TestHealthyTargetIsNeitherDegradedNorAbsent(t *testing.T) {
s := absentHarness(t, "felhom-backup", []agentapi.DiskInfo{{
Name: "mentes2", MountPath: "/mnt/mentes2", GuestPath: "/mnt/felhom-drives/mentes2",
Role: "user-data", BackupTarget: true,
}}, "/mnt/felhom-drives/mentes2")
st := s.resolveBackupTargetState(context.Background())
if st.Degraded || st.TargetAbsent {
t.Fatalf("a healthy target reported degraded=%v absent=%v", st.Degraded, st.TargetAbsent)
}
if msg := huText(t, degradedMessageFor(st)); msg != "" {
t.Errorf("healthy produced copy %q — a working box must look normal", msg)
}
}
// ── the copy contract ───────────────────────────────────────────────────────────────────────────
// The absent copy is VERBATIM the hub's customerMessages["backup_target_absent"]. If either side is
// reworded this fails, which is the only thing currently binding the two repos' strings together.
func TestAbsentCopyMatchesTheHubEmailWordForWord(t *testing.T) {
const hubCopy = "A rendszermentés meghajtója nem érhető el — amíg vissza nem " +
"csatlakoztatod, a teljes rendszermentés nem készül el."
banner := huText(t, backupTargetAbsentKey)
if banner != hubCopy {
t.Errorf("the banner and the email now tell the customer different stories about one drive.\n"+
" banner: %s\n email : %s", banner, hubCopy)
}
msg := huText(t, degradedMessageFor(BackupTargetState{Known: true, Degraded: true, TargetAbsent: true}))
for _, want := range []struct{ frag, why string }{
{"nem érhető el", "the FACT — the drive cannot be reached"},
{"nem készül el", "the CONSEQUENCE — the full system backup does not happen"},
{"vissza nem csatlakoztatod", "the REMEDY — reconnect it"},
} {
if !strings.Contains(msg, want.frag) {
t.Errorf("absent copy is missing %s (%q); got: %s", want.why, want.frag, msg)
}
}
}