R-1340 second producer: a restore_mixed hold's Why() names the cause from its record (found live on rc.1: the hub's Held apps line said off-site/database for a local restore with no database) — test red first
gates / gates (push) Successful in 30s

This commit is contained in:
admin committed 2026-10-29 14:35:07 +01:00
1 parent 6077ac0083
commit 99fac06aa7
4 files changed
+80 -2

No files matched your search

+5
View File
@@ -29,6 +29,11 @@
after a later save of the running controller" — R-1343 reproduced), `TestR1343_TheBoxCommandFallsBackWhenNoControllerRuns`,
`TestR1343_TheFallbackDropsAWaitingPutBackToo`, `TestHoldCtl_NotReachableFromOutside`,
`TestHoldCtl_UnknownRequestIsRefused`, `TestHoldCtl_ANonSocketFileIsNotReplaced`.
- **R-1340, second producer (found live 2026-10-29 on rc.1, Tester 1):** the hold's `Why()` — the hub's „Held apps" line,
the box's `app_held` event and `--restore-holds` — still said „held after an off-site restore failed its database
replay" for a LOCAL restore of an app with no database. It is now built from the record (`RestoreKind`, `MixedCause`,
`FailedStep`, `DBReplay`); a record from before these fields gets the neutral „held after a restore stopped half-way".
Test `TestR1340_WhyNamesTheMixedCause` (13 assertions failed on the old sentence).
- **R-1340 — the operator's „HELD STOPPED" mail names the real cause** (`cmd/controller/holdmail.go`
`heldStoppedOperatorText`, built from the hold record). Hold writers now record `RestoreKind` (off-site / local),
`MixedCause` (`definition_write` / `put_back` / `interrupted_put_back`), `FailedStep` (the put-back step that failed,
@@ -20,7 +20,9 @@ func TestR1286_EveryHoldReasonSaysWhatItIs(t *testing.T) {
}{
{settings.RestoreHold{Stack: "kimai", At: "2026-10-14T09:06:31Z", Reason: settings.HoldReasonUnhealthyStop, UnhealthyKind: "crash_loop"}, "stopped by the box for a crash loop"},
{settings.RestoreHold{Stack: "a", At: "t", Reason: settings.HoldReasonUpdateFailed}, "failed update"},
{settings.RestoreHold{Stack: "b", At: "t", Reason: settings.HoldReasonRestoreMixed}, "off-site restore failed its database replay"},
// R-1340 (2026-10-29): a record with no cause fields no longer claims „off-site … database replay" — that was the
// false story (a local restore of an app with no database read the same); it says what is known.
{settings.RestoreHold{Stack: "b", At: "t", Reason: settings.HoldReasonRestoreMixed}, "restore stopped half-way"},
{settings.RestoreHold{Stack: "c", At: "t", Reason: settings.HoldReasonPutBackPending}, "pre-restore copy is poured back"},
{settings.RestoreHold{Stack: "d", At: "t"}, "failed restore whose rollback also failed"},
} {
@@ -0,0 +1,41 @@
package settings
import (
"strings"
"testing"
)
// R-1340, second producer (found live 2026-10-29 on Tester 1): the hold's Why() — the hub's „Held apps" line and the
// box's app_held event — still said „held after an off-site restore failed its database replay" for a LOCAL restore of
// an app with no database whose put-back stop failed. It must name what the hold record carries.
// RED-PROOF: seen failing on the old fixed sentence.
func TestR1340_WhyNamesTheMixedCause(t *testing.T) {
for _, tc := range []struct {
name string
h RestoreHold
want, not []string
}{
{"local put-back, no database", RestoreHold{Reason: HoldReasonRestoreMixed, At: "T", RestoreKind: RestoreKindLocal, MixedCause: MixedCausePutBack, FailedStep: "stopping the app before the put-back"},
[]string{"local restore", "putting the live data back failed", "stopping the app before the put-back"}, []string{"database", "off-site"}},
{"off-site database replay", RestoreHold{Reason: HoldReasonRestoreMixed, At: "T", RestoreKind: RestoreKindOffsite, MixedCause: MixedCausePutBack, DBReplay: true},
[]string{"off-site restore", "database replay", "putting the live data back failed"}, nil},
{"definition write", RestoreHold{Reason: HoldReasonRestoreMixed, At: "T", RestoreKind: RestoreKindOffsite, MixedCause: MixedCauseDefinitionWrite},
[]string{"definition"}, []string{"database"}},
{"interrupted put-back", RestoreHold{Reason: HoldReasonRestoreMixed, At: "T", MixedCause: MixedCauseInterruptedPutBack},
[]string{"interrupted"}, []string{"database"}},
{"legacy record", RestoreHold{Reason: HoldReasonRestoreMixed, At: "T"},
[]string{"stopped half-way", "clear the hold"}, []string{"database", "off-site"}},
} {
got := tc.h.Why()
for _, w := range tc.want {
if !strings.Contains(got, w) {
t.Errorf("%s: %q lacks %q", tc.name, got, w)
}
}
for _, n := range tc.not {
if strings.Contains(got, n) {
t.Errorf("%s: %q says %q", tc.name, got, n)
}
}
}
}
+31 -1
View File
@@ -2051,7 +2051,7 @@ func (h RestoreHold) Why() string {
}
return "stopped by the box for a " + strings.ReplaceAll(kind, "_", " ") + " (" + h.At + ") — the household's Start gives it one more try"
case HoldReasonRestoreMixed:
return "held after an off-site restore failed its database replay (" + h.At + ") — clear the hold to start it"
return h.mixedWhy() + " (" + h.At + ") — clear the hold to start it"
case HoldReasonPutBackPending:
return "held until the pre-restore copy is poured back (" + h.At + ") — the put-back starts it"
default:
@@ -2059,6 +2059,36 @@ func (h RestoreHold) Why() string {
}
}
// mixedWhy (R-1340) names a restore_mixed hold from what its record carries — never „database" or „off-site" unless the
// record says so. A record written before these fields („legacy") gets the neutral sentence. Pinned by
// TestR1340_WhyNamesTheMixedCause.
func (h RestoreHold) mixedWhy() string {
kind := "a restore"
switch h.RestoreKind {
case RestoreKindOffsite:
kind = "an off-site restore"
case RestoreKindLocal:
kind = "a local restore"
}
switch h.MixedCause {
case MixedCauseDefinitionWrite:
return "held after " + kind + " could not write its definition and writing the live definition back failed too"
case MixedCauseInterruptedPutBack:
return "held after a restore was interrupted and its live data could not be put back at the next start"
case MixedCausePutBack:
what := "failed"
if h.DBReplay {
what = "failed its database replay"
}
s := "held after " + kind + " " + what + " and putting the live data back failed"
if h.FailedStep != "" {
s += " (" + h.FailedStep + ")"
}
return s
}
return "held after a restore stopped half-way"
}
// SetRestoreHold records a hold. Modelled on SetDisconnected: a condition, plus what it is holding.
func (s *Settings) SetRestoreHold(h RestoreHold) error {
s.mu.Lock()