controller (unreleased): R-921 check-first pre-check (no stop while another tier's job is in flight); R-922 email_cleared on a household's deliberate clear
gates / gates (push) Successful in 1m0s
gates / gates (push) Successful in 1m0s
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:
@@ -0,0 +1,57 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// R-922: the clear marker's rule as a table — set only by a deliberate clear, kept while the address stays
|
||||
// empty, dropped by a new address — and it survives a reload from disk (a clear the hub missed is pushed
|
||||
// again by the next start: SyncOnStartup).
|
||||
func TestR922_ClearedByRule(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
stored *NotificationPrefs
|
||||
newEmail string
|
||||
want bool
|
||||
}{
|
||||
{"never configured, empty save", &NotificationPrefs{}, "", false},
|
||||
{"nil stored prefs", nil, "", false},
|
||||
{"had an address, empty save = clear", &NotificationPrefs{Email: "a@example.hu"}, "", true},
|
||||
{"already cleared, empty save keeps it", &NotificationPrefs{EmailCleared: true}, "", true},
|
||||
{"cleared, new address drops it", &NotificationPrefs{EmailCleared: true}, "b@example.hu", false},
|
||||
{"address changed", &NotificationPrefs{Email: "a@example.hu"}, "b@example.hu", false},
|
||||
} {
|
||||
if got := tc.stored.ClearedBy(tc.newEmail); got != tc.want {
|
||||
t.Errorf("%s: ClearedBy(%q) = %v, want %v", tc.name, tc.newEmail, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestR922_MarkerPersistsAndDrivesStartupSync(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "settings.json")
|
||||
lg := log.New(io.Discard, "", 0)
|
||||
s, err := Load(path, lg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if s.GetNotificationPrefs().SyncOnStartup() {
|
||||
t.Fatal("a never-configured box would push on startup")
|
||||
}
|
||||
if err := s.SetNotificationPrefs(&NotificationPrefs{EmailCleared: true, CooldownHours: 6}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s2, err := Load(path, lg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p := s2.GetNotificationPrefs()
|
||||
if !p.EmailCleared {
|
||||
t.Fatal("the clear marker did not survive a reload")
|
||||
}
|
||||
if !p.SyncOnStartup() {
|
||||
t.Fatal("a cleared box does not push its clear on startup — a clear the hub missed would never reach it")
|
||||
}
|
||||
}
|
||||
@@ -622,6 +622,28 @@ type NotificationPrefs struct {
|
||||
Email string `json:"email,omitempty"`
|
||||
EnabledEvents []string `json:"enabled_events,omitempty"`
|
||||
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`
|
||||
// while the address stays empty, so the hub deletes the address it holds — and a push the hub missed
|
||||
// is repaired by the next one. A box that never had an address never sets it (the hub's no-clobber
|
||||
// guard must keep protecting a seeded address); a new address drops it. Set only by
|
||||
// web.settingsNotificationsHandler via ClearedBy. Pinned by internal/web TestR922_*.
|
||||
EmailCleared bool `json:"email_cleared,omitempty"`
|
||||
}
|
||||
|
||||
// ClearedBy reports whether saving `newEmail` over these stored prefs is (or keeps) a deliberate clear:
|
||||
// the new address is empty AND the stored one was not — or the stored prefs already record a clear.
|
||||
func (p *NotificationPrefs) ClearedBy(newEmail string) bool {
|
||||
if newEmail != "" || p == nil {
|
||||
return false
|
||||
}
|
||||
return p.Email != "" || p.EmailCleared
|
||||
}
|
||||
|
||||
// SyncOnStartup reports whether the startup push should run: an address to restore after a hub DB
|
||||
// rebuild, or a household clear the hub may not have received yet (R-922).
|
||||
func (p *NotificationPrefs) SyncOnStartup() bool {
|
||||
return p != nil && (p.Email != "" || p.EmailCleared)
|
||||
}
|
||||
|
||||
// DefaultEnabledEvents are the events enabled by default for new customers.
|
||||
|
||||
Reference in New Issue
Block a user