package backup import ( "context" "encoding/json" "errors" "os" "strings" "testing" "time" ) // W25 (rehearsal 2026-10-20): a failed off-site run showed the household „Utolsó hiba: offbox backup immich: exit // status 1". The household now reads which app failed (its own name), that the other apps are fine when they are, // and that the box tries again the next night; the raw text stays in the log and reaches the operator's event. func TestW25_FailedOffsiteRunSpeaksToTheHousehold(t *testing.T) { m, sett := newOffboxManager(t) usb := t.TempDir() addSchedulablePath(t, sett, usb) ns := m.namespaceRoot(usb) writeUnit(t, ns, "immich", "2026-10-20T00:00:00Z") writeUnit(t, ns, "mealie", "2026-10-20T00:00:00Z") b, _ := json.Marshal(RecoveryManifest{AppName: "immich", DisplayName: "Immich Fotók", CreatedAt: "2026-10-20T00:00:00Z"}) if err := os.WriteFile(RecoveryUnitManifestPath(ns, "immich"), b, 0o644); err != nil { t.Fatal(err) } _ = sett.SetAppOffbox("immich", true) _ = sett.SetAppOffbox("mealie", true) rr := &recordingOffboxRunner{backupErr: map[string]error{"immich": errors.New("exit status 1")}} m.SetOffboxRunner(rr.run) var operatorErr error m.SetOffboxNotify(func(_ time.Duration, _ int, err error) { operatorErr = err }) if err := m.RunOffboxBackup(context.Background()); err == nil { t.Fatal("a run with a failed app must report an error") } got := sett.GetOffboxTarget().LastError for _, want := range []string{"Nem sikerült a távoli mentés: Immich Fotók.", "A többi alkalmazás távoli mentése rendben van.", "A doboz a következő éjjel újra próbálkozik."} { if !strings.Contains(got, want) { t.Errorf("the household's note lacks %q: %q", want, got) } } for _, raw := range []string{"exit status", "offbox backup", "immich:"} { if strings.Contains(got, raw) { t.Errorf("raw technical text %q reached the household's note: %q", raw, got) } } // the operator's event keeps the technical detail, sanitised if op := m.OffsiteFailureMessage(operatorErr, time.Second); !strings.Contains(op, "offbox backup immich: exit status 1") { t.Errorf("the operator's event lost the technical detail: %q", op) } // English, and the cases where the other apps are NOT fine or no night comes res := offboxRunResult{backedUp: 1, failedApps: []string{"Immich", offsiteSharesName}} runErr := errors.New("offbox backup immich: exit status 1") en := offsiteHouseholdFailure("en", runErr, res, true) if en != "The remote backup did not work for: Immich, Shared folders. The remote backup of the other apps is fine. The box tries again the next night." { t.Errorf("en: %q", en) } res.missingUnprotected = []string{"paperless"} if hu := offsiteHouseholdFailure("hu", runErr, res, false); strings.Contains(hu, "többi") || strings.Contains(hu, "éjjel") { t.Errorf("with an app left out the others are not all fine, and with no nightly run there is no retry: %q", hu) } // a whole-run failure: its class sentence, never the raw text tr := offsiteHouseholdFailure("hu", errors.New("ssh: connect to host x port 23: connection refused"), offboxRunResult{}, true) if tr != "A távoli tárhely nem érhető el (hálózat vagy bejelentkezés). A doboz a következő éjjel újra próbálkozik." { t.Errorf("transport: %q", tr) } }