package backup import ( "context" "strings" "testing" ) // R-234 — a run that SKIPPED an app the customer selected is not a successful run. // // The same paragraph the R-203 verdict block already carries — "a warning beside a success is read // as a success" — was applied to one of the two shapes it describes. An app missing a declared // mandatory FOLDER made the run `incomplete`; an app skipped ENTIRELY, with nothing of it in the // snapshot at all, still reported `ok`. The smaller gap moved the verdict and the bigger one did not. // // Run-level on purpose: the classification and the verdict are both inside the run, and the sibling // test file records what happened when its first version asserted the capture helper alone — its // red-proof passed while the defect was untouched. // Scenario A — a selected, DEPLOYED app with no recovery unit makes the run incomplete, names itself, // and does not suppress what was captured. // // RED-PROOF: drop `unprotected` from the verdict condition (leave only mandatoryGaps) → this FAILS // with the run reporting ok over a skipped app, which is production behaviour up to v0.204.0. func TestOffboxRun_SkippedSelectedAppIsIncomplete(t *testing.T) { drive := t.TempDir() m, sett, prov := classifiedOffboxManager(t, drive) // `kept` has a unit and is pushed; `dropped` is selected and deployed but has NO unit, so the // per-app loop skips it. backedUp>0 is what made the existing no-silent-success guard stay quiet. mkUnit(t, drive, "kept") prov.hdd["kept"] = drive prov.has["kept"] = true prov.hdd["dropped"] = drive prov.has["dropped"] = true prov.deployed = map[string]bool{"kept": true, "dropped": true} _ = sett.SetAppOffbox("kept", true) _ = sett.SetAppOffbox("dropped", true) var gapNotified map[string][]string m.SetOffboxGapNotify(func(g map[string][]string) { gapNotified = g }) cap := &backupCapture{} m.SetOffboxRunner(cap.runner()) if err := m.RunOffboxBackup(context.Background()); err != nil { t.Fatalf("the run itself must SUCCEED — a skipped app is a coverage gap, not a failed run: %v", err) } got := sett.GetOffboxTarget() if got.LastStatus != "incomplete" { t.Fatalf("LastStatus = %q, want \"incomplete\" — the customer selected an app and the run did not "+ "carry it; on 2026-08-06 this reported „✓ Rendben” and the restore refused minutes later", got.LastStatus) } // Scenario A: the counters and the anchor still record what WAS captured. if got.LastSuccess == "" { t.Error("LastSuccess must still record what was captured — half a backup is not no backup") } if cap.backups != 1 { t.Errorf("the app that HAD a unit must still be pushed, got %d backup calls", cap.backups) } // Scenario E: which app, and why. if !strings.Contains(got.LastWarning, "dropped") { t.Errorf("the warning must NAME the skipped app, got %q", got.LastWarning) } if !strings.Contains(got.LastWarning, "nincs helyi ment") { t.Errorf("the warning must say WHY it was skipped, got %q", got.LastWarning) } if !strings.Contains(got.LastWarning, "következő ment") { t.Errorf("the warning must say WHEN it will be protected, got %q", got.LastWarning) } // Scenario B: the operator hears about it, in the same vocabulary as a folder gap. if len(gapNotified["dropped"]) == 0 { t.Fatalf("the operator signal must carry the skipped app, got %v", gapNotified) } } // Scenario C — a healthy run is untouched. Without this, "always incomplete" would also pass above, // and a status that is never green is a status that stops being read. // // RED-PROOF: count EVERY skip (drop the classification switch and use len(res.missing)) → a healthy // run goes amber and this FAILS. func TestOffboxRun_HealthyRunStaysOk(t *testing.T) { drive := t.TempDir() m, sett, prov := classifiedOffboxManager(t, drive) mkUnit(t, drive, "kept") prov.hdd["kept"] = drive prov.has["kept"] = true _ = sett.SetAppOffbox("kept", true) fired := false m.SetOffboxGapNotify(func(map[string][]string) { fired = true }) cap := &backupCapture{} m.SetOffboxRunner(cap.runner()) if err := m.RunOffboxBackup(context.Background()); err != nil { t.Fatalf("run: %v", err) } got := sett.GetOffboxTarget() if got.LastStatus != "ok" { t.Fatalf("LastStatus = %q, want ok — every selected app was carried", got.LastStatus) } if fired { t.Error("the operator signal must NOT fire when nothing was missed") } if strings.Contains(got.LastWarning, "NEM kerültek be") { t.Errorf("a healthy run must carry no skip warning, got %q", got.LastWarning) } } // Scenario D — a box with NOTHING selected keeps today's behaviour: ok, with the existing // zero-selection notice. An unconfigured box reporting incomplete forever is its own defect. // // RED-PROOF: count the empty selection as a gap → this box goes permanently amber and this FAILS. func TestOffboxRun_NothingSelectedIsNotAGap(t *testing.T) { drive := t.TempDir() m, sett, _ := classifiedOffboxManager(t, drive) cap := &backupCapture{} m.SetOffboxRunner(cap.runner()) if err := m.RunOffboxBackup(context.Background()); err != nil { t.Fatalf("run: %v", err) } got := sett.GetOffboxTarget() if got.LastStatus != "ok" { t.Fatalf("LastStatus = %q, want ok — nothing was selected, so nothing was skipped", got.LastStatus) } if !strings.Contains(got.LastWarning, "nincs mentésre jelölt alkalmazás") { t.Errorf("the existing zero-selection notice must survive, got %q", got.LastWarning) } } // Scenario F — a selected app that is NOT deployed. Decided deliberately: it is NAMED with what to do // about it, and it does NOT move the verdict, because a box left amber forever by an app somebody // removed is a status nobody reads. func TestOffboxRun_SelectedButUndeployedIsNamedNotCounted(t *testing.T) { drive := t.TempDir() m, sett, prov := classifiedOffboxManager(t, drive) mkUnit(t, drive, "kept") prov.hdd["kept"] = drive prov.has["kept"] = true prov.deployed = map[string]bool{"kept": true} // "removed-app" deliberately absent _ = sett.SetAppOffbox("kept", true) // selected, no unit, and NOT in the deployed set _ = sett.SetAppOffbox("removed-app", true) fired := false m.SetOffboxGapNotify(func(map[string][]string) { fired = true }) cap := &backupCapture{} m.SetOffboxRunner(cap.runner()) if err := m.RunOffboxBackup(context.Background()); err != nil { t.Fatalf("run: %v", err) } got := sett.GetOffboxTarget() if got.LastStatus != "ok" { t.Fatalf("LastStatus = %q, want ok — an app that is not installed cannot be protected, and must "+ "not hold the box amber forever", got.LastStatus) } if !strings.Contains(got.LastWarning, "removed-app") { t.Errorf("the undeployed selection must still be NAMED, got %q", got.LastWarning) } if !strings.Contains(got.LastWarning, "vedd ki a kijelöl") { t.Errorf("it must say what to do about it, got %q", got.LastWarning) } if fired { t.Error("an undeployed app must not raise the operator gap signal") } }