R-1182: a finished off-site abandonment closes on the hub's abandon_purge_done (hub v0.163.0 keeps other copies' packages); unreleased
gates / gates (push) Successful in 1m18s

This commit is contained in:
2026-10-21 13:36:12 +02:00
parent 01c967e49e
commit 8f5bea0148
4 changed files with 68 additions and 2 deletions
+8 -1
View File
@@ -1,4 +1,11 @@
## Unreleased (2026-10-20) — the monitoring page's storage labels follow the reader's language (W26); text only
## Unreleased (2026-10-20/21) — the monitoring page's storage labels follow the reader's language (W26); a finished off-site abandonment closes on the hub's word (R-1182)
- **R-1182 (2026-10-21, with hub v0.163.0):** the hub now purges only the recovery packages that open the set-aside copy
it deleted, and keeps the packages of other stored copies, so `superseded_present` can stay true after a finished
abandonment. The ACK field `escrow.abandon_purge_done` now reaches `report.EscrowStatus`; the confirmer's new hook
`AbandonPurgeDone` (wired in `main.go`) closes the box's declaration. An older hub sends no field → the old rule
holds. Tests `TestR1182_TheHubsPurgeDoneClosesTheDeclaration` (red-proved: the call removed → `hook calls = 0`) and
`TestR1182_MainWiresAbandonPurgeDone`.
Found by the Tester-2 rehearsal of 2026-10-20 (felhom.eu `audits/rehearsal-2026-10-20/B6-failures/s16-README.md`, word
W26): on the English dashboard the monitoring page showed every storage label and purpose in Hungarian. No release in
+8 -1
View File
@@ -1137,7 +1137,14 @@ func main() {
// on every ACK and discarded. Wired at the same point as the two above, deliberately —
// a second wiring site in this file is how six features got built and never reached.
RecordEscrowKeyHash: sett.SetHubEscrowKeySHA256,
Logger: logger,
// R-1182 (hub v0.163.0): the hub finished this box's abandonment purge while it rightly keeps the
// packages of OTHER set-aside copies — close the declaration (superseded_present may stay true).
AbandonPurgeDone: func() {
if backupMgr != nil {
backupMgr.ClearAbandonPurgeIfConfirmed(false)
}
},
Logger: logger,
}
// R-314/R-279/R-177 (`09` §3 decision 185): the operator's closed list of actions, carried in the
// report reply and run in THIS process (opactions_wiring.go). The result rides the next report;
@@ -36,6 +36,11 @@ type EscrowStatus struct {
// package" and simply keeps today's message: an older hub cannot make the screen say anything new.
SupersededPresent bool `json:"superseded_present"`
SupersededAt string `json:"superseded_at"`
// AbandonPurgeDone (R-1182, hub v0.163.0) — the hub answers THIS box's abandonment declaration: the packages that
// open the deleted set-aside copy are gone. The hub now keeps the packages of OTHER stored copies, so
// SupersededPresent can stay true for good after a finished abandonment; this field is what closes the
// declaration then. Absent on an older hub → false → the old rule (SupersededPresent=false) still closes it.
AbandonPurgeDone bool `json:"abandon_purge_done"`
}
// EscrowAutoConfirmer runs the auto-confirm check on each report ACK. Long-lived (one per process) so
@@ -70,6 +75,10 @@ type EscrowAutoConfirmer struct {
// immediately, so the one fact that distinguishes a REBUILT box from a box that never had
// off-site backups was thrown away on every cycle. nil → not recorded (older wiring, tests).
RecordPresence func(present bool) error
// AbandonPurgeDone is called when the ACK says the hub finished THIS box's abandonment purge (R-1182) — the box
// closes its declaration. Wired at the same point as RecordPresence, for the reason given there. nil → the
// declaration closes only on SupersededPresent=false (the pre-R-1182 rule).
AbandonPurgeDone func()
// RecordSuperseded persists the ACK's `superseded_present` / `superseded_at` (v0.201.0, R-222) —
// whether the hub is ALSO keeping an EARLIER sealed package for this box. Wired here for the same
// reason as RecordPresence: this is the one place the ACK's escrow object already arrives, and a
@@ -157,6 +166,10 @@ func (c *EscrowAutoConfirmer) Reconcile(es *EscrowStatus) {
c.logf("[WARN] [escrow-confirm] could not record the hub's superseded-package state (present=%v): %v", es.SupersededPresent, err)
}
}
// R-1182: the hub confirmed this box's abandonment purge. Same place and discipline as the two recorders above.
if es.AbandonPurgeDone && c.AbandonPurgeDone != nil {
c.AbandonPurgeDone()
}
// R-241: persist the hash the hub's package covers, with the moment we heard it. Same discipline,
// same place, same reason as the two above — and this one is the fact the recovery screen has been
// unable to see. A record failure is logged, never swallowed, and never blocks the auto-confirm.
@@ -0,0 +1,39 @@
package report
import (
"io"
"log"
"testing"
)
// R-1182 (hub v0.163.0): the hub now keeps the packages of OTHER set-aside copies after an abandonment, so
// superseded_present can stay true for good; `abandon_purge_done` is what closes this box's declaration. The hook
// runs on an unconfigured box too (the declaring box has deleted its set-aside copy; its target may be anything).
// COMPANION RED-PROOF: drop the AbandonPurgeDone call in Reconcile → the first assertion fails.
func TestR1182_TheHubsPurgeDoneClosesTheDeclaration(t *testing.T) {
calls := 0
c := &EscrowAutoConfirmer{
Pending: func() bool { return false },
Escrowed: func() bool { return false },
AbandonPurgeDone: func() { calls++ },
Logger: log.New(io.Discard, "", 0),
}
c.Reconcile(&EscrowStatus{IdentityBlobPresent: true, SupersededPresent: true, AbandonPurgeDone: true})
if calls != 1 {
t.Fatalf("purge-done hook calls = %d, want 1 — the box would declare its abandonment for ever", calls)
}
c.Reconcile(&EscrowStatus{IdentityBlobPresent: true, SupersededPresent: true})
if calls != 1 {
t.Fatalf("hook ran on an ACK without abandon_purge_done (calls = %d)", calls)
}
}
func TestR1182_MainWiresAbandonPurgeDone(t *testing.T) {
found, saw := confirmerFieldIsWired(t, "AbandonPurgeDone")
if !saw {
t.Fatal("no EscrowAutoConfirmer literal found in main.go")
}
if !found {
t.Fatal("EscrowAutoConfirmer is built WITHOUT AbandonPurgeDone — a finished abandonment never closes (R-1182)")
}
}