Files
felhom-controller/controller/internal/backup/r659_whole_copy_test.go
T
admin 7fcda8f1a4
gates / gates (push) Successful in 25s
R-699: a unit that holds no data is never an update-precondition copy (found live on 9202)
A just-installed app's unit, captured by the status refresh before any backup, satisfied
the precondition on its manifest time; tandoor's PostgreSQL was converted with no backup
of its database. Listed still; never a copy on Tier 1 or Tier 2. Red-proof RP6.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-27 12:04:03 +02:00

161 lines
6.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package backup
import (
"context"
"errors"
"strings"
"testing"
"time"
"gitea.dooplex.hu/admin/felhom-controller/internal/util"
)
// R-659 (v0.268.0; operator ruling 2026-09-24, `09` §3 decision 25, option A) — a held app's sentence
// names only a copy that brings the app back WHOLE; with none, it says so and names nothing.
//
// Measured 2026-09-24 00:00 on 9202, chaos round 11: nextcloud (drive files declared), own unit the only
// copy; the hold named „saját meghajtó"; the restore of exactly that copy REFUSED (R-538).
var r659At = time.Date(2026, 9, 23, 21, 42, 39, 0, time.UTC)
// r659Manager: one app; `files` declares a mandatory drive leg (nextcloud's class). `tiers` are the
// copies present, each at its own time.
func r659Manager(t *testing.T, files bool, tiers map[int]time.Time) *Manager {
t.Helper()
drive := t.TempDir()
m, _, prov := classifiedOffboxManager(t, drive)
prov.hdd["nextcloud"] = drive
if files {
prov.binds["nextcloud"] = []ClassifiedBind{mandatoryHDD("appdata/nextcloud")}
prov.has["nextcloud"] = true
}
m.updateTier2PointFn = func(string) (Tier2RestorePoint, error) {
at, ok := tiers[UpdateTierSecondDrive]
if !ok {
return Tier2RestorePoint{}, errors.New("no Tier-2 copy")
}
return Tier2RestorePoint{Restorable: true, CopyDateProven: true, CopyLastSuccess: at.Format(time.RFC3339), CopyDate: at.Format(time.RFC3339)}, nil
}
m.updateTier1PointsFn = func(string) ([]RestorePoint, bool) {
at, ok := tiers[UpdateTierLocal]
if !ok {
return nil, false
}
return []RestorePoint{{Time: at.Format(time.RFC3339), Tier: 1, DataProven: true}}, true
}
m.updateOffsiteTimesFn = func(context.Context) (map[string]time.Time, error) {
at, ok := tiers[UpdateTierOffsite]
if !ok {
return map[string]time.Time{}, nil
}
return map[string]time.Time{"nextcloud": at}, nil
}
return m
}
// TestR659_TheHoldNamesOnlyAWholeCopy — app class × copies present.
//
// COMPANION RED-PROOF (REPORT.md): make WholeOnTier answer true for every tier (the v0.267.0 reading:
// any copy is a route back). The round-11 case then fails at "names „saját meghajtó"".
func TestR659_TheHoldNamesOnlyAWholeCopy(t *testing.T) {
unit := r659At.Add(-2 * time.Hour)
second := r659At.Add(-20 * time.Hour)
off := r659At.Add(-5 * time.Hour)
noWhole := util.Text("hu", "hold.update.no_whole_copy", "nextcloud")
cases := []struct {
name string
files bool
tiers map[int]time.Time
wantNone bool
wantLabel string // the tier label the sentence must name (hu)
}{
{"files / unit only (round 11)", true, map[int]time.Time{UpdateTierLocal: unit}, true, ""},
{"files / second drive only", true, map[int]time.Time{UpdateTierSecondDrive: second}, true, ""},
{"files / unit + second drive", true, map[int]time.Time{UpdateTierLocal: unit, UpdateTierSecondDrive: second}, true, ""},
{"files / off-site + unit", true, map[int]time.Time{UpdateTierLocal: unit, UpdateTierOffsite: off}, false, "távoli mentés"},
{"files / none", true, map[int]time.Time{}, true, ""},
{"volumes / unit only", false, map[int]time.Time{UpdateTierLocal: unit}, false, "saját meghajtó"},
{"volumes / second drive older than unit", false, map[int]time.Time{UpdateTierLocal: unit, UpdateTierSecondDrive: second}, false, "saját meghajtó"},
{"volumes / off-site newest", false, map[int]time.Time{UpdateTierSecondDrive: second, UpdateTierOffsite: off}, false, "távoli mentés"},
{"volumes / none", false, map[int]time.Time{}, true, ""},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
m := r659Manager(t, c.files, c.tiers)
none, err := m.HoldAfterFailedUpdateWhole(context.Background(), "nextcloud", r659At, "untouched")
if err != nil {
t.Fatal(err)
}
held, why := m.RestoreHoldForLang("nextcloud", "hu")
if !held {
t.Fatal("not held")
}
if c.wantNone {
if !none || why != noWhole {
t.Fatalf("no whole copy exists, yet the hold names %q (none=%v)", why, none)
}
if !m.HoldNoWholeCopy("nextcloud") {
t.Fatal("the page flag is not set — the restore button would stay")
}
h, _ := m.UpdateHold("nextcloud")
if len(h.CopiesSeen) != len(c.tiers) {
t.Fatalf("copies seen %v, want %d recorded for support", h.CopiesSeen, len(c.tiers))
}
return
}
if none || strings.Contains(why, "nincs olyan másolat") {
t.Fatalf("a whole copy exists, yet the hold says none: %q", why)
}
if !strings.Contains(why, c.wantLabel) {
t.Fatalf("the hold names %q, want the copy %q", why, c.wantLabel)
}
if m.HoldNoWholeCopy("nextcloud") {
t.Fatal("the page flag is set over a whole copy")
}
})
}
}
// The English sentence is the operator's copy, verbatim, and the Hungarian likewise.
func TestR659_SentenceBothLanguages(t *testing.T) {
m := r659Manager(t, true, map[int]time.Time{UpdateTierLocal: r659At.Add(-time.Hour)})
if _, err := m.HoldAfterFailedUpdateWhole(context.Background(), "nextcloud", r659At, "untouched"); err != nil {
t.Fatal(err)
}
_, en := m.RestoreHoldForLang("nextcloud", "en")
if en != "The update of nextcloud did not work, and neither did the automatic undo. This box has no copy that can bring the app back together with its files. Felhom support has been told — until then, do not restart the app or delete its data." {
t.Fatalf("en = %q", en)
}
_, hu := m.RestoreHoldForLang("nextcloud", "hu")
if hu != "A(z) nextcloud frissítése nem sikerült, és az automatikus visszaállítás sem. Ezen a dobozon nincs olyan másolat, amely az alkalmazást a fájljaival együtt vissza tudná hozni. A Felhom ügyfélszolgálatát értesítettük — addig ne indítsd újra, és ne töröld az adatait." {
t.Fatalf("hu = %q", hu)
}
}
// TestR659_TruthTableAgreesWithTheRestoresRefusal — WholeOnTier for the unit tiers must be exactly
// "the unit restore does not refuse for missing files". The two predicates cannot drift.
func TestR659_TruthTableAgreesWithTheRestoresRefusal(t *testing.T) {
for _, files := range []bool{true, false} {
drive := t.TempDir()
m, _, prov := classifiedOffboxManager(t, drive)
prov.hdd["nextcloud"] = drive
if files {
prov.binds["nextcloud"] = []ClassifiedBind{mandatoryHDD("appdata/nextcloud")}
prov.has["nextcloud"] = true
}
mkUnit(t, drive, "nextcloud")
_, err := m.RestoreFromRecoveryUnitAt("nextcloud", RecoveryUnitPath(drive, "nextcloud"))
var refusal *ErrUnitLacksFileLegs
refused := errors.As(err, &refusal)
for _, tier := range []int{UpdateTierLocal, UpdateTierSecondDrive} {
if m.WholeOnTier("nextcloud", tier) == refused {
t.Fatalf("files=%v tier %d: WholeOnTier=%v but the unit restore refused=%v — the page would send a household to a refusal",
files, tier, m.WholeOnTier("nextcloud", tier), refused)
}
}
if !m.WholeOnTier("nextcloud", UpdateTierOffsite) {
t.Fatal("the off-site full restore brings files and database back — it is whole")
}
}
}