D22: every restore puts the data back exactly on failure and after a power cut (R-1177, R-1195); whole restore asks the unit first (R-1178); no mirror during a restore (R-1180); W22 restore refusals in Hungarian; unreleased
gates / gates (push) Successful in 1m40s

This commit is contained in:
2026-10-21 21:19:46 +02:00
parent 7848d4aaac
commit d582feb245
25 changed files with 1065 additions and 71 deletions
@@ -2,6 +2,7 @@ package backup
import (
"context"
"gitea.dooplex.hu/admin/felhom-controller/internal/util"
"io"
"log"
"os"
@@ -321,7 +322,11 @@ func TestReconstituteDBOnlyStartFailureStillBringsTheStackUp(t *testing.T) {
// TestRestoreFromUnitReplayFailureStillBringsTheStackUp is the local path's version, and it also
// pins the pre-existing semantics: a replay error becomes a dataErr and surfaces as the "completed
// with data errors" outcome, with the app back up.
func TestRestoreFromUnitReplayFailureStillBringsTheStackUp(t *testing.T) {
// TestR1177_ReplayFailureWithNoCopyHoldsTheApp — D22 (2026-10-21) replaced this test's old outcome („completed with
// data errors" and the app started on the half-replayed database). A unit with no volume archive leaves nothing to
// copy, so a database replay that failed half-way cannot be put back: that is the R-379 half state, and the app is
// HELD stopped (operator ruling 2026-08-22), never started on it.
func TestR1177_ReplayFailureWithNoCopyHoldsTheApp(t *testing.T) {
m, prov, _ := r47UnitFixture(t, immichLikeCompose, true)
m.importDBDump = func(context.Context, DiscoveredDB, string) error {
return context.DeadlineExceeded
@@ -331,14 +336,14 @@ func TestRestoreFromUnitReplayFailureStillBringsTheStackUp(t *testing.T) {
if err == nil {
t.Fatal("a failed replay must be surfaced, not swallowed")
}
if !strings.Contains(err.Error(), "completed with data errors") {
t.Fatalf("the pre-existing outcome semantics must be preserved, got: %v", err)
if want := util.MsgError("err.backup.db_restore_failed_held_mixed", "app").Error(); err.Error() != want {
t.Fatalf("a half-replayed database with no copy must be HELD, got: %v", err)
}
if !prov.fullStarted {
t.Fatal("the stack was left DB-ONLY after a failed replay")
if prov.fullStarted {
t.Fatalf("the app was started on a half-replayed database — calls %v", prov.calls)
}
if got := strings.Join(prov.calls, ","); got != "stop,recreate,startsvc:immich-postgres,start" {
t.Fatalf("sequence = %q, want the full start to follow the failed replay", got)
if got := strings.Join(prov.calls, ","); got != "stop,recreate,startsvc:immich-postgres,stop" {
t.Fatalf("sequence = %q, want the DB-only window closed again (stop) and no full start", got)
}
}