controller v0.264.0: the household is told when an update is undone or held, in its language
gates / gates (push) Successful in 25s
gates / gates (push) Successful in 25s
app_update_undone / app_update_held events (09 decision 15), on by default and seeded once on existing boxes; R-606 update sentences as key+args rendered per reader; R-646 startup applied-meta backfill for apps current with the catalog; R-620 a disabled notifier WARNs once per event type. Needs hub v0.120.0. 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,48 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// v0.264.0 — the one-time, ADD-ONLY seed of the two update event types into a household's stored prefs.
|
||||
//
|
||||
// COMPANION RED-PROOF (REPORT.md): drop the seedAppUpdateEvents call from Load — the first assertion fails.
|
||||
func TestSeedAppUpdateEvents_AddOnlyOnce(t *testing.T) {
|
||||
p := filepath.Join(t.TempDir(), "settings.json")
|
||||
if err := os.WriteFile(p, []byte(`{"notifications":{"email":"h@example.hu","enabled_events":["backup_failed"]}}`), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lg := log.New(io.Discard, "", 0)
|
||||
s, err := Load(p, lg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ev := s.Notifications.EnabledEvents
|
||||
has := func(list []string, x string) bool {
|
||||
for _, e := range list {
|
||||
if e == x {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
if !has(ev, "backup_failed") || !has(ev, "app_update_undone") || !has(ev, "app_update_held") {
|
||||
t.Fatalf("the household's choice must be kept and both types added, got %v", ev)
|
||||
}
|
||||
// The household opts out of one; a restart must NOT add it back.
|
||||
s.Notifications.EnabledEvents = []string{"backup_failed", "app_update_held"}
|
||||
if err := s.save(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s2, err := Load(p, lg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if has(s2.Notifications.EnabledEvents, "app_update_undone") {
|
||||
t.Errorf("a later opt-out must stick — the seed runs once, got %v", s2.Notifications.EnabledEvents)
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,9 @@ type Settings struct {
|
||||
// existing customer's stored prefs. Persisted so a later opt-out sticks (the 3a-fix getter append
|
||||
// re-enabled it on every read — this replaces it).
|
||||
OffboxEnlargeNoticeSeeded bool `json:"offbox_enlarge_notice_seeded,omitempty"`
|
||||
// AppUpdateEventsSeeded (v0.264.0) guards the ONE-TIME add of app_update_undone / app_update_held
|
||||
// into an existing household's stored prefs (seedAppUpdateEvents).
|
||||
AppUpdateEventsSeeded bool `json:"app_update_events_seeded,omitempty"`
|
||||
|
||||
// HubEscrowIdentityPresent (v0.199.0, R-204 item 4 / R-193) caches the report ACK's
|
||||
// `escrow.identity_blob_present` — whether the HUB is holding a sealed recovery package for this
|
||||
@@ -604,6 +607,10 @@ var DefaultEnabledEvents = []string{
|
||||
"expected_backup_missed",
|
||||
"expected_dbdump_missed",
|
||||
"offbox_enlarge_blocked", // 3a-fix (warning-class): remote enlargement refused by the quota gate
|
||||
// v0.264.0 (`09` §3 decision 15): the household is told ONCE when an update is undone, and when it
|
||||
// (or its undo) failed and the app is held. On by default; seeded into existing prefs once below.
|
||||
"app_update_undone",
|
||||
"app_update_held",
|
||||
}
|
||||
|
||||
// PendingEvent is an event queued for the next Hub push cycle.
|
||||
@@ -697,6 +704,7 @@ func Load(path string, logger *log.Logger) (*Settings, error) {
|
||||
}
|
||||
s.migrateResticToRsync()
|
||||
s.seedOffboxEnlargeNotice()
|
||||
s.seedAppUpdateEvents()
|
||||
return s, nil
|
||||
}
|
||||
|
||||
@@ -718,6 +726,29 @@ func (s *Settings) seedOffboxEnlargeNotice() {
|
||||
}
|
||||
}
|
||||
|
||||
// seedAppUpdateEvents runs ONCE (guarded by AppUpdateEventsSeeded, v0.264.0) — the
|
||||
// seedOffboxEnlargeNotice shape. A household whose stored prefs predate app_update_undone /
|
||||
// app_update_held gets both appended: they could not have switched off a type that did not exist.
|
||||
// ADD-ONLY, never removes a choice; persisted, so a LATER opt-out sticks. The hub runs the same one-time
|
||||
// add on its side — BOTH are needed, because this list is pushed to the hub on every save of the
|
||||
// notification page and would otherwise take the types away again.
|
||||
func (s *Settings) seedAppUpdateEvents() {
|
||||
if s.AppUpdateEventsSeeded {
|
||||
return
|
||||
}
|
||||
s.AppUpdateEventsSeeded = true
|
||||
if s.Notifications != nil && s.Notifications.EnabledEvents != nil {
|
||||
s.Notifications.EnabledEvents = appendIfAbsent(s.Notifications.EnabledEvents, "app_update_undone")
|
||||
s.Notifications.EnabledEvents = appendIfAbsent(s.Notifications.EnabledEvents, "app_update_held")
|
||||
if s.log != nil {
|
||||
s.log.Printf("[INFO] [settings] app update events added to the household's notification prefs (one-time, add-only)")
|
||||
}
|
||||
}
|
||||
if err := s.save(); err != nil && s.log != nil {
|
||||
s.log.Printf("[ERROR] [settings] Failed to save the app-update-events seed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// migrateResticToRsync converts any cross-drive backup configs using restic to rsync.
|
||||
// Called once during Load() before the mutex is exposed.
|
||||
func (s *Settings) migrateResticToRsync() {
|
||||
|
||||
Reference in New Issue
Block a user