controller v0.296.0: a backup run cut off by a power cut or restart is said on the backup pages (R-519); the whole-system backup text states today's measurement (R-518)
gates / gates (push) Successful in 30s
gates / gates (push) Successful in 30s
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,99 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// R-519 (controller v0.296.0). RED-PROOFS recorded in felhom.eu/documentation/audits/hub-safety-2026-10-05/partE/:
|
||||
// 1. delete the m.markRunStarted call in runDBDumpsInternal → TestRunRecord_TheRealRunIsOnRecordWhileItRuns fails;
|
||||
// 2. synthesise LastDBDump with `Success: true` again → TestRunRecord_SynthesisedStatusIsNotOKAfterACut fails.
|
||||
|
||||
// A run the stop cut off is said — once on load, then on the page until a run ends with every step OK. A run that
|
||||
// ended with a failed step does not clear it.
|
||||
func TestRunRecord_CutRunIsSaidUntilACompleteOne(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "appdata-run.json")
|
||||
t0 := time.Date(2026, 9, 14, 19, 40, 3, 0, time.UTC)
|
||||
|
||||
before, _ := newTestManager(t, "/srv/sys")
|
||||
before.SetRunRecordPath(path)
|
||||
before.markRunStarted(t0) // the power goes here: no markRunEnded
|
||||
|
||||
after, _ := newTestManager(t, "/srv/sys")
|
||||
after.SetRunRecordPath(path)
|
||||
if got := after.LoadRunRecord(); !got.Equal(t0) {
|
||||
t.Fatalf("the cut run was not found at startup: %v", got)
|
||||
}
|
||||
if !after.InterruptedRunAt().Equal(t0) || !after.GetFullStatus(time.Time{}).Interrupted {
|
||||
t.Fatal("the page status does not carry the cut run")
|
||||
}
|
||||
|
||||
again, _ := newTestManager(t, "/srv/sys")
|
||||
again.SetRunRecordPath(path)
|
||||
if got := again.LoadRunRecord(); !got.IsZero() {
|
||||
t.Fatalf("the same cut was reported twice: %v", got)
|
||||
}
|
||||
if !again.InterruptedRunAt().Equal(t0) {
|
||||
t.Fatal("the notice must survive a second restart until a complete run")
|
||||
}
|
||||
|
||||
again.markRunStarted(t0.Add(time.Hour))
|
||||
again.markRunEnded(false) // a run with a failed step
|
||||
if !again.InterruptedRunAt().Equal(t0) {
|
||||
t.Fatal("a run with a failed step cleared the notice")
|
||||
}
|
||||
again.markRunStarted(t0.Add(2 * time.Hour))
|
||||
again.markRunEnded(true)
|
||||
if !again.InterruptedRunAt().IsZero() || again.GetFullStatus(time.Time{}).Interrupted {
|
||||
t.Fatal("a complete run did not clear the notice")
|
||||
}
|
||||
last, _ := newTestManager(t, "/srv/sys")
|
||||
last.SetRunRecordPath(path)
|
||||
if got := last.LoadRunRecord(); !got.IsZero() || !last.InterruptedRunAt().IsZero() {
|
||||
t.Fatal("the cleared notice came back after a restart")
|
||||
}
|
||||
}
|
||||
|
||||
// The production path: runDBDumpsInternal itself puts the run on record (running=true) before its first step,
|
||||
// and takes it off on an early error return.
|
||||
func TestRunRecord_TheRealRunIsOnRecordWhileItRuns(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "appdata-run.json")
|
||||
m, _ := newTestManager(t, t.TempDir())
|
||||
m.SetRunRecordPath(path)
|
||||
sawRunning := false
|
||||
m.discoverDBs = func(context.Context) ([]DiscoveredDB, error) {
|
||||
var rec runRecordFile
|
||||
if b, err := os.ReadFile(path); err == nil && json.Unmarshal(b, &rec) == nil && rec.Running {
|
||||
sawRunning = true
|
||||
}
|
||||
return nil, errors.New("docker is not reachable")
|
||||
}
|
||||
_ = m.runDBDumpsInternal(context.Background())
|
||||
if !sawRunning {
|
||||
t.Fatal("the run was not on record while it ran — a cut here would go unnoticed")
|
||||
}
|
||||
var rec runRecordFile
|
||||
b, _ := os.ReadFile(path)
|
||||
if json.Unmarshal(b, &rec) != nil || rec.Running {
|
||||
t.Fatalf("an ended run is still on record as running: %s", b)
|
||||
}
|
||||
}
|
||||
|
||||
// After a restart the page's "last database backup" is synthesised from the files. After a cut it must not read OK
|
||||
// (BIGNIGHT F2: „Utolsó adatbázis mentés … OK" over a torn run).
|
||||
func TestRunRecord_SynthesisedStatusIsNotOKAfterACut(t *testing.T) {
|
||||
m, _ := newTestManager(t, "/srv/sys")
|
||||
m.cachedStatus = &FullBackupStatus{DumpFiles: []DumpFileInfo{{StackName: "adventurelog", FileName: "adventurelog-postgres.sql", ModTime: time.Now()}}}
|
||||
if st := m.GetFullStatus(time.Time{}); st.LastDBDump == nil || !st.LastDBDump.Success {
|
||||
t.Fatal("control: with no cut the synthesised status reads OK")
|
||||
}
|
||||
m.runInterrupted = time.Now().Add(-time.Minute)
|
||||
if st := m.GetFullStatus(time.Time{}); st.LastDBDump == nil || st.LastDBDump.Success || !st.Interrupted {
|
||||
t.Fatalf("after a cut the synthesised status still reads OK: %+v", st.LastDBDump)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user