package backup import ( "os" "path/filepath" "testing" ) // R-103 Group C — the ADDITIVE file restore is untouched. // // R-102 gave the Tier-2 copy a second, DESTRUCTIVE action. The file restore beside it is proven live // (Campaign 9 A1 and A3, 39 s and 46 s, byte-identical returns) and its promises are load-bearing: // nothing live is overwritten and nothing is deleted. This group is the non-regression assertion, and // it is written first-class rather than as an afterthought, because a silent widening here would turn // „restore my deleted files" into „overwrite everything with last night's copy". // C1 — TestR103_CanRestoreStillAnswersLegsOnly. // // CanRestore() gates the additive restore and must keep answering exactly one question. Widening it // to include the unit is R-356 arriving a second time: there, ONE predicate meant both "has this app // a drive?" and "is this app installed?", and it refused 40 running apps for months. // // Red-proof (recorded in REPORT.md): make CanRestore return `len(c.Legs) > 0 || c.HasUnit` → the // unit-only case below fails. func TestR103_CanRestoreStillAnswersLegsOnly(t *testing.T) { cases := []struct { name string cov Tier2Coverage want bool }{ {"no legs, no unit", Tier2Coverage{}, false}, {"no legs, unit present", Tier2Coverage{HasUnit: true}, false}, {"no legs, unit RESTORABLE", Tier2Coverage{HasUnit: true, UnitRestorable: true}, false}, {"legs present", Tier2Coverage{Legs: []string{"hdd"}}, true}, {"legs and unit", Tier2Coverage{Legs: []string{"hdd", "userdata"}, HasUnit: true, UnitRestorable: true}, true}, } for _, c := range cases { if got := c.cov.CanRestore(); got != c.want { t.Errorf("%s: CanRestore() = %v, want %v", c.name, got, c.want) } } // And the second predicate answers its own question, independently of the first. if (Tier2Coverage{Legs: []string{"hdd"}}).CanRestoreUnit() { t.Error("CanRestoreUnit() went true on file legs alone — the two predicates are entangled") } if !(Tier2Coverage{UnitRestorable: true}).CanRestoreUnit() { t.Error("CanRestoreUnit() went false on a restorable unit") } } // C2 — TestR103_FileRestoreBehaviourUnchanged. The additive restore's COUNTS and its two // non-destruction promises, over a copy that also holds a restorable unit — the case R-102 created // and the one where a leak between the paths would show. func TestR103_FileRestoreBehaviourUnchanged(t *testing.T) { m, fake, liveDrive, destDrive := newT2RManager(t) destBase := filepath.Join(destDrive, "backups", "secondary", "app") // The copy holds BOTH: a file leg and a full, openable recovery unit. mustWrite(t, filepath.Join(destBase, "hdd", "appdata", "photos", "gone.jpg"), "RESTORED") mustWrite(t, filepath.Join(destBase, "hdd", "appdata", "photos", "kept.jpg"), "FROM-THE-BACKUP") unit := tier2UnitDir(destBase) mustWrite(t, UnitManifestFile(unit), `{"schema_version":1,"app_name":"app"}`) mustWrite(t, filepath.Join(UnitVolumeDumpDir(unit), "vol_a.tar"), "tar") // Live: one of the two files exists with DIFFERENT content, and a third file exists only live. liveDir := filepath.Join(liveDrive, "appdata", "photos") mustWrite(t, filepath.Join(liveDir, "kept.jpg"), "THE-CUSTOMERS-NEWER-EDIT") mustWrite(t, filepath.Join(liveDir, "only-live.jpg"), "NOT-IN-THE-BACKUP") cov, err := m.Tier2RestoreCoverage("app") if err != nil { t.Fatalf("coverage: %v", err) } if !cov.CanRestoreUnit() { t.Fatal("fixture is wrong: the unit must be restorable, or this proves nothing") } n, err := m.RestoreTier2Files("app") if err != nil { t.Fatalf("file restore: %v", err) } // Two: `gone.jpg` above and `a.jpg` from the shared fixture. NOT three — `kept.jpg` exists live // and is skipped — and NOT more, which is what a leak from the unit's volume tars would look like. if n != 2 { t.Errorf("filesRestored = %d, want 2 (the two MISSING files only) — the file restore's reach changed", n) } if got, _ := os.ReadFile(filepath.Join(liveDir, "gone.jpg")); string(got) != "RESTORED" { t.Errorf("the missing file did not come back: %q", got) } if got, _ := os.ReadFile(filepath.Join(liveDir, "kept.jpg")); string(got) != "THE-CUSTOMERS-NEWER-EDIT" { t.Errorf("an EXISTING live file was overwritten: %q — the additive promise is broken", got) } if _, sErr := os.Stat(filepath.Join(liveDir, "only-live.jpg")); sErr != nil { t.Error("a live file absent from the backup was DELETED — the second non-destruction promise is broken") } if len(fake.stopped) != 1 || len(fake.started) != 1 { t.Errorf("lifecycle changed: stopped=%v started=%v", fake.stopped, fake.started) } // The unit the file restore does not read is untouched by it. if got, _ := os.ReadFile(filepath.Join(UnitVolumeDumpDir(unit), "vol_a.tar")); string(got) != "tar" { t.Error("the file restore wrote into the copy's recovery unit") } }