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) } }