@@ -214,6 +214,98 @@ func TestProcess_DuringRunStillFiresAlongsideEndOfRun(t *testing.T) {
214214 }
215215}
216216
217+ // TestProcessEndOfRun_OverRun_AggregatesAcrossWholeRun verifies that a rule
218+ // with `condition: avg(value) over run > X` and `mode: end-of-run` aggregates
219+ // across the entire run, regardless of how long the run lasted, and fires
220+ // correctly at run exit. Distinguishes "over run" from "over Nm" by spanning
221+ // timestamps wider than any plausible wall-clock window.
222+ func TestProcessEndOfRun_OverRun_AggregatesAcrossWholeRun (t * testing.T ) {
223+ rules := []EngineRule {
224+ {
225+ Name : "whole_run_avg" ,
226+ Match : map [string ]string {"metric" : "mem" },
227+ Condition : "avg(value) over run > 50" ,
228+ Message : "avg mem was {{ .avg }}" ,
229+ Alerts : []string {"stdout" },
230+ Mode : "end-of-run" ,
231+ },
232+ }
233+ eng , err := NewEngine (rules , 1000 )
234+ if err != nil {
235+ t .Fatalf ("NewEngine: %v" , err )
236+ }
237+
238+ // Feed events spanning 2 hours — far beyond any wall-clock window.
239+ t0 := time .Now ()
240+ eng .Process (ingester.Event {Metric : "mem" , Value : 40 , At : t0 }, t0 )
241+ eng .Process (ingester.Event {Metric : "mem" , Value : 60 , At : t0 .Add (1 * time .Hour )}, t0 .Add (1 * time .Hour ))
242+ eng .Process (ingester.Event {Metric : "mem" , Value : 80 , At : t0 .Add (2 * time .Hour )}, t0 .Add (2 * time .Hour ))
243+
244+ // At end-of-run the buffer should still hold all three entries:
245+ // avg(40, 60, 80) = 60, which is > 50.
246+ alerts := eng .ProcessEndOfRun (t0 .Add (2 * time .Hour ))
247+ if len (alerts ) != 1 {
248+ t .Fatalf ("expected 1 end-of-run alert, got %d" , len (alerts ))
249+ }
250+ if alerts [0 ].Avg != 60 {
251+ t .Errorf ("alert.Avg = %v, want 60 (avg of full run)" , alerts [0 ].Avg )
252+ }
253+ if alerts [0 ].Count != 3 {
254+ t .Errorf ("alert.Count = %v, want 3 (all events in run)" , alerts [0 ].Count )
255+ }
256+ }
257+
258+ // TestProcess_OverRun_FiresMidRunWithCooldown verifies the orthogonal
259+ // composition: `over run` without `mode: end-of-run` fires mid-run when
260+ // the threshold crosses, and cooldown prevents repeated firing.
261+ func TestProcess_OverRun_FiresMidRunWithCooldown (t * testing.T ) {
262+ rules := []EngineRule {
263+ {
264+ Name : "errors_pile_up" ,
265+ Match : map [string ]string {"metric" : "errors" },
266+ Condition : "count(value) over run > 2" ,
267+ Cooldown : 10 * time .Minute ,
268+ Message : "errors: {{ .count }}" ,
269+ Alerts : []string {"stdout" },
270+ },
271+ }
272+ eng , err := NewEngine (rules , 1000 )
273+ if err != nil {
274+ t .Fatalf ("NewEngine: %v" , err )
275+ }
276+
277+ t0 := time .Now ()
278+ // Events 1, 2 — count is at most 2, condition (count > 2) false.
279+ for i := 0 ; i < 2 ; i ++ {
280+ alerts := eng .Process (ingester.Event {
281+ Metric : "errors" , Value : 1 ,
282+ At : t0 .Add (time .Duration (i ) * time .Second ),
283+ }, t0 .Add (time .Duration (i )* time .Second ))
284+ if len (alerts ) > 0 {
285+ t .Fatalf ("event %d: rule fired prematurely (count=%v)" , i , alerts [0 ].Count )
286+ }
287+ }
288+ // Event 3 — count crosses to 3, condition true, alert fires.
289+ alerts := eng .Process (ingester.Event {
290+ Metric : "errors" , Value : 1 ,
291+ At : t0 .Add (2 * time .Second ),
292+ }, t0 .Add (2 * time .Second ))
293+ if len (alerts ) != 1 {
294+ t .Fatalf ("expected 1 alert at event 3, got %d" , len (alerts ))
295+ }
296+ if alerts [0 ].Count != 3 {
297+ t .Errorf ("alert.Count = %v, want 3" , alerts [0 ].Count )
298+ }
299+ // Event 4 — count is 4, condition true, but cooldown blocks the alert.
300+ alerts = eng .Process (ingester.Event {
301+ Metric : "errors" , Value : 1 ,
302+ At : t0 .Add (3 * time .Second ),
303+ }, t0 .Add (3 * time .Second ))
304+ if len (alerts ) != 0 {
305+ t .Errorf ("expected cooldown to block alert at event 4, got %d" , len (alerts ))
306+ }
307+ }
308+
217309// TestParseLabelKey verifies that the labelKey reverser handles the formats
218310// produced by LabelSetKey.
219311func TestParseLabelKey (t * testing.T ) {
0 commit comments