R-585: the last customer-facing producers follow the household's language
backup_integrity_ok / backup_integrity_failed now take facts and push bundle keys (Hungarian bytes unchanged - go-parity, pinned verbatim by TestR585_IntegrityHungarianIsUnchanged). The interrupted-operation alert (backup_failed, customer-enabled by default) used to send the operator's ENGLISH sentence to every household; NotifyInterruptedOperation composes it per language, the English byte-identical to the operator's log line. The now-callerless NotifyBackupFailed is removed. local_api_endpoint_drift is operator-only (no customer toggle) and already English by design - not changed. 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:
@@ -135,7 +135,7 @@ func TestMainReportsTheInterruptedOperation(t *testing.T) {
|
||||
body := mainBody(t)
|
||||
names := callsInMain(t, body)
|
||||
|
||||
if indexOfCall(names, "NotifyBackupFailed") < 0 {
|
||||
if indexOfCall(names, "NotifyInterruptedOperation") < 0 {
|
||||
t.Fatal("func main() no longer reports an interrupted app-data operation to the operator — the " +
|
||||
"controller died mid-backup and nobody is told (§2.4)")
|
||||
}
|
||||
@@ -144,7 +144,7 @@ func TestMainReportsTheInterruptedOperation(t *testing.T) {
|
||||
//
|
||||
// R-174 STRENGTHENED THIS. `!= nil` alone is no longer sufficient, because Recover now returns a
|
||||
// non-nil result for a recovery that merely REFUSED starts (an absent data drive) — the drive
|
||||
// gate working as designed. `NotifyBackupFailed` sends `backup_failed`, which is customer-enabled
|
||||
// gate working as designed. `NotifyInterruptedOperation` (R-585; was `NotifyBackupFailed`) sends `backup_failed`, which is customer-enabled
|
||||
// by default (settings.DefaultEnabledEvents), so a nil-only guard would email the customer
|
||||
// "A biztonsági mentés sikertelen!" about an app nothing is wrong with. The guard must consult
|
||||
// Alarming().
|
||||
@@ -156,7 +156,7 @@ func TestMainReportsTheInterruptedOperation(t *testing.T) {
|
||||
}
|
||||
carries := false
|
||||
for _, name := range callsInMain(t, ifst.Body) {
|
||||
if name == "NotifyBackupFailed" {
|
||||
if name == "NotifyInterruptedOperation" {
|
||||
carries = true
|
||||
}
|
||||
}
|
||||
@@ -188,7 +188,7 @@ func TestMainReportsTheInterruptedOperation(t *testing.T) {
|
||||
if !guardedByAlarming {
|
||||
t.Fatal("the interrupted-operation alert is not guarded by appStopRecovery.Alarming() — a " +
|
||||
"recovery that only REFUSED starts (drive absent) would be reported through " +
|
||||
"NotifyBackupFailed, a customer-enabled event type, telling the customer their backup " +
|
||||
"NotifyInterruptedOperation (backup_failed), a customer-enabled event type, telling the customer their backup " +
|
||||
"failed when the drive gate was simply doing its job (R-174)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -695,7 +695,15 @@ func main() {
|
||||
// deliberate hold through it would email the customer "A biztonsági mentés sikertelen!" about an
|
||||
// app nothing is wrong with. The refusal is already logged at WARN with its reason.
|
||||
if appStopRecovery != nil && appStopRecovery.Alarming() {
|
||||
notifier.NotifyBackupFailed(appStopRecovery.Message(), appStopRecovery.Detail())
|
||||
// R-585: facts, not the operator's English sentence — `backup_failed` is customer-enabled by
|
||||
// default, so this line reaches the household and must follow its language.
|
||||
notifier.NotifyInterruptedOperation(notify.InterruptedOperation{
|
||||
ReasonKey: appStopRecovery.Reason.MessageKey(),
|
||||
Reason: string(appStopRecovery.Reason),
|
||||
Restarted: len(appStopRecovery.Restarted),
|
||||
Failed: len(appStopRecovery.Failed),
|
||||
Refused: len(appStopRecovery.Refused),
|
||||
}, appStopRecovery.Detail())
|
||||
} else if appStopRecovery != nil {
|
||||
logger.Printf("[WARN] [appstop] %s — not alarming: %s", appStopRecovery.Message(), appStopRecovery.Detail())
|
||||
}
|
||||
@@ -3773,7 +3781,7 @@ func runOffsiteIntegrityCheck(ctx context.Context, mgr *backup.Manager, n *notif
|
||||
// design (08 §6.1). A weekly success e-mail is how people stop reading their alerts. It is
|
||||
// still pushed, because the hub stores it and the event stream is where "was it checked?" is
|
||||
// answered.
|
||||
n.NotifyIntegrityOK(integrityOKMsg(res))
|
||||
n.NotifyIntegrityOK(res.Duration.Round(time.Second).String(), res.ReadDataSubset) // R-585: facts; the bundle composes
|
||||
return res
|
||||
default:
|
||||
mgr.RecordIntegrityVerdict(res)
|
||||
@@ -3783,22 +3791,11 @@ func runOffsiteIntegrityCheck(ctx context.Context, mgr *backup.Manager, n *notif
|
||||
// The customer gets a SENTENCE. restic's own words go to the log, truncated, where the operator
|
||||
// can diagnose without a rebuild. R-379 is the reason that split exists: 615 bytes of raw
|
||||
// database text reached a customer once.
|
||||
n.NotifyIntegrityFailed(integrityFailedMsg, "restic check reported repository errors")
|
||||
n.NotifyIntegrityFailed("restic check reported repository errors") // R-585: the sentence is event.backup_integrity_failed
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
// R-359 customer-facing strings. Named constants because tests assert them verbatim and because a
|
||||
// silent edit is how an honest message drifts into a comforting one.
|
||||
const (
|
||||
// integrityFailedMsg names what to do and what NOT to do. "Ne törölj semmit" is load-bearing: a
|
||||
// customer who believes their backups are broken may try to "start fresh", which destroys the one
|
||||
// copy that might still be partly recoverable.
|
||||
integrityFailedMsg = "A távoli mentés ellenőrzése hibát talált a tárolóban. A mentések egy része sérült lehet. Ne törölj semmit, és vedd fel velünk a kapcsolatot."
|
||||
|
||||
integrityOKBase = "A távoli mentés ellenőrzése rendben lezajlott."
|
||||
)
|
||||
|
||||
// ── R-87 — the nightly off-site PROOF's one caller ─────────────────────────────────────────────
|
||||
//
|
||||
// ONE function, like runOffsiteIntegrityCheck above, so a future debug button cannot drift from the
|
||||
@@ -3859,17 +3856,6 @@ func proofEmptyDetail(res backup.ProofResult) string {
|
||||
return d
|
||||
}
|
||||
|
||||
// integrityOKMsg states what was actually checked, so a structure-only pass is never read as a
|
||||
// full data verification. The depth is a fact the customer's sentence has to carry: "checked" means
|
||||
// two different things depending on it.
|
||||
func integrityOKMsg(res backup.IntegrityResult) string {
|
||||
msg := integrityOKBase + " (" + res.Duration.Round(time.Second).String()
|
||||
if res.ReadDataSubset != "" {
|
||||
msg += ", a mentett adatok " + res.ReadDataSubset + "-át újraolvasva"
|
||||
}
|
||||
return msg + ")"
|
||||
}
|
||||
|
||||
// updateGuardsAdapter implements stacks.UpdateGuards over the backup manager (slice 4). The stacks
|
||||
// package cannot import backup, so this is the one place the two meet. Nil-safe on b: a box with
|
||||
// backup disabled has no restore point, and the update is refused for that true reason.
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"testing"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/i18n"
|
||||
)
|
||||
|
||||
// R-359 — the scheduled job must be PROVEN WIRED.
|
||||
@@ -87,16 +89,33 @@ func TestR359_DebugCallbackIsWired(t *testing.T) {
|
||||
|
||||
func TestR359_OutcomeMessagesCarryNoMachineDetail(t *testing.T) {
|
||||
// The customer gets a sentence; restic's words go to the log. R-379: 615 bytes of raw database
|
||||
// text reached a customer once.
|
||||
for _, bad := range []string{"sftp:", "restic", "exit status", "@", "/srv/"} {
|
||||
if contains(integrityFailedMsg, bad) {
|
||||
t.Errorf("the failure sentence carries machine detail %q: %q", bad, integrityFailedMsg)
|
||||
}
|
||||
// text reached a customer once. R-585: the sentence is a bundle key now, so BOTH languages are
|
||||
// held to it — an English household must be told what not to do just as plainly.
|
||||
b, err := i18n.Shared()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// It must still tell them what to do — and what NOT to do.
|
||||
for _, want := range []string{"Ne törölj semmit", "vedd fel velünk a kapcsolatot"} {
|
||||
if !contains(integrityFailedMsg, want) {
|
||||
t.Errorf("the failure sentence is a dead end; missing %q", want)
|
||||
for _, tc := range []struct {
|
||||
lang string
|
||||
wants []string
|
||||
}{
|
||||
{"hu", []string{"Ne törölj semmit", "vedd fel velünk a kapcsolatot"}},
|
||||
{"en", []string{"Do not delete anything", "contact us"}},
|
||||
} {
|
||||
msg, fellBack, ok := b.Text(tc.lang, "event.backup_integrity_failed")
|
||||
if !ok || fellBack {
|
||||
t.Fatalf("%s: event.backup_integrity_failed is missing (ok=%v fellBack=%v)", tc.lang, ok, fellBack)
|
||||
}
|
||||
for _, bad := range []string{"sftp:", "restic", "exit status", "@", "/srv/"} {
|
||||
if contains(msg, bad) {
|
||||
t.Errorf("%s: the failure sentence carries machine detail %q: %q", tc.lang, bad, msg)
|
||||
}
|
||||
}
|
||||
// It must still tell them what to do — and what NOT to do.
|
||||
for _, want := range tc.wants {
|
||||
if !contains(msg, want) {
|
||||
t.Errorf("%s: the failure sentence is a dead end; missing %q", tc.lang, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user