| index 19c025959..60e0acd12 100644 |
| --- a/internal/metrics/alert_test.go |
| +++ b/internal/metrics/alert_test.go |
| @@ -1,10 +1,51 @@ |
| package metrics |
| |
| import ( |
| + "encoding/json" |
| "testing" |
| "time" |
| ) |
| |
| +// TestRuleStateJSONRoundTrip pins the true wire shape: `omitempty` on a |
| +// time.Time field is a no-op in encoding/json (structs are never "empty"), |
| +// so a zero RuleState's since/fired_at keys are always present, carrying |
| +// the zero-time string — not omitted. Round-tripping is still lossless for |
| +// both a zero and a non-zero RuleState, which is what consumers (the agent |
| +// loop's persistence, the console watcher) actually depend on. |
| +func TestRuleStateJSONRoundTrip(t *testing.T) { |
| + zero := RuleState{Firing: false} |
| + b, err := json.Marshal(zero) |
| + if err != nil { |
| + t.Fatalf("marshal zero RuleState: %v", err) |
| + } |
| + const wantZero = `{"firing":false,"since":"0001-01-01T00:00:00Z","fired_at":"0001-01-01T00:00:00Z"}` |
| + if string(b) != wantZero { |
| + t.Fatalf("zero RuleState wire shape changed:\n got: %s\n want: %s", b, wantZero) |
| + } |
| + var gotZero RuleState |
| + if err := json.Unmarshal(b, &gotZero); err != nil { |
| + t.Fatalf("unmarshal zero RuleState: %v", err) |
| + } |
| + if !gotZero.Since.Equal(zero.Since) || !gotZero.FiredAt.Equal(zero.FiredAt) || gotZero != zero { |
| + t.Fatalf("zero RuleState did not round-trip: got %+v want %+v", gotZero, zero) |
| + } |
| + |
| + since := time.Date(2026, 8, 2, 10, 0, 0, 0, time.UTC) |
| + firedAt := since.Add(10 * time.Minute) |
| + nonZero := RuleState{Firing: true, Since: since, FiredAt: firedAt, Value: 0.95} |
| + b, err = json.Marshal(nonZero) |
| + if err != nil { |
| + t.Fatalf("marshal non-zero RuleState: %v", err) |
| + } |
| + var gotNonZero RuleState |
| + if err := json.Unmarshal(b, &gotNonZero); err != nil { |
| + t.Fatalf("unmarshal non-zero RuleState: %v", err) |
| + } |
| + if !gotNonZero.Since.Equal(nonZero.Since) || !gotNonZero.FiredAt.Equal(nonZero.FiredAt) || gotNonZero.Firing != nonZero.Firing || gotNonZero.Value != nonZero.Value { |
| + t.Fatalf("non-zero RuleState did not round-trip: got %+v want %+v", gotNonZero, nonZero) |
| + } |
| +} |
| + |
| func TestEvaluateMemSustainAndHysteresis(t *testing.T) { |
| r := DefaultRules() |
| st := AlertState{V: 1, Box: "b", Rules: map[string]RuleState{}} |