2f8ff2414c
gates / gates (push) Successful in 17s
R-537 — the contents label is now PER TIER. One string computed from the app's shape was rendered on all three tier rows; a Tier-1 unit has no file-copy step, so for the four class-A apps it was claiming „Adatok" for files it does not hold. R-538 — a unit restore REFUSES before anything is touched when the unit cannot return the app's drive-side files, and names the route that can. It runs before the stack is stopped because the measured harm included the app's own wastebasket going unreachable, which still held every byte. R-536 — „Alkalmazás telepítve" moved from the deploy's acceptance to its completion, with app_deploy_started and app_deploy_failed as the honest pair. Each fix red-proofed: seen failing with its own sentence, passing when restored. Requires hub v0.116.0 for the two new event types. MinAgent unchanged (0.131.0). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
79 lines
3.9 KiB
Go
79 lines
3.9 KiB
Go
package backup
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
)
|
|
|
|
// R-538 — a unit restore must REFUSE when the unit holds no copy of the app's files.
|
|
//
|
|
// The defect this pins, measured live on 2026-09-16: five photos were put into Nextcloud, the
|
|
// customer pressed „Visszaállítás indítása" on the Tier-1 unit, and the restore replayed three
|
|
// volume tars and a database dump over an app whose files live on the data drive. It reported
|
|
// „3 adatkötet és az adatbázis visszaállítva", and afterwards the folder listed all five photos and
|
|
// none of them opened — the replayed database referenced files that were never captured, and it had
|
|
// also stopped referencing the app's own wastebasket, which still held every byte.
|
|
//
|
|
// The assertion is the CONSEQUENCE, not the mechanism: the call returns the refusal and the app is
|
|
// left alone. Red-proof: delete the guard in RestoreFromRecoveryUnitAtWith → this test fails at
|
|
// "a restore that cannot return the files must refuse".
|
|
func TestUnitRestore_RefusesWhenTheUnitCannotHoldTheFiles(t *testing.T) {
|
|
drive := t.TempDir()
|
|
m, _, prov := classifiedOffboxManager(t, drive)
|
|
|
|
// A class-A app: it declares a MANDATORY bind under the drive, which is where its files live and
|
|
// which a Tier-1 unit structurally cannot capture.
|
|
prov.hdd["nextcloud"] = drive
|
|
prov.binds["nextcloud"] = []ClassifiedBind{mandatoryHDD("appdata/nextcloud")}
|
|
prov.has["nextcloud"] = true
|
|
mkUnit(t, drive, "nextcloud")
|
|
|
|
_, err := m.RestoreFromRecoveryUnitAt("nextcloud", RecoveryUnitPath(drive, "nextcloud"))
|
|
var refusal *ErrUnitLacksFileLegs
|
|
if !errors.As(err, &refusal) {
|
|
t.Fatalf("a restore that cannot return the files must refuse; got err=%v", err)
|
|
}
|
|
if refusal.Stack != "nextcloud" || len(refusal.Paths) == 0 {
|
|
t.Fatalf("the refusal must name the app and the paths it cannot return: %+v", refusal)
|
|
}
|
|
|
|
// NEGATIVE CONTROL, and it is the half that keeps the guard from being over-broad: an app that
|
|
// declares no drive-side files (all 45 class-B templates, whose data IS in the volumes the unit
|
|
// captured) must NOT be refused. If this ever starts refusing, the guard has stopped asking about
|
|
// the unit and started asking about nothing in particular.
|
|
prov.hdd["privatebin"] = drive
|
|
prov.has["privatebin"] = false
|
|
mkUnit(t, drive, "privatebin")
|
|
_, err = m.RestoreFromRecoveryUnitAt("privatebin", RecoveryUnitPath(drive, "privatebin"))
|
|
if errors.As(err, &refusal) {
|
|
t.Fatalf("an app with no drive-side files must not be refused: %v", err)
|
|
}
|
|
|
|
// The explicit second step („csak az adatbázist és a beállításokat") passes the guard. It may
|
|
// still fail further down for unrelated fixture reasons — what is asserted is only that consent
|
|
// is what the guard consults.
|
|
_, err = m.RestoreFromRecoveryUnitAtWith("nextcloud", RecoveryUnitPath(drive, "nextcloud"), UnitRestoreOptions{AcceptMissingFiles: true})
|
|
if errors.As(err, &refusal) {
|
|
t.Fatalf("explicit consent must pass the guard, not be refused by it: %v", err)
|
|
}
|
|
}
|
|
|
|
// DeclaredDriveFileLegs is the ONE predicate the label (R-537) and the refusal (R-538) share. A
|
|
// second copy of this question is how a page and a guard drift apart, so it is pinned here too.
|
|
func TestDeclaredDriveFileLegs_IsAboutDeclarationNotExistence(t *testing.T) {
|
|
drive := t.TempDir()
|
|
m, _, prov := classifiedOffboxManager(t, drive)
|
|
prov.hdd["nextcloud"] = drive
|
|
prov.binds["nextcloud"] = []ClassifiedBind{mandatoryHDD("appdata/nextcloud")}
|
|
prov.has["nextcloud"] = true
|
|
|
|
// The folder does NOT exist on disk in this fixture. It must still count: a label that tells the
|
|
// truth only until the customer starts using the app is not telling the truth.
|
|
if !m.HasDriveFileLegs("nextcloud") {
|
|
t.Fatal("a declared mandatory drive path must count even before the customer has put anything in it")
|
|
}
|
|
if got := m.DeclaredDriveFileLegs("unknown-app"); got != nil {
|
|
t.Fatalf("an app the provider does not know has no declared legs; got %v", got)
|
|
}
|
|
}
|