v0.279.0: after_install (decision 45), known default logins on the page, Part D empty-backup alarm, night chain (R-705), R-706
gates / gates (push) Successful in 27s
gates / gates (push) Successful in 27s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
||||
)
|
||||
|
||||
// hollowProvider: per-app volumes and running state; everything else from floorProvider.
|
||||
type hollowProvider struct {
|
||||
floorProvider
|
||||
volumes map[string][]string
|
||||
running map[string]bool
|
||||
}
|
||||
|
||||
func (p *hollowProvider) GetDockerVolumes(n string) []string { return p.volumes[n] }
|
||||
func (p *hollowProvider) RefreshAndIsRunning(n string) bool { return p.running[n] }
|
||||
|
||||
// Part D (v0.279.0) — the CONSEQUENCE: a running app whose newest copy holds no data reaches the operator
|
||||
// (once per app per tier per day) and the backup page; a copy with data clears it; a stopped (held) app
|
||||
// and an app with no volumes are never flagged.
|
||||
// COMPANION RED-PROOF: make judgeCopy ignore unitCarriesData (the pre-0.279.0 shape: nothing but a log) →
|
||||
// no notification and the first assertion fails.
|
||||
func TestPartD_ARunningAppWithAnEmptyCopyIsAnAlarm(t *testing.T) {
|
||||
sett, err := settings.Load(filepath.Join(t.TempDir(), "settings.json"), log.New(io.Discard, "", 0))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg := &config.Config{}
|
||||
cfg.Paths.SystemDataPath = t.TempDir()
|
||||
m := NewManager(cfg, sett, log.New(io.Discard, "", 0))
|
||||
p := &hollowProvider{floorProvider: floorProvider{stacks: []string{"cloud", "held", "novol"}, dir: t.TempDir()},
|
||||
volumes: map[string][]string{"cloud": {"cloud_db"}, "held": {"held_db"}},
|
||||
running: map[string]bool{"cloud": true, "novol": true}}
|
||||
m.SetStackProvider(p)
|
||||
day := time.Date(2026, 9, 28, 15, 20, 0, 0, time.UTC)
|
||||
m.hollow.now = func() time.Time { return day }
|
||||
var told []string
|
||||
m.SetHollowCopyNotify(func(app, tier string) { told = append(told, app+"/"+tier) })
|
||||
|
||||
unit := t.TempDir()
|
||||
writeMan := func(withData bool) {
|
||||
man := RecoveryManifest{SchemaVersion: 2, AppName: "cloud"}
|
||||
if withData {
|
||||
man.DBDumps = []string{"cloud.sql"}
|
||||
}
|
||||
b, _ := json.Marshal(man)
|
||||
_ = os.WriteFile(UnitManifestFile(unit), b, 0o644)
|
||||
}
|
||||
writeMan(false) // yesterday's shape: a manifest listing nothing
|
||||
|
||||
for _, app := range []string{"cloud", "held", "novol"} {
|
||||
m.judgeCopy(app, HollowTierOffsite, unit)
|
||||
}
|
||||
if len(told) != 1 || told[0] != "cloud/offsite" {
|
||||
t.Fatalf("operator told %v, want exactly [cloud/offsite] (held+stopped and volume-less apps are not flagged)", told)
|
||||
}
|
||||
if h := m.HollowCopies(); len(h) != 1 || h[0].App != "cloud" || h[0].Tier != HollowTierOffsite {
|
||||
t.Fatalf("page list %+v, want the one running app", h)
|
||||
}
|
||||
// Same day again: still flagged, NOT told twice.
|
||||
m.judgeCopy("cloud", HollowTierOffsite, unit)
|
||||
if len(told) != 1 {
|
||||
t.Fatalf("told twice in one day: %v", told)
|
||||
}
|
||||
// A missing unit (no manifest) is also no data — fail closed.
|
||||
m.judgeCopy("cloud", HollowTierLocal, filepath.Join(unit, "absent"))
|
||||
if len(told) != 2 || told[1] != "cloud/local" {
|
||||
t.Fatalf("an absent local unit was not flagged: %v", told)
|
||||
}
|
||||
// The next day: told again.
|
||||
m.hollow.now = func() time.Time { return day.Add(24 * time.Hour) }
|
||||
m.judgeCopy("cloud", HollowTierOffsite, unit)
|
||||
if len(told) != 3 {
|
||||
t.Fatalf("not told again the next day: %v", told)
|
||||
}
|
||||
// The copy carries data again: cleared from the page.
|
||||
writeMan(true)
|
||||
m.judgeCopy("cloud", HollowTierOffsite, unit)
|
||||
m.judgeCopy("cloud", HollowTierLocal, unit)
|
||||
if h := m.HollowCopies(); len(h) != 0 {
|
||||
t.Fatalf("a copy with data is still listed: %+v", h)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user