R-893: hold the app after ANY failure once the definition or a volume moved; hold persisted before the stop; run_job done says it ran, not what it found (security review)
gates / gates (push) Successful in 56s
gates / gates (push) Successful in 56s
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:
@@ -190,24 +190,33 @@ func TestR354_ScenarioD_LiveRecoveryUnitIsNeverWritten(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestR354_ScenarioE_PartialReplayIsAFailure — a volume replay that fails must never read as a
|
||||
// completion, and must name what failed.
|
||||
// completion, and must name what failed (to the operator).
|
||||
//
|
||||
// UPDATED 2026-10-08 (R-893 option C, `09` §3 decision 192): one volume HAS been replaced, so a start
|
||||
// would run the app on a mix of the snapshot's volume and the live rest. The app is now HELD stopped
|
||||
// for support instead of brought up; the volume that did not come back is named in the operator's
|
||||
// hold notice (the household reads the plain hold sentence).
|
||||
func TestR354_ScenarioE_PartialReplayIsReportedAsFailure(t *testing.T) {
|
||||
m, prov, _ := reconFixture(t, "20260719T060000Z", "2026-07-19T06:00:00Z", pgDump(1))
|
||||
seedScratchVolumes(t, m, "immich", "immich_a.tar", "immich_b.tar")
|
||||
m.volumeReplayFrom = func(_, _ string) (int, error) {
|
||||
return 1, fmt.Errorf("failed to restore 1 volume(s): [immich_b]")
|
||||
}
|
||||
var noticed error
|
||||
m.SetRestoreHoldNotify(func(_ string, replayErr, _ error) { noticed = replayErr })
|
||||
|
||||
res, err := m.ReconstituteFromOffsite(context.Background(), "immich", false)
|
||||
if err == nil {
|
||||
t.Fatal("a partial volume replay must be reported as a failure, not a completion")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "immich_b") {
|
||||
t.Errorf("the failure must name the volume that did not come back; got %q", err.Error())
|
||||
if noticed == nil || !strings.Contains(noticed.Error(), "immich_b") {
|
||||
t.Errorf("the operator's hold notice must name the volume that did not come back; got %v", noticed)
|
||||
}
|
||||
// Best-effort bring-up: a failed restore must not also be an outage.
|
||||
if !prov.fullStarted {
|
||||
t.Error("the app was left stopped after a failed volume replay")
|
||||
if prov.fullStarted {
|
||||
t.Error("the app was STARTED on a partly replaced set of volumes — it must be held (R-893)")
|
||||
}
|
||||
if held, _ := m.RestoreHoldFor("immich"); !held {
|
||||
t.Error("no restore hold was left after a partial volume replay")
|
||||
}
|
||||
// The count of what DID come back is still carried, so the report can say "1 of 2".
|
||||
if res.VolumesReplayed != 1 {
|
||||
|
||||
Reference in New Issue
Block a user