Skip to content

Commit 516050c

Browse files
Merge pull request #622 from DavidRajnoha/cypress-incidents-regressions-firing-alerts
OBSINTA-777: [Incidents] Regression tests for firing alerts in Incidents
2 parents d8a518a + 6b0b78b commit 516050c

File tree

2 files changed

+368
-26
lines changed

2 files changed

+368
-26
lines changed
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
/*
2+
Regression tests for time-based alert resolution issues with real firing alerts.
3+
4+
Section 3.3: Alerts Marked as Resolved After Time
5+
Tests that alerts maintain their firing state correctly when time passes without
6+
incident refresh. Previously, alerts were incorrectly marked as resolved when
7+
deselecting and reselecting an incident after waiting.
8+
9+
Section 4.7: Cached End Time for Prometheus Query
10+
Tests that the end time parameter in Prometheus queries uses current time instead
11+
of cached initial load time. Previously, the Redux state would cache the initial
12+
page load time, causing firing alerts to be incorrectly marked as resolved.
13+
14+
Both tests require continuously firing alerts and cannot be tested with mocked data.
15+
16+
Verifies: OU-XXX (time-based resolution bugs)
17+
*/
18+
19+
import { incidentsPage } from '../../../views/incidents-page';
20+
21+
const MCP = {
22+
namespace: Cypress.env('COO_NAMESPACE'),
23+
packageName: 'cluster-observability-operator',
24+
operatorName: 'Cluster Observability Operator',
25+
config: {
26+
kind: 'UIPlugin',
27+
name: 'monitoring',
28+
},
29+
};
30+
31+
const MP = {
32+
namespace: 'openshift-monitoring',
33+
operatorName: 'Cluster Monitoring Operator',
34+
};
35+
36+
describe('Regression: Time-Based Alert Resolution (E2E with Firing Alerts)', () => {
37+
let currentAlertName: string;
38+
39+
before(() => {
40+
cy.beforeBlockCOO(MCP, MP);
41+
42+
cy.log('Create or reuse firing alert for testing');
43+
cy.createKubePodCrashLoopingAlert('TimeBasedResolution2').then((alertName) => {
44+
currentAlertName = alertName;
45+
cy.log(`Test will monitor alert: ${currentAlertName}`);
46+
});
47+
});
48+
49+
beforeEach(() => {
50+
cy.transformMetrics();
51+
});
52+
53+
it('1. Section 3.3 - Alert not incorrectly marked as resolved after time passes', () => {
54+
cy.log('1.1 Navigate to Incidents page and clear filters');
55+
incidentsPage.goTo();
56+
incidentsPage.clearAllFilters();
57+
58+
const intervalMs = 60_000;
59+
const maxMinutes = 30;
60+
61+
cy.log('1.2 Wait for incident with custom alert to appear and get selected');
62+
cy.waitUntil(
63+
() => incidentsPage.findIncidentWithAlert(currentAlertName),
64+
{
65+
interval: intervalMs,
66+
timeout: maxMinutes * intervalMs,
67+
errorMsg: `Incident with alert ${currentAlertName} should appear within ${maxMinutes} minutes`
68+
}
69+
);
70+
71+
incidentsPage.elements.incidentsDetailsTable().should('exist');
72+
73+
cy.log('1.3 Verify alert is firing by checking end time shows "---"');
74+
cy.wrap(0).as('initialFiringCount');
75+
76+
incidentsPage.getSelectedIncidentAlerts().then((alerts) => {
77+
expect(alerts.length).to.be.greaterThan(0);
78+
79+
alerts.forEach((alert, index) => {
80+
alert.getAlertRuleCell().invoke('text').then((alertRuleText) => {
81+
const cleanAlertName = alertRuleText.trim().replace("AlertRuleAR", "");
82+
83+
if (cleanAlertName != currentAlertName) {
84+
cy.log(`Alert ${index + 1}: ${cleanAlertName} does not match ${currentAlertName}, skipping`);
85+
return;
86+
}
87+
88+
cy.log(`Alert ${index + 1}: Found matching alert ${cleanAlertName}`);
89+
90+
alert.getEndCell().invoke('text').then((endText) => {
91+
const cleanEndText = endText.trim();
92+
cy.log(`Alert ${index + 1} end time: "${cleanEndText}"`);
93+
const isFiring = cleanEndText === '---';
94+
if (isFiring) {
95+
cy.get('@initialFiringCount').then((count: any) => {
96+
cy.wrap(count + 1).as('initialFiringCount');
97+
});
98+
cy.log(`Alert ${index + 1} is FIRING`);
99+
} else {
100+
cy.log(`Alert ${index + 1} is resolved`);
101+
}
102+
});
103+
});
104+
});
105+
}).then(() => {
106+
cy.get('@initialFiringCount').then((count: any) => {
107+
cy.log(`Total firing alerts found: ${count}`);
108+
expect(count).to.be.greaterThan(0, `Expected at least 1 firing alert for ${currentAlertName}, but found ${count}`);
109+
});
110+
});
111+
112+
cy.log('Verified: Alert initially shows firing state (end time = "---")');
113+
114+
115+
const waitMinutes = 0.1
116+
cy.log(`1.6 Wait ${waitMinutes} minutes without refreshing the incidents page`);
117+
cy.wait(waitMinutes * 60_000);
118+
119+
cy.log('1.10 Verify alert is STILL firing (end time still shows "---", not resolved)');
120+
cy.wrap(0).as('currentFiringCount');
121+
122+
incidentsPage.getSelectedIncidentAlerts().then((alerts) => {
123+
expect(alerts.length).to.be.greaterThan(0);
124+
125+
alerts.forEach((alert, index) => {
126+
alert.getAlertRuleCell().invoke('text').then((alertRuleText) => {
127+
const cleanAlertName = alertRuleText.trim().replace("AlertRuleAR", "");
128+
129+
if (cleanAlertName != currentAlertName) {
130+
cy.log(`Alert ${index + 1}: ${cleanAlertName} does not match ${currentAlertName}, skipping`);
131+
return;
132+
}
133+
134+
cy.log(`Alert ${index + 1}: Found matching alert ${cleanAlertName}`);
135+
136+
alert.getEndCell().invoke('text').then((endText) => {
137+
const cleanEndText = endText.trim();
138+
cy.log(`Alert ${index + 1} end time: "${cleanEndText}"`);
139+
const isFiring = cleanEndText === '---';
140+
if (isFiring) {
141+
cy.get('@currentFiringCount').then((count: any) => {
142+
cy.wrap(count + 1).as('currentFiringCount');
143+
});
144+
cy.log(`Alert ${index + 1} is STILL FIRING`);
145+
} else {
146+
cy.log(`Alert ${index + 1} is now resolved (BUG!)`);
147+
}
148+
});
149+
});
150+
});
151+
}).then(() => {
152+
cy.get('@initialFiringCount').then((initialCount: any) => {
153+
cy.get('@currentFiringCount').then((currentCount: any) => {
154+
cy.log(`Initial firing alerts: ${initialCount}, Current firing alerts: ${currentCount}`);
155+
expect(currentCount).to.equal(initialCount, `Expected same number of firing alerts after wait (${initialCount}), but got ${currentCount}`);
156+
expect(currentCount).to.be.greaterThan(0, `Expected at least 1 firing alert, but found ${currentCount}`);
157+
});
158+
});
159+
});
160+
161+
cy.log('Verified: Alert maintains firing state after time passes and reselection (end time = "---")');
162+
});
163+
164+
it('2. Section 4.7 - Prometheus query end time updates to current time on filter refresh', () => {
165+
cy.log('2.1 Navigate to Incidents page and clear filters');
166+
incidentsPage.goTo();
167+
incidentsPage.clearAllFilters();
168+
169+
cy.log('2.2 Capture initial page load time');
170+
const initialLoadTime = Date.now();
171+
cy.wrap(initialLoadTime).as('initialLoadTime');
172+
173+
cy.log('2.3 Search for and select incident with custom alert');
174+
incidentsPage.findIncidentWithAlert(currentAlertName).should('eq', true);
175+
176+
cy.log('2.4 Verify alert is firing (end time = "---")');
177+
cy.wrap(0).as('firingCountTest2');
178+
179+
incidentsPage.getSelectedIncidentAlerts().then((alerts) => {
180+
expect(alerts.length).to.be.greaterThan(0);
181+
182+
alerts.forEach((alert, index) => {
183+
alert.getAlertRuleCell().invoke('text').then((alertRuleText) => {
184+
const cleanAlertName = alertRuleText.trim().replace("AlertRuleAR", "");
185+
186+
if (cleanAlertName != currentAlertName) {
187+
return;
188+
}
189+
190+
alert.getEndCell().invoke('text').then((endText) => {
191+
const cleanEndText = endText.trim();
192+
if (cleanEndText === '---') {
193+
cy.get('@firingCountTest2').then((count: any) => {
194+
cy.wrap(count + 1).as('firingCountTest2');
195+
});
196+
}
197+
});
198+
});
199+
});
200+
}).then(() => {
201+
cy.get('@firingCountTest2').then((count: any) => {
202+
expect(count).to.be.greaterThan(0, `Expected at least 1 firing alert for ${currentAlertName}`);
203+
});
204+
});
205+
206+
cy.log('Verified: Alert initially shows firing state');
207+
208+
const waitMinutes = 11;
209+
const REFRESH_FREQUENCY = 300;
210+
211+
cy.log(`2.5 Wait ${waitMinutes} minutes without refreshing incidents`);
212+
cy.wait(waitMinutes * 60_000);
213+
214+
cy.log('2.6 Set up intercept to capture Prometheus query parameters');
215+
const queryEndTimes: number[] = [];
216+
cy.intercept('GET', '**/api/prometheus/api/v1/query_range*', (req) => {
217+
req.continue((res) => {
218+
const queryParams = new URLSearchParams(req.url.split('?')[1]);
219+
const endTimeParam = queryParams.get('end');
220+
if (endTimeParam) {
221+
queryEndTimes.push(parseFloat(endTimeParam));
222+
}
223+
});
224+
}).as('prometheusQuery');
225+
226+
cy.log('2.7 Refresh the days filter to trigger new Prometheus queries');
227+
incidentsPage.setDays('7 days');
228+
229+
cy.log('2.8 Wait for all Prometheus queries to complete');
230+
cy.wait(2000);
231+
232+
cy.wrap(null).then(() => {
233+
cy.log(`Captured ${queryEndTimes.length} Prometheus queries`);
234+
235+
236+
if (queryEndTimes.length > 0) {
237+
const mostRecentEndTime = Math.max(...queryEndTimes);
238+
const oldestEndTime = Math.min(...queryEndTimes);
239+
const currentTime = Date.now() / 1000;
240+
const timeDifference = Math.abs(currentTime - mostRecentEndTime);
241+
242+
cy.log(`Query end times range: ${oldestEndTime} to ${mostRecentEndTime}`);
243+
cy.log(`Current time: ${currentTime}, Most recent query end time: ${mostRecentEndTime}, Difference: ${timeDifference}s`);
244+
245+
cy.get('@initialLoadTime').then((initialTime: any) => {
246+
const initialTimeSeconds = initialTime / 1000;
247+
const timePassedSinceLoad = currentTime - initialTimeSeconds;
248+
249+
cy.log(`Time passed since initial load: ${timePassedSinceLoad}s`);
250+
251+
expect(timeDifference).to.be.lessThan(REFRESH_FREQUENCY,
252+
`Most recent end time should be close to current time (within ${REFRESH_FREQUENCY} seconds)`);
253+
254+
expect(mostRecentEndTime).to.be.greaterThan(initialTimeSeconds + (waitMinutes * 60) - REFRESH_FREQUENCY,
255+
`End time should be updated to current time, not cached from initial load (${waitMinutes} minutes ago)`);
256+
});
257+
258+
cy.log('Verified: Most recent end time parameter uses current time, not cached initial load time');
259+
} else {
260+
throw new Error('No Prometheus queries were captured');
261+
}
262+
});
263+
});
264+
265+
it('3. Verify alert lifecycle - alert continues firing throughout test', () => {
266+
cy.log('3.1 Navigate to Incidents page');
267+
incidentsPage.goTo();
268+
incidentsPage.clearAllFilters();
269+
270+
cy.log('3.2 Search for and select incident with custom alert');
271+
incidentsPage.findIncidentWithAlert(currentAlertName).should('eq', true);
272+
273+
cy.log('3.3 Verify end time shows "---" for firing alert');
274+
cy.wrap(0).as('firingCountTest3');
275+
276+
incidentsPage.getSelectedIncidentAlerts().then((alerts) => {
277+
expect(alerts.length).to.be.greaterThan(0);
278+
279+
alerts.forEach((alert, index) => {
280+
alert.getAlertRuleCell().invoke('text').then((alertRuleText) => {
281+
const cleanAlertName = alertRuleText.trim().replace("AlertRuleAR", "");
282+
283+
if (cleanAlertName != currentAlertName) {
284+
return;
285+
}
286+
287+
alert.getEndCell().invoke('text').then((endText) => {
288+
const cleanEndText = endText.trim();
289+
if (cleanEndText === '---') {
290+
cy.get('@firingCountTest3').then((count: any) => {
291+
cy.wrap(count + 1).as('firingCountTest3');
292+
});
293+
}
294+
});
295+
});
296+
});
297+
}).then(() => {
298+
cy.get('@firingCountTest3').then((count: any) => {
299+
expect(count).to.be.greaterThan(0, `Expected at least 1 firing alert for ${currentAlertName}`);
300+
});
301+
});
302+
303+
cy.log('Verified: Alert lifecycle maintained correctly throughout test suite (end time = "---")');
304+
});
305+
});
306+
307+

0 commit comments

Comments
 (0)