R-1199 reopened: a box with no list of its own reads the household's from the hub and adopts it, pushes its default once only when no household saved one; every push names its source; an empty saved list stays empty; R-1216 (D28 = A): the install page's swap sentence only when host swap is really short (shared case table); unreleased
gates / gates (push) Successful in 1m55s

This commit is contained in:
2026-10-22 12:13:26 +02:00
parent 08fa440d04
commit add310c80a
13 changed files with 512 additions and 61 deletions
@@ -0,0 +1,36 @@
package settings
import (
"io"
"log"
"os"
"path/filepath"
"testing"
)
// R-1199 reopened (2026-10-22): a household that unticks every mail must see an empty list after a restart — the hub
// mails by that empty list. Before: omitempty dropped the [] and the getter turned the missing list into the defaults.
// Both shapes: a fresh save, and the file Tester 1 and demo-hp actually hold (`{"cooldown_hours": 6}` only).
// RED-PROOF: with the getter's nil → DefaultEnabledEvents restored, both assertions fail — seen failing 2026-10-22.
func TestR1199Reopened_AnEmptySavedListStaysEmpty(t *testing.T) {
p := filepath.Join(t.TempDir(), "s.json")
s, _ := Load(p, log.New(io.Discard, "", 0))
if err := s.SetNotificationPrefs(&NotificationPrefs{EnabledEvents: []string{}}); err != nil {
t.Fatal(err)
}
s2, _ := Load(p, log.New(io.Discard, "", 0))
if got := s2.GetNotificationPrefs().EnabledEvents; len(got) != 0 {
t.Fatalf("a saved empty list came back as %v", got)
}
q := filepath.Join(t.TempDir(), "old.json")
os.WriteFile(q, []byte(`{"notifications":{"cooldown_hours":6}}`), 0o600)
s3, _ := Load(q, log.New(io.Discard, "", 0))
if got := s3.GetNotificationPrefs().EnabledEvents; len(got) != 0 || !s3.NotificationPrefsSaved() {
t.Fatalf("the live boxes' file shape reads as %v (saved=%t), want an empty saved list", got, s3.NotificationPrefsSaved())
}
// A box with nothing saved still shows the defaults.
s4, _ := Load(filepath.Join(t.TempDir(), "n.json"), log.New(io.Discard, "", 0))
if got := s4.GetNotificationPrefs().EnabledEvents; len(got) != len(DefaultEnabledEvents) {
t.Fatalf("an unsaved box must show the defaults, got %d", len(got))
}
}
+14 -3
View File
@@ -649,8 +649,11 @@ func (p StoragePath) IsInternal() bool { return p.Kind == StorageKindInternal }
// NotificationPrefs holds customer notification preferences.
type NotificationPrefs struct {
Email string `json:"email,omitempty"`
EnabledEvents []string `json:"enabled_events,omitempty"`
Email string `json:"email,omitempty"`
// EnabledEvents: no omitempty since R-1199 reopened (2026-10-22) — a household that unticks every mail saves [],
// and omitempty dropped it, so after the next start the page showed every default mail ticked while the hub mailed
// by the empty list (measured on Tester 1 and demo-hp: their file held only `cooldown_hours`).
EnabledEvents []string `json:"enabled_events"`
CooldownHours int `json:"cooldown_hours,omitempty"` // default: 6
// EmailCleared (R-922, operator ruling 2026-10-09 option A) marks a DELIBERATE clear: the household
// saved an empty address where one was stored. Every hub push carries it as `email_cleared: true`
@@ -770,6 +773,11 @@ func Load(path string, logger *log.Logger) (*Settings, error) {
if err != nil {
if os.IsNotExist(err) {
logger.Printf("[INFO] [settings] No settings.json found, using defaults")
// A fresh box's defaults already carry every type the one-time add-only seeds add, so they are DONE for it.
// Unset, they ran at the second start and re-added a type the household had just switched off — the
// „a later opt-out sticks" promise of each seed was false on every new box (found 2026-10-22, R-1199
// reopened). Persisted with the first save. Pinned by TestR1199Reopened_AnEmptySavedListStaysEmpty.
s.OffboxEnlargeNoticeSeeded, s.AppUpdateEventsSeeded, s.UnhealthyStopEventSeeded = true, true, true
return s, nil
}
return nil, fmt.Errorf("reading settings file: %w", err)
@@ -1354,7 +1362,10 @@ func (s *Settings) GetNotificationPrefs() *NotificationPrefs {
prefs.CooldownHours = 6
}
if prefs.EnabledEvents == nil {
prefs.EnabledEvents = DefaultEnabledEvents
// A SAVED list with no events is a household that unticked every mail (the only writer is the settings page;
// before R-1199 reopened omitempty dropped its []). It reads as empty — never as the defaults, which the hub
// does not mail by. Pinned by TestR1199Reopened_AnEmptySavedListStaysEmpty.
prefs.EnabledEvents = []string{}
}
// Return a copy of the slice verbatim. The offbox_enlarge_blocked seed is a ONE-TIME persisted
// migration (seedOffboxEnlargeNotice at Load), NOT a getter append — so a customer's later opt-out