74b5eae5b0
gates / gates (push) Successful in 35s
An unreadable storage right after an agent restart read the off-site tier DUE (the in-memory record was empty). The newest success per tier is now kept on disk and read ONLY when the storage cannot be read: fresh -> not due, older than the cadence -> due, none -> due (unknown) as before. A storage that answers stays the ground truth. Ships with v0.150.0 after the 2026-10-07 read-back; nothing delivered. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
60 lines
2.3 KiB
Go
60 lines
2.3 KiB
Go
package backup
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-agent/internal/hub"
|
|
)
|
|
|
|
// R-894: the on-disk newest success per tier survives a restart (a new state from the same file).
|
|
func TestBackupSuccessState_SurvivesRestart(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "backup-success-state.json")
|
|
s := NewBackupSuccessState(path)
|
|
at := time.Date(2026, 10, 1, 20, 15, 0, 0, time.UTC)
|
|
if err := s.RecordBackupSuccess("felhom-pbs", hub.Backup{VMID: 9201, Success: true, StartedAt: at.Format(time.RFC3339)}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, ok := NewBackupSuccessState(path).LastKnownSuccess("felhom-pbs", 9201)
|
|
if !ok || !got.Equal(at) {
|
|
t.Fatalf("after a restart the saved copy must read back; got %v ok=%v", got, ok)
|
|
}
|
|
if _, ok := NewBackupSuccessState(path).LastKnownSuccess("local", 9201); ok {
|
|
t.Fatal("another tier must not borrow this tier's copy")
|
|
}
|
|
}
|
|
|
|
// Only a NEWER success replaces the saved one; failures and unparseable times are ignored.
|
|
func TestBackupSuccessState_KeepsNewestSuccessOnly(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "s.json")
|
|
s := NewBackupSuccessState(path)
|
|
newer := time.Date(2026, 10, 5, 0, 0, 0, 0, time.UTC)
|
|
older := newer.Add(-48 * time.Hour)
|
|
for _, b := range []hub.Backup{
|
|
{VMID: 1, Success: true, StartedAt: newer.Format(time.RFC3339)},
|
|
{VMID: 1, Success: true, StartedAt: older.Format(time.RFC3339)}, // older: ignored
|
|
{VMID: 1, Success: false, StartedAt: newer.Add(time.Hour).Format(time.RFC3339)}, // failure: ignored
|
|
{VMID: 1, Success: true, StartedAt: "not-a-time"}, // unparseable: ignored
|
|
} {
|
|
if err := s.RecordBackupSuccess("t", b); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if got, _ := NewBackupSuccessState(path).LastKnownSuccess("t", 1); !got.Equal(newer) {
|
|
t.Fatalf("want the newest success %v, got %v", newer, got)
|
|
}
|
|
}
|
|
|
|
// A corrupt file degrades to "nothing known" (the pre-R-894 DUE answer), never a crash.
|
|
func TestBackupSuccessState_CorruptFileIsEmpty(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "s.json")
|
|
if err := os.WriteFile(path, []byte("{not json"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := NewBackupSuccessState(path).LastKnownSuccess("t", 1); ok {
|
|
t.Fatal("a corrupt file must read as nothing known")
|
|
}
|
|
}
|