v0.295.0: a box that was off at its backup time catches up once (R-871, decision 109); the missed-backup banner (decision 110); a late daily timer after a host suspend is skipped
gates / gates (push) Successful in 31s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-05 09:27:16 +02:00
parent e730a629fd
commit 635c33d381
24 changed files with 2074 additions and 18 deletions
@@ -0,0 +1,123 @@
package nightchain
import (
"testing"
"time"
)
// R-871 / decision 110 — the banner's rules. Red-proofs: catchup-2026-10-05/partB/red-proofs.txt.
func laptop() *HourDays { // on 18:00–23:59 every day of the week (Tester 2's shape)
var h HourDays
for k := 18; k <= 23; k++ {
h[k] = 7
}
return &h
}
func offAt(on bool) func(time.Time) (bool, bool) {
return func(time.Time) (bool, bool) { return on, true }
}
func base(now time.Time) BannerInput {
return BannerInput{Now: now, Window: "02:30", Loc: bud, SeededAt: at(1, 12, 0), DBDumpOK: at(3, 2, 31),
OnAtW: offAt(false), Hours: laptop()}
}
// Shown: the last dump is 2+ days old, the box was off at 02:30, and the record suggests 21:00.
// RED-PROOF: return Banner{} when stale (the pre-v0.295.0 silence) → "not shown".
func TestBanner_ShownWithReasonAndSuggestion(t *testing.T) {
b := ComputeBanner(base(at(5, 19, 0)))
if !b.Show {
t.Fatal("not shown although the last backup is 2 days old")
}
if b.OffAt != "02:30" || b.Suggest != "21:00" || b.DaysAgo != 2 {
t.Fatalf("banner = %+v, want off at 02:30, suggest 21:00, 2 days ago", b)
}
}
// Not shown: a backup within the last 26 h.
func TestBanner_NotShownWhenFresh(t *testing.T) {
in := base(at(5, 19, 0))
in.DBDumpOK = at(5, 2, 31)
if b := ComputeBanner(in); b.Show {
t.Fatalf("shown although the last dump is 16 h old: %+v", b)
}
}
// Not shown right after an upgrade (the ledger was seeded less than 26 h ago and knows no dump).
func TestBanner_NotShownBeforeTheRecordKnows(t *testing.T) {
in := base(at(5, 19, 0))
in.SeededAt, in.DBDumpOK = at(5, 9, 0), time.Time{}
if b := ComputeBanner(in); b.Show {
t.Fatalf("shown on the day of the upgrade: %+v", b)
}
}
// Dismissed: closed until the NEXT missed backup time, then back.
// RED-PROOF: drop the Dismissed check → "a closed banner came back without a new miss".
func TestBanner_DismissedThenBackAfterANewMiss(t *testing.T) {
in := base(at(5, 19, 0))
b := ComputeBanner(in)
in.Dismissed = b.MissedAt // the household closed it (02:30 on the 5th)
if ComputeBanner(in).Show {
t.Fatal("a closed banner came back without a new miss")
}
in.Now = at(5, 23, 0) // same evening — still the same miss
if ComputeBanner(in).Show {
t.Fatal("a closed banner came back the same evening")
}
in.Now = at(6, 19, 0) // off again at 02:30 on the 6th
if !ComputeBanner(in).Show {
t.Fatal("the banner did not come back after the next missed backup time")
}
}
// Gone by itself after a successful night (e.g. the catch-up ran).
func TestBanner_GoneAfterASuccess(t *testing.T) {
in := base(at(5, 19, 0))
in.DBDumpOK = at(5, 18, 20) // the catch-up's dump
if ComputeBanner(in).Show {
t.Fatal("still shown after a successful backup")
}
}
// The off-site tier counts when configured: a fresh dump with a 3-day-old off-site copy still shows.
func TestBanner_OffsiteTierCounts(t *testing.T) {
in := base(at(5, 19, 0))
in.DBDumpOK = at(5, 18, 20)
in.OffsiteConfigured, in.OffsiteOK = true, at(2, 4, 20)
b := ComputeBanner(in)
if !b.Show || b.DaysAgo != 3 {
t.Fatalf("banner = %+v, want shown with the off-site copy's 3 days", b)
}
}
// No clear pattern → no suggestion ("choose a time when the box is usually on"); box ON at W → no "off at".
// RED-PROOF: let Suggest return the first start without checking the days → a time is suggested from noise.
func TestBanner_NoPatternNoSuggestion(t *testing.T) {
in := base(at(5, 19, 0))
var noisy HourDays
for k := 18; k <= 23; k++ {
noisy[k] = 2 // on some evenings — 2 of 7 days is not "usually"
}
in.Hours, in.OnAtW = &noisy, offAt(true)
b := ComputeBanner(in)
if !b.Show || b.Suggest != "" || b.OffAt != "" {
t.Fatalf("banner = %+v, want shown, no suggestion, no 'off at'", b)
}
}
// A box usually ON at its window gets no new time (the miss has another cause).
func TestBanner_UsuallyOnAtWNoSuggestion(t *testing.T) {
var always HourDays
for k := range always {
always[k] = 7
}
if s := Suggest(always, 2); s != "" {
t.Fatalf("suggested %q for a box that is on at its window", s)
}
if s := Suggest(*laptop(), 2); s != "21:00" {
t.Fatalf("laptop suggestion %q, want 21:00", s)
}
}