feat: add notification suppression rules (#1525)

* feat: add notification suppression rules

* fix: restore label routing and routing test mocks for suppression

* fix: allow bell mute shortcuts for history-only notification categories

Suppression rule validation used the routable category whitelist, which rejected history-only categories such as update_started that appear in the bell during stack updates.

* feat: expand Mute Rules UX with compose-first entry points and activity badges

* fix: add missing NodeContext mocks for notification suppression tests
This commit is contained in:
Anso
2026-07-02 15:26:48 -04:00
committed by GitHub
parent bc111d28f3
commit b65daf6845
52 changed files with 2794 additions and 51 deletions
@@ -32,6 +32,15 @@ function makeCtx(overrides: Partial<StackMenuCtx> = {}): StackMenuCtx {
createAndAssignLabel: vi.fn(),
openLabelManager: vi.fn(),
openScheduleTask: vi.fn(),
canMuteNotifications: false,
muteStackAll: vi.fn(),
muteStackDeploySuccess: vi.fn(),
muteStackMonitor: vi.fn(),
openStackMuteRules: vi.fn(),
muteLabelAll: vi.fn(),
muteLabelExternal: vi.fn(),
muteLabelLowPriority: vi.fn(),
openLabelMuteRules: vi.fn(),
...overrides,
};
}
@@ -111,6 +120,25 @@ describe('useStackMenuItems', () => {
expect(lifecycle?.items.some(i => i.id === 'schedule')).toBeFalsy();
});
it('includes Mute submenu in Inspect when canMuteNotifications', () => {
const { result } = renderHook(() => useStackMenuItems('web.yml', makeCtx({ canMuteNotifications: true })));
const inspect = result.current.find(g => g.id === 'inspect')!;
const muteItem = inspect.items.find(i => i.id === 'mute');
expect(muteItem).toBeDefined();
expect(muteItem?.subItems?.map(s => s.id)).toEqual([
'mute-stack-all',
'mute-stack-deploy',
'mute-stack-monitor',
'mute-stack-manage',
]);
});
it('hides Mute submenu when canMuteNotifications is false', () => {
const { result } = renderHook(() => useStackMenuItems('web.yml', makeCtx({ canMuteNotifications: false })));
const inspect = result.current.find(g => g.id === 'inspect')!;
expect(inspect.items.find(i => i.id === 'mute')).toBeUndefined();
});
it('keeps label assignment available for any tier', () => {
const { result } = renderHook(() => useStackMenuItems('web.yml', makeCtx({
labels: [{ id: 1, node_id: 0, name: 'prod', color: 'teal' }],