0626b9fc8a
gates / gates (push) Successful in 46s
Tier-2 and the one-copy notice ask the host agent (POST /disks/same-disk, agent 0.157.0): different + proven -> made and counted; same disk -> not made (D1); unknown (VM, hidden USB serial) -> made, not counted, the row says why (D2); a NAS never counts and no longer hides the notice (R-1075). An older agent keeps the st_dev rule, logged once. D4: a data-folder or media app on a box with no drive is refused in words with a link to the drives page (R-1068's raw-error half). MinAgent 0.157.0. Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
138 lines
6.1 KiB
Go
138 lines
6.1 KiB
Go
package backup
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
|
)
|
|
|
|
// felhom.eu architecture/16-storage-physical-identity.md §4 — the agent's verdict decides Tier-2 and the
|
|
// one-copy notice. Each test asserts the CONSEQUENCE (is a target picked, does it count, what does the page
|
|
// say), not the mechanism.
|
|
|
|
// judgeBy answers per target path; paths not in the map are "unmapped" (→ the old st_dev rule).
|
|
func judgeBy(m map[string]DiskVerdict) func(a, b string) (DiskVerdict, bool) {
|
|
return func(a, b string) (DiskVerdict, bool) {
|
|
if v, ok := m[b]; ok {
|
|
return v, true
|
|
}
|
|
return DiskVerdict{Reason: "unmapped"}, true
|
|
}
|
|
}
|
|
|
|
var (
|
|
provenDifferent = DiskVerdict{Different: true, Proven: true}
|
|
provenSame = DiskVerdict{Different: false, Proven: true, Reason: "same_disk"}
|
|
unknownVM = DiskVerdict{Different: true, Proven: false, Reason: "vm"}
|
|
)
|
|
|
|
func addDrive(t *testing.T, sett *settings.Settings, sp settings.StoragePath) {
|
|
t.Helper()
|
|
sp.Schedulable = true
|
|
if err := sett.AddStoragePath(sp); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// Row 1: different + proven → made, counted, notice hidden.
|
|
func TestDisk16_ProvenDifferent_CountedNoNotice(t *testing.T) {
|
|
m, sett := newTestManager(t, "/srv/sys")
|
|
addDrive(t, sett, settings.StoragePath{Path: "/mnt/b", Label: "B"})
|
|
m.SetDiskJudge(judgeBy(map[string]DiskVerdict{"/mnt/b": provenDifferent}))
|
|
tg, err := m.selectTier2TargetFrom("app", "/srv/sys", 1, 1)
|
|
if err != nil || tg.Unproven || tg.NamespaceRoot != "/mnt/b" {
|
|
t.Fatalf("want /mnt/b counted, got %+v err=%v", tg, err)
|
|
}
|
|
if w := m.singleCopyWarning(); w != "" {
|
|
t.Fatalf("a proven second disk must hide the notice, got %q", w)
|
|
}
|
|
}
|
|
|
|
// Row 2: same disk (two partitions) → NOT made (D1), notice shown — even though st_dev would say "different".
|
|
func TestDisk16_SameDisk_NotMadeAndNoticeShown(t *testing.T) {
|
|
m, sett := newTestManager(t, "/srv/sys")
|
|
addDrive(t, sett, settings.StoragePath{Path: "/mnt/part2", Label: "P2"})
|
|
m.SetDiskJudge(func(a, b string) (DiskVerdict, bool) { return provenSame, true })
|
|
if tg, err := m.selectTier2TargetFrom("app", "/mnt/part1", 1, 1); err == nil {
|
|
t.Fatalf("a same-disk partition must not be a Tier-2 target, got %+v", tg)
|
|
}
|
|
if w := m.singleCopyWarning(); w != m.note("note.backup.single_copy") {
|
|
t.Fatalf("same disk: want the one-copy notice, got %q", w)
|
|
}
|
|
}
|
|
|
|
// Row 3: unknown (a VM) → made, NOT counted (D2), notice shown with the "cannot check" sentence.
|
|
func TestDisk16_Unknown_MadeNotCountedCannotCheck(t *testing.T) {
|
|
m, sett := newTestManager(t, "/srv/sys")
|
|
addDrive(t, sett, settings.StoragePath{Path: "/mnt/felhom-drives/media_a", Label: "A"})
|
|
m.SetDiskJudge(func(a, b string) (DiskVerdict, bool) { return unknownVM, true })
|
|
tg, err := m.selectTier2TargetFrom("app", "/mnt/felhom-drives/adatlemez", 1, 1)
|
|
if err != nil || !tg.Unproven || tg.UnprovenReason != "vm" {
|
|
t.Fatalf("unknown: want the copy made and marked unproven, got %+v err=%v", tg, err)
|
|
}
|
|
w := m.singleCopyWarning()
|
|
if w != m.note("note.backup.single_copy_unproven") || !strings.Contains(w, "nem tudja ellenőrizni") {
|
|
t.Fatalf("unknown: want the cannot-check notice, got %q", w)
|
|
}
|
|
// A proven-different drive beats an unknown one.
|
|
addDrive(t, sett, settings.StoragePath{Path: "/mnt/felhom-drives/real", Label: "R"})
|
|
m.SetDiskJudge(judgeBy(map[string]DiskVerdict{"/mnt/felhom-drives/media_a": unknownVM, "/mnt/felhom-drives/real": provenDifferent}))
|
|
if tg, _ := m.selectTier2TargetFrom("app", "/mnt/felhom-drives/adatlemez", 1, 1); tg == nil || tg.NamespaceRoot != "/mnt/felhom-drives/real" || tg.Unproven {
|
|
t.Fatalf("a proven drive must win over an unknown one, got %+v", tg)
|
|
}
|
|
}
|
|
|
|
// Row 4: a network share → never a Tier-2 target and never hides the notice (R-1075).
|
|
func TestDisk16_NetworkShare_NeverHidesTheNotice(t *testing.T) {
|
|
m, sett := newTestManager(t, "/srv/sys")
|
|
addDrive(t, sett, settings.StoragePath{Path: "/mnt/felhom-drives/nas", Label: "NAS", Kind: settings.StorageKindNetwork})
|
|
// No judge at all — the OLD st_dev rule calls the NAS "different", which is exactly R-1075.
|
|
if m.hasOffDriveTarget() {
|
|
t.Fatal("R-1075: a NAS made the box claim an off-drive copy")
|
|
}
|
|
if w := m.singleCopyWarning(); w != m.note("note.backup.single_copy") {
|
|
t.Fatalf("a NAS must not hide the one-copy notice, got %q", w)
|
|
}
|
|
if tg, err := m.selectTier2TargetFrom("app", "/srv/sys", 1, 1); err == nil && !tg.IsSystemDrive {
|
|
t.Fatalf("a NAS must never be a Tier-2 target, got %+v", tg)
|
|
}
|
|
}
|
|
|
|
// An older agent (supported=false) keeps today's behaviour exactly: the st_dev seam decides.
|
|
func TestDisk16_OldAgent_KeepsTheStDevRule(t *testing.T) {
|
|
m, sett := newTestManager(t, "/srv/sys")
|
|
addDrive(t, sett, settings.StoragePath{Path: "/mnt/b", Label: "B"})
|
|
calls := 0
|
|
m.SetDiskJudge(func(a, b string) (DiskVerdict, bool) { calls++; return DiskVerdict{}, false })
|
|
tg, err := m.selectTier2TargetFrom("app", "/srv/sys", 1, 1)
|
|
if err != nil || tg.NamespaceRoot != "/mnt/b" || tg.Unproven {
|
|
t.Fatalf("old agent: want today's pick (/mnt/b, counted), got %+v err=%v", tg, err)
|
|
}
|
|
if calls == 0 {
|
|
t.Fatal("the judge was never asked")
|
|
}
|
|
if m.singleCopyWarning() != "" {
|
|
t.Fatal("old agent: a second drive by st_dev must still hide the notice (today's behaviour)")
|
|
}
|
|
// And the st_dev rule's own same-disk answer still stands.
|
|
m.samePhysicalDevice = func(a, b string) bool { return true }
|
|
if _, err := m.selectTier2TargetFrom("app", "/srv/x", 1, 1); err == nil {
|
|
t.Fatal("old agent: st_dev 'same' must still refuse the target")
|
|
}
|
|
}
|
|
|
|
// The success record carries the mark the app row renders.
|
|
func TestDisk16_UnprovenCopyIsRecorded(t *testing.T) {
|
|
m, sett := newTestManager(t, "/srv/sys")
|
|
m.recordTier2Success("app", &Tier2Target{NamespaceRoot: "/mnt/a", Unproven: true, UnprovenReason: "vm"}, 1, "", 0)
|
|
cd := sett.GetCrossDriveConfig("app")
|
|
if cd == nil || !cd.Unproven || cd.UnprovenReason != "vm" {
|
|
t.Fatalf("want the unproven mark recorded, got %+v", cd)
|
|
}
|
|
m.recordTier2Success("app", &Tier2Target{NamespaceRoot: "/mnt/a"}, 1, "", 0)
|
|
if cd := sett.GetCrossDriveConfig("app"); cd.Unproven {
|
|
t.Fatal("a later proven copy must clear the mark")
|
|
}
|
|
}
|