package web import ( "context" "strings" "testing" "gitea.dooplex.hu/admin/felhom-controller/internal/agentapi" "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 } // 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 := 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 := 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 := 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." if backupTargetAbsentText != hubCopy { t.Errorf("the banner and the email now tell the customer different stories about one drive.\n"+ " banner: %s\n email : %s", backupTargetAbsentText, hubCopy) } msg := 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) } } }