From 2a43c5181576998f96cd8fac2de060d879644c74 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 10:28:43 +0100 Subject: [PATCH] test(notifications): verify saved choices at monitor startup Exercise the production monitor constructor twice from persisted routing, resolve, activation and webhook filtering choices without reapplying notification setters. Document that constructor restoration does not establish browser saving, process restart or destination delivery. Change-source: pulse-maintainer --- .../v6/internal/subsystems/notifications.md | 13 ++++ .../monitor_notification_startup_test.go | 64 +++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 internal/monitoring/monitor_notification_startup_test.go diff --git a/docs/release-control/v6/internal/subsystems/notifications.md b/docs/release-control/v6/internal/subsystems/notifications.md index 7392a0964..9164cb44d 100644 --- a/docs/release-control/v6/internal/subsystems/notifications.md +++ b/docs/release-control/v6/internal/subsystems/notifications.md @@ -825,3 +825,16 @@ missed decoded query representations and a separate ntfy transport caller: provider-only helper examples were not sufficient sink coverage. This contract does not assert arbitrary response-body/third-party error secrecy, installed recipient delivery, candidate qualification or historical customer exposure. + +### Saved notification choices at monitor construction + +`internal/monitoring/monitor_notification_startup_test.go` exercises the actual +monitor constructor twice against the same saved configuration without applying +notification-manager setters in the fixture. It checks initial routing, both +resolve choices, enabled/activation gating and encrypted webhook configuration +restoration. The cases include webhook, Apprise, email and all destinations. + +This boundary uses the configuration persistence API to save choices, not the +HTTP or browser save path. It does not start a new operating-system process, +drive an alert lifecycle or establish recipient delivery. Those installed +acceptance obligations remain separate from constructor restoration coverage. diff --git a/internal/monitoring/monitor_notification_startup_test.go b/internal/monitoring/monitor_notification_startup_test.go new file mode 100644 index 000000000..4580d9195 --- /dev/null +++ b/internal/monitoring/monitor_notification_startup_test.go @@ -0,0 +1,64 @@ +package monitoring + +import ( + "reflect" + "testing" + + "github.com/rcourtman/pulse-go-rewrite/internal/alerts" + "github.com/rcourtman/pulse-go-rewrite/internal/config" + "github.com/rcourtman/pulse-go-rewrite/internal/notifications" +) + +// Cover the real monitor constructor, not a fixture which reapplies manager +// setters after restart. Saving here uses persistence directly: this is not +// browser/API save acceptance, a process restart, or destination receipt proof. +func TestNewRestoresSavedNotificationChoices(t *testing.T) { + for _, tc := range []struct { + name, target string + resolve, enabled bool + activation alerts.ActivationState + }{ + {"webhook_without_recovery", "webhook", false, true, alerts.ActivationActive}, + {"apprise_with_recovery", "apprise", true, true, alerts.ActivationActive}, + {"email_disabled", "email", true, false, alerts.ActivationActive}, + {"all_pending", "all", false, true, alerts.ActivationPending}, + } { + t.Run(tc.name, func(t *testing.T) { + dir := t.TempDir() + t.Setenv("PULSE_DATA_DIR", dir) + persistence := config.NewConfigPersistence(dir) + saved := alerts.AlertConfig{Enabled: tc.enabled, ActivationState: tc.activation, + Schedule: alerts.ScheduleConfig{InitialNotify: tc.target, NotifyOnResolve: tc.resolve}} + if err := persistence.SaveAlertConfig(saved); err != nil { + t.Fatal(err) + } + webhooks := []notifications.WebhookConfig{{ID: "saved-ops", Name: "saved-ops", URL: "https://example.invalid/alerts", Enabled: true, Service: "generic", MinimumSeverity: "warning", TagFilter: []string{"ops"}, TagMode: "any"}} + if err := persistence.SaveWebhooks(webhooks); err != nil { + t.Fatal(err) + } + for startup := 0; startup < 2; startup++ { + func() { + m, err := New(&config.Config{DataPath: dir}) + if err != nil { + t.Fatal(err) + } + defer m.Stop() + n := m.GetNotificationManager() + if got := n.GetInitialNotifyTarget(); got != tc.target { + t.Errorf("startup %d target=%q, want %q", startup, got, tc.target) + } + if got := n.GetNotifyOnResolve(); got != tc.resolve { + t.Errorf("startup %d resolve=%t, want %t", startup, got, tc.resolve) + } + wantEnabled := tc.enabled && tc.activation == alerts.ActivationActive + if got := n.IsEnabled(); got != wantEnabled { + t.Errorf("startup %d enabled=%t, want %t", startup, got, wantEnabled) + } + if got := n.GetWebhooks(); !reflect.DeepEqual(got, webhooks) { + t.Errorf("startup %d did not restore saved webhook configuration", startup) + } + }() + } + }) + } +}