package backup import ( "bytes" "context" "encoding/json" "log" "os" "path/filepath" "strings" "testing" ) // R-412 leg 1 — a push that carried nothing must not read as a plain success. // // RUN-LEVEL on purpose, and the sibling R-234 file records why: its first version asserted the // capture helper alone, and its red-proof passed while the defect was untouched. The line under test // is emitted inside the per-app loop of the real run, so the test drives the real run and reads the // real logger. // // WORDING ONLY. This asserts what the run SAYS, not that it refuses — whether the push should re-read // the unit before sending is R-412 leg 2 and is deliberately still open. // writeUnitManifest gives a unit a manifest declaring exactly what is asked for. func writeUnitManifest(t *testing.T, unitDir string, dbDumps, volDumps []string) { t.Helper() b, err := json.Marshal(RecoveryManifest{DBDumps: dbDumps, VolumeDumps: volDumps}) if err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(unitDir, "manifest.json"), b, 0o644); err != nil { t.Fatal(err) } } // TestR412a_EmptyPushDoesNotReadAsAPlainSuccess — D1. // // RED-PROOF (run 2026-09-01, recorded in REPORT.md): restoring the single unconditional // `[INFO] backed up %s (%s, %d mandatory path(s))` makes this fail — the run logs a plain success over // a snapshot holding none of the app's data, which is exactly what was measured on demo-hp on // 2026-08-31. func TestR412a_EmptyPushDoesNotReadAsAPlainSuccess(t *testing.T) { drive := t.TempDir() m, sett, prov := classifiedOffboxManager(t, drive) var buf bytes.Buffer m.logger = log.New(&buf, "", 0) // `hollow` has a unit whose manifest declares NOTHING — the R-403 shape. hollow := mkUnit(t, drive, "hollow") writeUnitManifest(t, hollow, nil, nil) prov.hdd["hollow"] = drive prov.has["hollow"] = true // `sound` has a unit that declares real data, so the contrast is in the same run. sound := mkUnit(t, drive, "sound") writeUnitManifest(t, sound, []string{"sound-postgres.sql"}, []string{"sound_data.tar"}) prov.hdd["sound"] = drive prov.has["sound"] = true prov.deployed = map[string]bool{"hollow": true, "sound": true} _ = sett.SetAppOffbox("hollow", true) _ = sett.SetAppOffbox("sound", true) cap := &backupCapture{} m.SetOffboxRunner(cap.runner()) if err := m.RunOffboxBackup(context.Background()); err != nil { t.Fatalf("the run itself must still succeed — this is a wording change, not a guard: %v", err) } out := buf.String() // POSITIVE CONTROL FIRST: the run must actually have logged about both apps, or every assertion // below is over an empty string. if !strings.Contains(out, "hollow") || !strings.Contains(out, "sound") { t.Fatalf("the run logged about neither app — the assertions below would prove nothing.\n%s", out) } // NEGATIVE CONTROL: a string that cannot be there. if strings.Contains(out, "ZZZ-NOT-IN-THE-LOG") { t.Fatal("negative control matched — the search is not discriminating") } // The hollow app's line must say what it did NOT carry. ASCII fragments (R-364). var hollowLine string for _, l := range strings.Split(out, "\n") { if strings.Contains(l, "backed up hollow") { hollowLine = l } } if hollowLine == "" { t.Fatalf("no per-app push line for the hollow app at all.\n%s", out) } for _, frag := range []string{"NO database dump", "NO volume tar", "none of the app"} { if !strings.Contains(hollowLine, frag) { t.Fatalf("the hollow push line must say what it did not carry; %q missing from:\n %s", frag, hollowLine) } } if strings.Contains(hollowLine, "[INFO]") { t.Fatalf("a push carrying no data must not be logged at INFO like an ordinary success:\n %s", hollowLine) } // And the SOUND app's line must be untouched — the change must not relabel healthy runs. var soundLine string for _, l := range strings.Split(out, "\n") { if strings.Contains(l, "backed up sound") { soundLine = l } } if soundLine == "" { t.Fatalf("no per-app push line for the sound app.\n%s", out) } if strings.Contains(soundLine, "NO database dump") { t.Fatalf("a healthy push must NOT carry the empty-push wording — a warning that fires on everything costs the same as the comforting lie it replaces:\n %s", soundLine) } }