An unknown drawn as a zero: the defect v0.226.0's own fix introduced
gates / gates (push) Failing after 13s

Writing the REPORT's observation "the no-unit fallback already reports a zero
result, which is honest" exposed that the sentence was FALSE.

A zero UnitRestoreResult is Scenario B's shape. So RestoreFromRecoveryUnit's
fallback to RestoreApp -- which returns only an error, and whose signature is
deliberately out of scope -- would have printed "ez a mentes csak a
beallitasokat tartalmazta, adatot nem" over a restore that may have replayed the
app's entire dataset. That is an unknown drawn as a zero: the exact R-88 failure
direction this whole change exists to remove, re-introduced by the change.

UnitRestoreResult now carries CountsUnknown, the fallback sets it, and there is a
fourth sentence claiming only what is known -- the restore ran, the app is back,
and we cannot say what came back. RestoreApp's signature is untouched.

Pinned by TestUnitRestoreOutcome_NoUnitFallbackSaysUnknownNotEmpty. The A5 seam
test was corrected too: its fixture has no recovery unit, so it exercises exactly
this path and had been asserting the wrong sentence -- it now asserts the
unknown, which is what pins the fallback to it.

IT WAS THE observations GATE REFUSING THE PUSH THAT FORCED THE RE-READ. A gate
written to stop findings dying in an overwritten REPORT.md caught a live defect
instead. Also files R-397 (NotifyIntegrityOK/Failed are dead code AND the
monitoring page advertises a weekly integrity check that does not exist) and
R-398 (resticStep is not a seam, which is why R-358's ordering needed an AST
test) rather than leaving them in a file that is overwritten every session.

REPORT.md is the full run record: baselines re-confirmed, per-test results, the
five red-proofs with their observed output, the live validation with verbatim
Hungarian messages, what was NOT validated and why, teardown across three
layers, and the register 165 -> 167 -> 161.

Green gate clean: 28 packages, rc 0. All 12 controller gates OK.
This commit is contained in:
2026-08-30 19:51:16 +02:00
parent e4e0aa8f46
commit c0c8fe67bf
5 changed files with 145 additions and 26 deletions
+10
View File
@@ -1528,6 +1528,12 @@ func unitRestoreOutcomeMsg(app string, res backup.UnitRestoreResult) string {
}
return fmt.Sprintf(unitRestoreDataMsgFmt, app, what)
}
// An unknown must never be drawn as a zero. The no-unit fallback cannot report counts, and the
// zero-value shape would otherwise read as „the backup held only settings" over a restore that may
// have replayed the app's whole dataset. Same failure direction as R-88: degrade to UNKNOWN.
if res.CountsUnknown {
return fmt.Sprintf(unitRestoreCountsUnknownMsgFmt, app)
}
if res.ManifestVolumes+res.ManifestDBs > 0 {
return fmt.Sprintf(unitRestoreNoneReturnedMsgFmt, app, res.ManifestVolumes, res.ManifestDBs)
}
@@ -1551,6 +1557,10 @@ const (
// database dumps LISTED. It states the data is unchanged because that is true and load-bearing: the
// replay only ever writes into volumes, so a replay that did nothing removed nothing, and a customer
// who believes otherwise will do something worse than waiting.
// unitRestoreCountsUnknownMsgFmt — the no-unit fallback. It claims only what is known: the restore
// ran and the app is back. It deliberately does NOT say data returned and does NOT say it did not.
unitRestoreCountsUnknownMsgFmt = "A(z) %s visszaállítása lefutott — az alkalmazás újraindult. Ehhez a mentéshez nem tartozik mentési egység, ezért nem tudjuk megmondani, mi állt vissza belőle. Ellenőrizd az alkalmazásban, hogy megvannak-e az adataid."
unitRestoreNoneReturnedMsgFmt = "A(z) %s: FIGYELEM — a mentés %d adatkötetet és %d adatbázis-mentést sorol fel, de egyik sem állt vissza. Az adataid változatlanok maradtak. Kérj segítséget, mielőtt újra próbálod."
)
@@ -170,14 +170,49 @@ func TestR353_HandlerPublishesTheOutcome(t *testing.T) {
t.Fatal("the restore never reached a terminal status")
}
// There is no recovery unit and no data on this drive, so nothing came back: Scenario B.
// THE ASSERTION THAT CARRIES THE SEAM: whatever branch fires, the sentence the customer is shown
// must be `unitRestoreOutcomeMsg`'s output and not the pre-fix string.
if strings.Contains(last, "visszaállítva (snap-123)") {
t.Fatalf("THE PRE-FIX SENTENCE REACHED THE CUSTOMER: %q — it is true of a restore that "+
"returned an entire dataset and of one that returned nothing", last)
}
for _, want := range []string{"csak a beállításokat tartalmazta", "NEM álltak vissza"} {
if !strings.Contains(last, want) {
t.Fatalf("the published outcome does not state that no data came back; missing %q in %q", want, last)
}
// This fixture has no recovery unit, so the production path takes the RestoreApp fallback — where
// the counts are genuinely unknown. The honest sentence for that is the unknown one, and asserting
// it here is what pins the fallback to it: the zero-value shape would otherwise print
// „csak a beállításokat tartalmazta" over a restore that may have returned everything.
if strings.Contains(last, "csak a beállításokat tartalmazta") {
t.Fatalf("the no-unit fallback claimed the backup held no data, from counts it never "+
"established: %q", last)
}
if !strings.Contains(last, "nem tudjuk megmondani") {
t.Fatalf("the published outcome does not state the unknown as unknown; got %q", last)
}
}
// TestUnitRestoreOutcome_NoUnitFallbackSaysUnknownNotEmpty — the defect the first draft of this fix
// introduced, caught by the observations gate forcing a re-read of my own code.
//
// `RestoreFromRecoveryUnit` falls back to `RestoreApp` when there is no recovery unit, and `RestoreApp`
// returns only an error — its signature is deliberately out of scope. So the result is a ZERO value,
// and a zero `UnitRestoreResult` is Scenario B's shape: „ez a mentés csak a beállításokat tartalmazta,
// adatot nem". That sentence would be printed over a fallback restore that had just replayed the app's
// entire dataset. **An unknown drawn as a zero is the R-88 failure direction**, and it is exactly what
// this whole change exists to remove — so it must not be re-introduced by the fix itself.
func TestUnitRestoreOutcome_NoUnitFallbackSaysUnknownNotEmpty(t *testing.T) {
msg := unitRestoreOutcomeMsg("legacyapp", backup.UnitRestoreResult{CountsUnknown: true})
if strings.Contains(msg, "csak a beállításokat tartalmazta") {
t.Fatal("FALSE CLAIM: told the customer the backup held no data when the counts were never " +
"established — a fallback restore may have returned everything they own")
}
if strings.Contains(msg, "adatkötet") {
t.Fatal("claimed a volume count that was never measured")
}
if !strings.Contains(msg, "nem tudjuk megmondani") {
t.Errorf("an unknown must be STATED as unknown, not left silent; got %q", msg)
}
// And it must still tell them the restore ran, or the sentence reads as a failure.
if !strings.Contains(msg, "lefutott") {
t.Errorf("the message must say the restore completed; got %q", msg)
}
}