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
+22
View File
@@ -374,6 +374,28 @@ func (m *Manager) knownStackNames() []string {
return names
}
// isStackDeployed reports whether the named app is currently deployed on this box. R-356: the
// restore side used to answer "is it installed?" by asking "does it have a drive?" — true for the 13
// needs_hdd apps and permanently false for the other 40, which are correctly driveless. The two
// questions are now separate, and this one is the one about installation.
//
// Nil provider ⇒ FALSE, deliberately: with no provider we cannot tell whether the app exists, and the
// caller's next act is to WRITE customer data to a destination. "Cannot tell" must not become
// "go ahead" — it fails closed into the refusal, which is recoverable, instead of into a copy, which
// is not.
func (m *Manager) isStackDeployed(stack string) bool {
stack = strings.TrimSpace(stack)
if stack == "" {
return false
}
for _, name := range m.knownStackNames() {
if name == stack {
return true
}
}
return false
}
// groupStacksByDrive groups deployed stacks by their home drive path.
func (m *Manager) groupStacksByDrive() map[string][]StackSummary {
if m.stackProvider == nil {