v0.275.0: a backup's data and its version travel together (R-696, 07 §6.6, D4 option A); R-695, R-691, R-694
gates / gates (push) Successful in 23s
gates / gates (push) Successful in 23s
The unit's data files are stamped with the versions that wrote them; the capture keeps the definition the data belongs to; a restore never starts data under another version's definition (unit restores refuse a mismatch; the off-site restore writes the snapshot's definition); every tier's time is its data's; the conversion-copy release needs a dump on the new engine. File-browser sync single-flight + no empty kept folder (R-695); the kept view joins the folder's owning group, language switch resyncs (R-691); a restore-generated login is not shown as the password (R-694). Red-proofs in felhom.eu/documentation/audits/version-travel-2026-09-26/. 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:
@@ -423,6 +423,8 @@ func TestConvert_ReleaseAfterAProvenBackup(t *testing.T) {
|
||||
}
|
||||
g.mu.Lock()
|
||||
g.points = []UpdateRestorePoint{{Tier: UpdateTierLocal, ProvenAt: slice4T0.Add(time.Hour)}}
|
||||
// v0.275.0 (A4): and the dump in that backup was written by the converted engine.
|
||||
g.stamps = []DataDumpStamp{{File: "db-dumps/nextcloud-postgres.sql", At: slice4T0.Add(time.Hour), Images: map[string]string{"db": "postgres:18-alpine@sha256:18"}}}
|
||||
g.mu.Unlock()
|
||||
if got := m.ReleaseConversionCopies(context.Background()); len(got) != 1 || fc.nCopies() != 0 {
|
||||
t.Fatalf("released %v after a proven backup; copies=%d", got, fc.nCopies())
|
||||
@@ -505,3 +507,68 @@ func TestConvert_MarkDoesNotChangeOldLadderPrints(t *testing.T) {
|
||||
t.Fatalf("an entry without the mark prints it: %s", b)
|
||||
}
|
||||
}
|
||||
|
||||
// A5 red-proof 2 — the release refuses a PRE-CONVERSION dump (R-696). The measured demo-hp night: a copy
|
||||
// "proven" after the conversion (the unit's refresh time) while its dump was written by PostgreSQL 16
|
||||
// before the conversion; v0.274.0 removed the 16 datadir copy on that. Now the copy stays until a dump
|
||||
// written after the conversion BY THE NEW ENGINE is recorded — and each half of that is needed.
|
||||
func TestA5_TheReleaseRefusesAPreConversionDump(t *testing.T) {
|
||||
m, dir, g, _, fc, _ := convManager(t, goodMark)
|
||||
if err := m.StartGuardedUpdate("nextcloud"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if st := waitUpdateDone(t, m, "nextcloud"); st.UpdatePhase != UpdatePhaseDone {
|
||||
t.Fatalf("setup: %q", st.UpdatePhase)
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.stacks["nextcloud"].AppConfig = LoadAppConfig(dir)
|
||||
m.mu.Unlock()
|
||||
cc := LoadAppConfig(dir).ConversionCopy
|
||||
if cc == nil || cc.Service != "db" {
|
||||
t.Fatalf("conversion_copy = %+v, want one naming the converted service", cc)
|
||||
}
|
||||
at, _ := time.Parse(time.RFC3339, cc.At)
|
||||
g.mu.Lock()
|
||||
g.points = []UpdateRestorePoint{{Tier: UpdateTierLocal, ProvenAt: at.Add(10 * time.Minute)}} // "proven" after it
|
||||
g.mu.Unlock()
|
||||
for _, c := range []struct {
|
||||
why string
|
||||
stamp DataDumpStamp
|
||||
}{
|
||||
{"a dump written BEFORE the conversion, by 16", DataDumpStamp{File: "db-dumps/nextcloud-postgres.sql", At: at.Add(-5 * time.Minute), Images: map[string]string{"db": "postgres:16-alpine@sha256:16"}}},
|
||||
{"a dump after the conversion, but recorded as 16", DataDumpStamp{File: "db-dumps/nextcloud-postgres.sql", At: at.Add(5 * time.Minute), Images: map[string]string{"db": "postgres:16-alpine@sha256:16"}}},
|
||||
{"a dump after the conversion with no recorded images", DataDumpStamp{File: "db-dumps/nextcloud-postgres.sql", At: at.Add(5 * time.Minute)}},
|
||||
} {
|
||||
g.mu.Lock()
|
||||
g.stamps = []DataDumpStamp{c.stamp}
|
||||
g.mu.Unlock()
|
||||
if got := m.ReleaseConversionCopies(context.Background()); len(got) != 0 || fc.nCopies() != 1 {
|
||||
t.Fatalf("%s: released %v; copies=%d — the 16 datadir copy went on a backup that holds no 18 dump", c.why, got, fc.nCopies())
|
||||
}
|
||||
}
|
||||
g.mu.Lock()
|
||||
g.stamps = []DataDumpStamp{{File: "db-dumps/nextcloud-postgres.sql", At: at.Add(5 * time.Minute), Images: map[string]string{"db": "postgres:18-alpine@sha256:18"}}}
|
||||
g.mu.Unlock()
|
||||
if got := m.ReleaseConversionCopies(context.Background()); len(got) != 1 || fc.nCopies() != 0 {
|
||||
t.Fatalf("released %v with an 18 dump after the conversion; copies=%d", got, fc.nCopies())
|
||||
}
|
||||
}
|
||||
|
||||
// A record written before v0.275.0 carries no service: every Postgres-family image in the dump's
|
||||
// recorded set must then be at the new major.
|
||||
func TestA4_AnOldRecordWithoutAServiceChecksEveryPostgresImage(t *testing.T) {
|
||||
cc := &ConversionCopy{From: 16, To: 18}
|
||||
after := slice4T0
|
||||
st := func(imgs map[string]string) []DataDumpStamp {
|
||||
return []DataDumpStamp{{File: "db-dumps/x-postgres.sql", At: after.Add(time.Minute), Images: imgs}}
|
||||
}
|
||||
if _, ok := convertedDumpAt(st(map[string]string{"app": "x/app:1", "db": "postgres:18-alpine"}), cc, after); !ok {
|
||||
t.Fatal("an 18 dump was not accepted")
|
||||
}
|
||||
if _, ok := convertedDumpAt(st(map[string]string{"app": "x/app:1", "db": "postgres:16-alpine"}), cc, after); ok {
|
||||
t.Fatal("a 16 dump was accepted")
|
||||
}
|
||||
if _, ok := convertedDumpAt(st(map[string]string{"app": "x/app:1"}), cc, after); ok {
|
||||
t.Fatal("a dump with no Postgres image recorded was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user