New OPERATOR-ONLY event type recovery_unit_capture_failed (controller v0.191.0, R-158): in allowedEventTypes AND notify.operatorOnlyEvents. Deliberately not a reuse of backup_failed, which carries customer copy and sits in the controller's DefaultEnabledEvents — reusing it would email the customer in Hungarian about a failure they cannot act on. R-158's own proposal said backup_failed; D-c overrides it. disk_warning/disk_critical lose their generic customerMessages entries. Both were allowlisted, copy'd, default-enabled and checkbox'd with NO producer anywhere; controller v0.191.0 becomes that producer and sends a DYNAMIC Hungarian message naming the drive and its free space. FormatCustomerEmail prefers the entry over the message, so keeping a static entry would discard the label and the byte figures — the same reason offbox_enlarge_blocked and disk_health_degraded have none. The deletion is pinned by a test. New notify.IsOperatorOnly so the api package can pin BOTH registers of a new event type in ONE test; allowlisted-but-not-operator-only is invisible when they are checked separately, and it is the defect v0.78.0 shipped. The register itself stays unexported. REUSE.md's "new event type" extension point rewritten: it told readers to always add a customerMessages entry, which is wrong for operator-only types and harmful for dynamic-message ones. Tests 574 -> 579. Red-proof: removing the operatorOnlyEvents entry shows the customer being emailed; the skipped/operator_only row is asserted as a positive observable.
This commit is contained in:
@@ -1573,6 +1573,20 @@ var allowedEventTypes = map[string]bool{
|
||||
"whole_guest_backup_failed": true,
|
||||
"whole_guest_backup_recovered": true,
|
||||
|
||||
// R-158 / R-167 (controller v0.191.0, decision D-c): a per-app Tier-1 recovery-unit capture
|
||||
// failed. Until then a `[WARN]` line in the controller reached no hub channel at all — the fifth
|
||||
// instance in this project of a mechanism built and left disconnected.
|
||||
//
|
||||
// DELIBERATELY NOT `backup_failed`, for exactly the reason recorded above for the whole-guest
|
||||
// pair: that type carries a customerMessages entry AND sits in the controller's
|
||||
// DefaultEnabledEvents, so reusing it emails the CUSTOMER, in Hungarian, about a failure they
|
||||
// cannot act on. R-158's original proposal named `backup_failed`; D-c routes this to the
|
||||
// operator, and where the two disagree D-c wins.
|
||||
//
|
||||
// OPERATOR-ONLY IS ENFORCED BY `notify.operatorOnlyEvents` — see the paragraph above. This entry
|
||||
// alone does NOT make it operator-only.
|
||||
"recovery_unit_capture_failed": true,
|
||||
|
||||
// Controller-pushed events
|
||||
"controller_started": true,
|
||||
"claim_lockout": true, // v0.50.0 — claim/reset code brute-force lockout tripped
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-hub/internal/notify"
|
||||
)
|
||||
|
||||
// R-158 / R-167 (D-c) — the per-app Tier-1 recovery-unit capture failure alert.
|
||||
//
|
||||
// A new event type is a PAIR of registers, and each half fails differently:
|
||||
// - missing from allowedEventTypes → POST /event returns 400 and the event VANISHES (R-97a);
|
||||
// - missing from operatorOnlyEvents → it is delivered TO THE CUSTOMER, in raw English, about a
|
||||
// failure they can take no action on.
|
||||
//
|
||||
// The second is the quiet one — delivery "works", so nothing looks broken. It is also the defect
|
||||
// v0.78.0 actually shipped: adding a type to allowedEventTypes and ASSUMING that made it
|
||||
// operator-only. `FormatCustomerEmail` treats a missing customerMessages entry as a fallback to the
|
||||
// raw message, not a block, and the only customer gate is configuration. Both halves are pinned
|
||||
// here, in the same test, because fixing one and not the other is the realistic mistake.
|
||||
func TestRecoveryUnitCaptureFailedIsAllowlistedAndOperatorOnly(t *testing.T) {
|
||||
const et = "recovery_unit_capture_failed"
|
||||
|
||||
if !allowedEventTypes[et] {
|
||||
t.Fatalf("%s must be in allowedEventTypes, or POST /event 400s and a per-app Tier-1 backup "+
|
||||
"failure reaches no hub channel at all — which is the R-158 gap, un-fixed", et)
|
||||
}
|
||||
if !notify.IsOperatorOnly(et) {
|
||||
t.Fatalf("%s is allowlisted but NOT in notify.operatorOnlyEvents — the customer would be "+
|
||||
"emailed about a recovery-unit capture failure they can take no action on. Adding a type "+
|
||||
"to allowedEventTypes does NOT make it operator-only; that was the v0.78.0 defect "+
|
||||
"(corrected in v0.79.0/R-97c) and this is the same mistake one event later", et)
|
||||
}
|
||||
}
|
||||
|
||||
// The customer's half of D-c must NOT be operator-only — a fill warning is precisely the alert a
|
||||
// customer CAN act on (free space, delete files, add a drive). Pinned in the same file as its
|
||||
// operator sibling so the routing decision is read as one thing, which is what D-c is.
|
||||
func TestFillWarningReachesTheCustomer(t *testing.T) {
|
||||
for _, et := range []string{"disk_warning", "disk_critical"} {
|
||||
if !allowedEventTypes[et] {
|
||||
t.Fatalf("%s must stay in allowedEventTypes — it is the customer fill warning the "+
|
||||
"controller emits from v0.191.0 (R-167)", et)
|
||||
}
|
||||
if notify.IsOperatorOnly(et) {
|
||||
t.Fatalf("%s is in operatorOnlyEvents — the customer would never be warned that their "+
|
||||
"disk is filling, which is the whole customer half of decision D-c", et)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user