v0.307.0: after a restart the Databases table lists each database once, not every pre-restore undo copy
gates / gates (push) Successful in 1m14s
gates / gates (push) Successful in 1m14s
Seen on demo-hp 2026-10-10 (adventurelog x4, bookstack x4). The synthesised run skips IsUndo files; only undo copies -> no synthesised run. Two tests, both red-proved. v0.306.0 (R-570) was built but never delivered; 0.307.0 carries it. 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:
@@ -1311,6 +1311,12 @@ func (m *Manager) GetFullStatus(nextDBDump time.Time) *FullBackupStatus {
|
||||
var results []DumpResult
|
||||
var latestTime time.Time
|
||||
for _, f := range status.DumpFiles {
|
||||
// 2026-10-10: a `pre-restore-` undo copy (R-379) is not part of any run — it stays in the file
|
||||
// list, never in the run's table (demo-hp listed adventurelog four times after a restart).
|
||||
// Pinned by TestSynthesisedRunListsNoUndoCopy.
|
||||
if f.IsUndo {
|
||||
continue
|
||||
}
|
||||
results = append(results, DumpResult{
|
||||
DB: DiscoveredDB{StackName: f.StackName, DBType: f.DBType, ContainerName: f.StackName},
|
||||
FilePath: f.FileName,
|
||||
@@ -1323,10 +1329,13 @@ func (m *Manager) GetFullStatus(nextDBDump time.Time) *FullBackupStatus {
|
||||
}
|
||||
// R-519: after a restart the run's result is not in memory, so it is SYNTHESISED from the files. A run
|
||||
// the stop cut off left a fresh .sql beside last night's tars — "Success" must not be read off that.
|
||||
status.LastDBDump = &DBDumpStatus{
|
||||
LastRun: latestTime,
|
||||
Results: results,
|
||||
Success: m.runInterrupted.IsZero(),
|
||||
// Only undo copies on disk → no run to show (nil, as with no files at all).
|
||||
if len(results) > 0 {
|
||||
status.LastDBDump = &DBDumpStatus{
|
||||
LastRun: latestTime,
|
||||
Results: results,
|
||||
Success: m.runInterrupted.IsZero(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// After a controller restart the Backup → Apps „Databases" table is SYNTHESISED from the dump files on disk (the last
|
||||
// run's result is not in memory). Seen 2026-10-10 on demo-hp after the 0.305.0 delivery: every `pre-restore-` undo copy
|
||||
// (R-379) was listed as one more row of the night's run — adventurelog four times, bookstack four times, each stamped
|
||||
// with the run's time. An undo copy is not part of any run: it stays in the file list (its visibility is a recorded
|
||||
// decision), but the run table shows one row per database, the newest real dump.
|
||||
func TestSynthesisedRunListsNoUndoCopy(t *testing.T) {
|
||||
m, _ := newTestManager(t, "/srv/sys")
|
||||
night := time.Date(2026, 10, 10, 2, 15, 0, 0, time.UTC)
|
||||
m.cachedStatus = &FullBackupStatus{DumpFiles: []DumpFileInfo{
|
||||
{StackName: "adventurelog", DBType: DBTypePostgres, FileName: "adventurelog-postgres.sql", ModTime: night},
|
||||
{StackName: "adventurelog", DBType: DBTypePostgres, FileName: "pre-restore-20260913T193505Z-adventurelog-postgres.sql", ModTime: night.AddDate(0, 0, -27), IsUndo: true},
|
||||
{StackName: "adventurelog", DBType: DBTypePostgres, FileName: "pre-restore-20260928T021839Z-adventurelog-postgres.sql", ModTime: night.AddDate(0, 0, -12), IsUndo: true},
|
||||
{StackName: "bookstack", DBType: DBTypeMariaDB, FileName: "bookstack-mariadb.sql", ModTime: night},
|
||||
{StackName: "bookstack", DBType: DBTypeMariaDB, FileName: "pre-restore-20260930T113419Z-bookstack-mariadb.sql", ModTime: night.AddDate(0, 0, -10), IsUndo: true},
|
||||
}}
|
||||
st := m.GetFullStatus(time.Time{})
|
||||
if st.LastDBDump == nil {
|
||||
t.Fatal("no synthesised run")
|
||||
}
|
||||
per := map[string]int{}
|
||||
for _, r := range st.LastDBDump.Results {
|
||||
per[r.DB.StackName]++
|
||||
if r.FilePath != "adventurelog-postgres.sql" && r.FilePath != "bookstack-mariadb.sql" {
|
||||
t.Errorf("an undo copy is listed as a row of the run: %s", r.FilePath)
|
||||
}
|
||||
}
|
||||
if per["adventurelog"] != 1 || per["bookstack"] != 1 || len(st.LastDBDump.Results) != 2 {
|
||||
t.Errorf("want one row per database (2), got %d: %v", len(st.LastDBDump.Results), per)
|
||||
}
|
||||
if !st.LastDBDump.LastRun.Equal(night) {
|
||||
t.Errorf("the run time must come from the real dumps: %v", st.LastDBDump.LastRun)
|
||||
}
|
||||
if len(st.DumpFiles) != 5 {
|
||||
t.Errorf("the file list must keep every file, undo copies included (R-379): %d", len(st.DumpFiles))
|
||||
}
|
||||
}
|
||||
|
||||
// Only undo copies on disk: there is no run to show, so no synthesised run (a zero-time „last backup" would be a lie).
|
||||
func TestSynthesisedRunAbsentWhenOnlyUndoCopies(t *testing.T) {
|
||||
m, _ := newTestManager(t, "/srv/sys")
|
||||
m.cachedStatus = &FullBackupStatus{DumpFiles: []DumpFileInfo{
|
||||
{StackName: "bookstack", DBType: DBTypeMariaDB, FileName: "pre-restore-20260930T113419Z-bookstack-mariadb.sql", ModTime: time.Now(), IsUndo: true},
|
||||
}}
|
||||
if st := m.GetFullStatus(time.Time{}); st.LastDBDump != nil {
|
||||
t.Errorf("a run was synthesised from undo copies alone: %+v", st.LastDBDump)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user