Files
felhom-controller/controller/internal/report/escrow_presence_test.go
T
admin 1214bae0a2 R-204 item 4 (box half): a rebuilt box DECLARES that it needs a credential (v0.199.0)
An absent off-site object has four meanings — never configured, mid-restart, a
transient config read failure, and rebuilt-and-stranded — and the hub cannot tell
them apart. The box can, from two local facts it holds with certainty, so it says
so instead of leaving the hub to deduce it from a silence (operator ruling).

The ACK's identity_blob_present is now recorded on EVERY ACK, before the gates
that used to discard it: on a box with no off-site target the auto-confirm returns
immediately, which is exactly a rebuilt box, so the one fact distinguishing it from
a box that never had off-site backups was thrown away every cycle.

The declaration needs BOTH halves — a fresh data area AND a hub-held recovery
package. Freshness alone is a box that never had off-site backups; dropping that
condition makes the whole fleet ask for credentials, which is what the Scenario B
test exists to catch.

The object carries enabled:false and zero sizes, which is what makes it inert to
the hub's existing fill and staleness checkers and to a pre-upgrade hub. A
configured box's JSON is byte-identical to v0.198.0's.
2026-08-05 10:47:51 +02:00

82 lines
3.4 KiB
Go

package report
import (
"io"
"log"
"testing"
)
// R-204 item 4 / R-193 — the ACK's `identity_blob_present` must be recorded on EVERY ACK, including
// (especially) on a box with no off-site target.
//
// THE DEFECT THIS PINS: Reconcile returns early when the box is neither pending nor escrowed, which
// is exactly a REBUILT box's state — so the one fact that distinguishes it from a box that never had
// off-site backups was discarded on every cycle. The recorder therefore runs BEFORE every gate, and
// the test drives Reconcile itself rather than calling the recorder, because the ordering IS the fix.
func TestEscrowConfirm_RecordsPresenceEvenWhenOffboxUnconfigured(t *testing.T) {
var recorded []bool
c := &EscrowAutoConfirmer{
// The unconfigured-box shape: neither pending nor escrowed. Every gate below will skip.
Pending: func() bool { return false },
Escrowed: func() bool { return false },
LocalHash: func() (string, bool) { return "", false },
Flip: func() error { t.Fatal("an unconfigured box must never flip"); return nil },
RecordPresence: func(p bool) error { recorded = append(recorded, p); return nil },
Logger: log.New(io.Discard, "", 0),
}
c.Reconcile(&EscrowStatus{IdentityBlobPresent: true, ResticPwSHA256: "SHA1"})
if len(recorded) != 1 || !recorded[0] {
t.Fatalf("presence not recorded on an unconfigured box: %v — a rebuilt box cannot learn the hub holds its recovery package", recorded)
}
// It must also record the NEGATIVE, so a customer RESET (the hub losing its escrow row) turns the
// box's declaration back off. A set-only flag would strand the declaration forever.
c.Reconcile(&EscrowStatus{IdentityBlobPresent: false})
if len(recorded) != 2 || recorded[1] {
t.Fatalf("a false presence was not recorded: %v", recorded)
}
}
// A nil ACK escrow object records nothing (an old hub, or no escrow row) — absence of a statement is
// not a statement of absence, and overwriting a known-true with false here would un-declare a genuinely
// stranded box every time an old hub answered.
func TestEscrowConfirm_NilAckRecordsNothing(t *testing.T) {
called := false
c := &EscrowAutoConfirmer{
Pending: func() bool { return false },
Escrowed: func() bool { return false },
RecordPresence: func(bool) error { called = true; return nil },
Logger: log.New(io.Discard, "", 0),
}
c.Reconcile(nil)
if called {
t.Fatal("a nil ACK escrow object must not record a presence")
}
}
// A recorder FAILURE must be logged, not swallowed, and must not stop the auto-confirm — the two are
// independent concerns and a failed settings write must not also break escrow confirmation.
func TestEscrowConfirm_RecordFailureDoesNotBlockAutoConfirm(t *testing.T) {
flipped := false
c := &EscrowAutoConfirmer{
Pending: func() bool { return true },
Escrowed: func() bool { return false },
LocalHash: func() (string, bool) { return "MATCH", true },
Flip: func() error { flipped = true; return nil },
RecordPresence: func(bool) error { return errRecord },
Logger: log.New(io.Discard, "", 0),
}
c.Reconcile(&EscrowStatus{IdentityBlobPresent: true, ResticPwSHA256: "MATCH"})
if !flipped {
t.Fatal("a presence-record failure blocked the escrow auto-confirm — they are independent")
}
}
type recordErr struct{}
func (recordErr) Error() string { return "record failed" }
var errRecord = recordErr{}