Skip to content
Closed
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import type { CombinedSummarizedAlerts } from '@kbn/alerting-plugin/server/types';
import { buildAlertEvent } from './build_alert_event';
import type { AlertEventRule } from '../types/alert_types';

const mockRule: AlertEventRule = {
id: 'rule-1',
name: 'Test Rule',
tags: ['test'],
consumer: 'alerts',
producer: 'infrastructure',
ruleTypeId: 'metrics.alert.threshold',
};

const makeAlertGroup = (ids: string[]) => ({
count: ids.length,
data: ids.map((id) => ({ _id: id, _index: 'test-index' })),
alert_count: { active: ids.length, recovered: 0, ignored: 0 },
});

describe('buildAlertEvent', () => {
it('should include only new alerts when ongoing and recovered are empty', () => {
const alerts = {
new: makeAlertGroup(['alert-new-1', 'alert-new-2']),
ongoing: makeAlertGroup([]),
recovered: makeAlertGroup([]),
all: makeAlertGroup(['alert-new-1', 'alert-new-2']),
} as unknown as CombinedSummarizedAlerts;

const result = buildAlertEvent({ alerts, rule: mockRule, spaceId: 'default' });

expect(result.alerts).toHaveLength(2);
expect(result.alerts.map((a) => a._id)).toEqual(['alert-new-1', 'alert-new-2']);
});

it('should merge alerts from all three states', () => {
const alerts = {
new: makeAlertGroup(['alert-new-1']),
ongoing: makeAlertGroup(['alert-ongoing-1']),
recovered: makeAlertGroup(['alert-recovered-1']),
all: makeAlertGroup(['alert-new-1', 'alert-ongoing-1', 'alert-recovered-1']),
} as unknown as CombinedSummarizedAlerts;

const result = buildAlertEvent({ alerts, rule: mockRule, spaceId: 'default' });

expect(result.alerts).toHaveLength(3);
expect(result.alerts.map((a) => a._id)).toEqual([
'alert-new-1',
'alert-ongoing-1',
'alert-recovered-1',
]);
});

it('should handle undefined state data gracefully', () => {
const alerts = {
new: { count: 0, data: undefined },
ongoing: { count: 0, data: undefined },
recovered: { count: 0, data: undefined },
all: makeAlertGroup([]),
} as unknown as CombinedSummarizedAlerts;

const result = buildAlertEvent({ alerts, rule: mockRule, spaceId: 'default' });

expect(result.alerts).toEqual([]);
});

it('should populate rule information correctly', () => {
const alerts = {
new: makeAlertGroup(['alert-1']),
ongoing: makeAlertGroup([]),
recovered: makeAlertGroup([]),
all: makeAlertGroup(['alert-1']),
} as unknown as CombinedSummarizedAlerts;

const result = buildAlertEvent({
alerts,
rule: mockRule,
ruleUrl: 'https://kibana/rule/1',
spaceId: 'my-space',
});

expect(result.rule).toEqual(mockRule);
expect(result.ruleUrl).toBe('https://kibana/rule/1');
expect(result.spaceId).toBe('my-space');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ export function buildAlertEvent(params: {
spaceId: string;
}): AlertEvent {
return {
alerts: params.alerts.new.data,
alerts: [
...(params.alerts.new?.data || []),
...(params.alerts.ongoing?.data || []),
...(params.alerts.recovered?.data || []),
],
rule: {
id: params.rule.id,
name: params.rule.name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,18 @@ export type WorkflowsConfig = Record<string, unknown>;

export type WorkflowsSecrets = Record<string, unknown>;

export interface AlertStates {
[key: string]: boolean | undefined;
new?: boolean;
ongoing?: boolean;
recovered?: boolean;
}
Comment on lines +14 to +19

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The index signature ([key: string]: ...) makes AlertStates accept arbitrary keys, which weakens type-safety (e.g., typos like { recoevered: true } compile) and diverges from the server’s fixed-key shape. Consider removing the index signature and using explicit keys only (optionally paired with a type AlertStateId = 'new' | 'ongoing' | 'recovered' for the checkbox option IDs) to keep public/server contracts aligned and prevent silent misconfiguration.

Copilot uses AI. Check for mistakes.

export interface WorkflowsActionParams {
subAction: string;
subActionParams: {
workflowId: string;
summaryMode?: boolean;
alertStates?: AlertStates;
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,11 @@ describe('WorkflowsParamsFields', () => {
expect(mockEditAction).toHaveBeenCalledWith('subAction', 'run', 0);
expect(mockEditAction).toHaveBeenCalledWith(
'subActionParams',
{ workflowId: '', summaryMode: true },
{
workflowId: '',
summaryMode: true,
alertStates: { new: true, ongoing: false, recovered: false },
},
0
);
});
Expand Down Expand Up @@ -919,7 +923,11 @@ describe('WorkflowsParamsFields', () => {
await waitFor(() => {
expect(mockEditAction).toHaveBeenCalledWith(
'subActionParams',
{ workflowId: '', summaryMode: true },
{
workflowId: '',
summaryMode: true,
alertStates: { new: true, ongoing: false, recovered: false },
},
0
);
});
Expand Down Expand Up @@ -1094,4 +1102,135 @@ describe('WorkflowsParamsFields', () => {
);
});
});

describe('Alert states (alertStates parameter)', () => {
test('should render "Run workflow for" section with checkboxes', async () => {
await act(async () => {
renderWithIntl(<WorkflowsParamsFields {...defaultProps} />);
});

await waitFor(() => {
expect(screen.getByText('Run workflow for')).toBeInTheDocument();
expect(screen.getByText('Firing alerts')).toBeInTheDocument();
expect(screen.getByText('Ongoing alerts')).toBeInTheDocument();
expect(screen.getByText('Recovered alerts')).toBeInTheDocument();
});
});

test('should render checkboxes with correct defaults (only firing checked)', async () => {
const props = {
...defaultProps,
actionParams: {
subAction: 'run',
subActionParams: {
workflowId: 'test-workflow',
summaryMode: true,
alertStates: { new: true, ongoing: false, recovered: false },
},
} as WorkflowsActionParams,
};

await act(async () => {
renderWithIntl(<WorkflowsParamsFields {...props} />);
});

await waitFor(() => {
const checkboxes = screen.getByTestId('workflow-alert-states-checkboxes');
expect(checkboxes).toBeInTheDocument();

const firingCheckbox = screen.getByLabelText('Firing alerts');
const ongoingCheckbox = screen.getByLabelText('Ongoing alerts');
const recoveredCheckbox = screen.getByLabelText('Recovered alerts');

expect(firingCheckbox).toBeChecked();
expect(ongoingCheckbox).not.toBeChecked();
expect(recoveredCheckbox).not.toBeChecked();
});
});

test('should toggle recovered checkbox and call editAction', async () => {
const props = {
...defaultProps,
actionParams: {
subAction: 'run',
subActionParams: {
workflowId: 'test-workflow',
summaryMode: true,
alertStates: { new: true, ongoing: false, recovered: false },
},
} as WorkflowsActionParams,
};

await act(async () => {
renderWithIntl(<WorkflowsParamsFields {...props} />);
});

const recoveredCheckbox = screen.getByLabelText('Recovered alerts');

await act(async () => {
fireEvent.click(recoveredCheckbox);
});

await waitFor(() => {
expect(mockEditAction).toHaveBeenCalledWith(
'subActionParams',
expect.objectContaining({
alertStates: { new: true, ongoing: false, recovered: true },
}),
0
);
});
});

test('should initialize alertStates when missing from subActionParams', async () => {
const props = {
...defaultProps,
actionParams: {
subAction: 'run',
subActionParams: {
workflowId: 'test-workflow',
summaryMode: true,
},
} as WorkflowsActionParams,
};

await act(async () => {
renderWithIntl(<WorkflowsParamsFields {...props} />);
});

await waitFor(() => {
expect(mockEditAction).toHaveBeenCalledWith(
'subActionParams',
expect.objectContaining({
alertStates: { new: true, ongoing: false, recovered: false },
}),
0
);
});
});

test('should show all checkboxes as checked when all states are enabled', async () => {
const props = {
...defaultProps,
actionParams: {
subAction: 'run',
subActionParams: {
workflowId: 'test-workflow',
summaryMode: true,
alertStates: { new: true, ongoing: true, recovered: true },
},
} as WorkflowsActionParams,
};

await act(async () => {
renderWithIntl(<WorkflowsParamsFields {...props} />);
});

await waitFor(() => {
expect(screen.getByLabelText('Firing alerts')).toBeChecked();
expect(screen.getByLabelText('Ongoing alerts')).toBeChecked();
expect(screen.getByLabelText('Recovered alerts')).toBeChecked();
});
});
});
});
Loading
Loading