diff --git a/backend/plugins/domain/message_gateway/push/template.go b/backend/plugins/domain/message_gateway/push/template.go index 7592409d..9e472bd3 100644 --- a/backend/plugins/domain/message_gateway/push/template.go +++ b/backend/plugins/domain/message_gateway/push/template.go @@ -6,6 +6,8 @@ package push import ( "encoding/json" "fmt" + "maps" + "slices" "strconv" "strings" ) @@ -87,13 +89,15 @@ func bodyTitle(body map[string]any) string { // bodyContent returns the notification body, rendering every entry with format // (a "%s … %v" pair) and joining them with sep when no content field is given. +// Entries render in sorted key order so identical bodies always produce +// identical text. func bodyContent(body map[string]any, format, sep string) string { if c, ok := body["content"].(string); ok && c != "" { return c } parts := make([]string, 0, len(body)) - for k, v := range body { - parts = append(parts, fmt.Sprintf(format, k, v)) + for _, k := range slices.Sorted(maps.Keys(body)) { + parts = append(parts, fmt.Sprintf(format, k, body[k])) } return strings.Join(parts, sep) } diff --git a/backend/plugins/domain/message_gateway/push/template_test.go b/backend/plugins/domain/message_gateway/push/template_test.go index 5ad3765d..5132212a 100644 --- a/backend/plugins/domain/message_gateway/push/template_test.go +++ b/backend/plugins/domain/message_gateway/push/template_test.go @@ -73,3 +73,24 @@ func TestParseTemplate(t *testing.T) { }) } } + +// Synthesized content must render in a stable order, otherwise two identical +// notifications produce different text on every send. +func TestBodyContentFallbackIsDeterministic(t *testing.T) { + body := map[string]any{ + "zebra": 1, + "alpha": 2, + "mike": 3, + "charlie": 4, + "yankee": 5, + } + + first := bodyContent(body, "%s=%v", ",") + for i := 1; i <= 50; i++ { + if got := bodyContent(body, "%s=%v", ","); got != first { + t.Fatalf("bodyContent order changed on call %d: %q != %q", i, got, first) + } + } + + assert.Equal(t, "alpha=2,charlie=4,mike=3,yankee=5,zebra=1", first) +}