R-356: the off-site restore refused every app that has no data drive
gates / gates (push) Failing after 12s

ReconstituteFromOffsite and PlaceOffsiteRestore both resolved the restore
destination with the RAW HDD_PATH and read an empty answer as "the app is not
installed". For 40 of the 53 catalogue apps that answer is correctly empty and
permanent, so both actions refused forever for a running, healthy app — and told
the customer to reinstall it "in the same place", which those apps never offer.

Separate the two questions. "Installed?" is asked of ListDeployedStacks via a new
Manager.isStackDeployed that fails CLOSED on a nil provider. "Where?" is answered
by GetAppDrivePath — the same resolver CaptureRecoveryUnit wrote the snapshot
with, so the restore aims at the place the backup came from.

The 13 drive apps are unchanged: own drive, mismatch check, ack still required.
A third refusal, with its own sentence, covers installed-but-no-resolvable-root.

Fixtures that marked an app "installed" by giving it an HDD path now state
deployment as its own fact. No assertion weakened.
This commit is contained in:
2026-08-22 13:08:46 +02:00
parent 2da259af38
commit 08eb1a6e3a
9 changed files with 541 additions and 25 deletions
@@ -15,6 +15,10 @@ func placeFixture(t *testing.T, full bool) (*Manager, *offbox3aProvider, string,
drive := t.TempDir()
m, _, prov := classifiedOffboxManager(t, drive)
prov.hdd["immich"] = drive
// R-356: "installed" is now asked directly (ListDeployedStacks), not inferred from owning a
// drive. The fixture must say so — before, the HDD path alone stood in for both facts, which is
// exactly the conflation this change removes.
prov.deployed = map[string]bool{"immich": true}
scratch, liveNs, err := m.offboxRestoreScratchDir("immich")
if err != nil {
@@ -61,7 +65,10 @@ func placeFixture(t *testing.T, full bool) (*Manager, *offbox3aProvider, string,
// A (F-3a-1a): undeployed placement refused with ZERO copies (never merges onto the SSD namespace).
func TestPlace_UndeployedRefused(t *testing.T) {
m, prov, scratch, copies := placeFixture(t, true)
prov.hdd["immich"] = "" // undeployed
// R-356: undeployed means ABSENT FROM ListDeployedStacks. Clearing only the HDD path no longer
// makes an app look uninstalled — a driveless app is the normal case for 40 of the 53 apps.
prov.deployed = map[string]bool{}
prov.hdd["immich"] = ""
err := m.PlaceOffsiteRestore(context.Background(), "immich")
if err == nil || !strings.Contains(err.Error(), "nincs telepítve") {
t.Fatalf("undeployed must refuse with 'nincs telepítve', got %v", err)