package report import ( "context" "log" "strings" "testing" "bytes" ) // Scenario F (v0.127.0) — the escrowed-state stale-blob re-check. The §8 truth table's NEW rows: // an ESCROWED box whose hub blob does not cover the current password (hash mismatch, or a present // blob with an EMPTY hash — the spike's hash-less supersession) raises a display-only stale flag // + ONE warn per distinct hub hash. State never flips; nothing blocks. type staleHarness struct { *confirmerHarness escrowed bool } func newStaleHarness(t *testing.T) *staleHarness { t.Helper() h := &staleHarness{confirmerHarness: &confirmerHarness{local: hubHash, localOK: true, logbuf: &bytes.Buffer{}}} h.escrowed = true // the box state under test h.c = &EscrowAutoConfirmer{ Pending: func() bool { return h.pending }, Escrowed: func() bool { return h.escrowed }, LocalHash: func() (string, bool) { return h.local, h.localOK }, Flip: func() error { h.flips++; return nil }, Wipe: func(context.Context) error { h.wipes++; return nil }, Logger: log.New(h.logbuf, "", 0), } return h } // escrowed + hash mismatch → stale flag + ONE warn (deduped per distinct hub hash), no flip. func TestEscrowStale_MismatchWarnsOnceAndFlags(t *testing.T) { h := newStaleHarness(t) h.local = otherHash h.c.Reconcile(matchStatus(hubHash)) if !h.c.StaleBlob() { t.Fatal("mismatch on an escrowed box must raise the stale flag") } if h.flips != 0 { t.Fatal("the stale re-check must NEVER flip state (no auto-UN-confirm)") } if got := strings.Count(h.logbuf.String(), "STALE escrow"); got != 1 { t.Fatalf("want exactly 1 STALE warn, got %d: %s", got, h.logbuf.String()) } // Dedupe: the same hub hash again → still exactly one warn; the flag stays up. h.c.Reconcile(matchStatus(hubHash)) if got := strings.Count(h.logbuf.String(), "STALE escrow"); got != 1 { t.Fatalf("same stale hash must warn ONCE, got %d", got) } if !h.c.StaleBlob() { t.Fatal("flag must persist across deduped ACKs") } // A NEW distinct stale hash → warns again. h.c.Reconcile(matchStatus("2222222222222222222222222222222222222222222222222222222222222222")) if got := strings.Count(h.logbuf.String(), "STALE escrow"); got != 2 { t.Fatalf("a new distinct stale hash must warn again, got %d", got) } } // escrowed + blob present with an EMPTY hash (the spike's exact hash-less supersession) → stale // + one warn under the hashless dedupe sentinel. func TestEscrowStale_HashlessBlobWarnsOnce(t *testing.T) { h := newStaleHarness(t) h.c.Reconcile(&EscrowStatus{IdentityBlobPresent: true}) // blob present, hash empty if !h.c.StaleBlob() { t.Fatal("a hash-less superseding blob must raise the stale flag") } if got := strings.Count(h.logbuf.String(), "NO password hash"); got != 1 { t.Fatalf("want the hash-less warn once, got %d: %s", got, h.logbuf.String()) } h.c.Reconcile(&EscrowStatus{IdentityBlobPresent: false}) // K-only legacy shape — still hash-less if got := strings.Count(h.logbuf.String(), "NO password hash"); got != 1 { t.Fatalf("hash-less must dedupe under its sentinel, got %d warns", got) } } // escrowed + hash MATCHES → clears an earlier stale flag; no warn on the clean path. func TestEscrowStale_MatchClearsFlag(t *testing.T) { h := newStaleHarness(t) h.local = otherHash h.c.Reconcile(matchStatus(hubHash)) // go stale if !h.c.StaleBlob() { t.Fatal("setup: expected stale") } h.local = hubHash h.c.Reconcile(matchStatus(hubHash)) // a covering blob arrives (re-ceremony ran) if h.c.StaleBlob() { t.Fatal("a matching hash must CLEAR the stale flag") } // And a clean box never warns. h2 := newStaleHarness(t) h2.c.Reconcile(matchStatus(hubHash)) if h2.c.StaleBlob() || strings.Contains(h2.logbuf.String(), "STALE") { t.Fatalf("match must be silent: %s", h2.logbuf.String()) } } // Not-escrowed / no-local-password / nil-status rows: the re-check never runs (silent). func TestEscrowStale_SilentRows(t *testing.T) { h := newStaleHarness(t) h.escrowed = false // offbox not configured (or any non-escrowed state) h.c.Reconcile(matchStatus(otherHash)) if h.c.StaleBlob() || h.logbuf.Len() != 0 { t.Fatalf("non-escrowed must be silent: %s", h.logbuf.String()) } h2 := newStaleHarness(t) h2.localOK = false // no local repo password file h2.c.Reconcile(matchStatus(otherHash)) if h2.c.StaleBlob() || h2.logbuf.Len() != 0 { t.Fatal("no local password → nothing to compare → silent") } h3 := newStaleHarness(t) h3.c.Reconcile(nil) // no escrow row at all (blob absent — out of the truth table) if h3.c.StaleBlob() || h3.logbuf.Len() != 0 { t.Fatal("nil status must be silent") } } // A fresh pending→escrowed auto-confirm clears any stale leftovers (the flag must not survive a // successful re-ceremony's confirm). func TestEscrowStale_AutoConfirmClears(t *testing.T) { h := newStaleHarness(t) h.local = otherHash h.c.Reconcile(matchStatus(hubHash)) // stale while escrowed if !h.c.StaleBlob() { t.Fatal("setup: expected stale") } // The re-ceremony re-staged + re-uploaded; the box re-enters pending (edit flow) and the new // blob covers the local password → auto-confirm path runs and must clear the flag. h.escrowed = false h.pending = true h.c.Reconcile(matchStatus(otherHash)) if h.flips != 1 { t.Fatal("setup: auto-confirm should have flipped") } if h.c.StaleBlob() { t.Fatal("a hub-verified auto-confirm must clear the stale flag") } }