feat: remove legacy alert rules, superseded by automations

Alert rules already auto-migrate into automations on boot and the old
engine stands down after. Deletes the rule/firing tables, routes, UI,
and AI tools; keeps notification channels since automations still use
them. Also fixes automations' notify step, which still called the
deleted repository.
This commit is contained in:
LukeGus committed 2026-09-22 10:45:12 -05:00
1 parent e0f14f2ecf
commit e372b0eda7
101 files changed
+29118 -5539

No files matched your search

-45
View File
@@ -1,45 +0,0 @@
import { describe, expect, it } from "vitest";
import { mapAlertFiring } from "../../api/alerts-api";
describe("mapAlertFiring", () => {
it("normalizes sqlite snake_case rows for the alerts UI", () => {
expect(
mapAlertFiring({
id: 9,
user_id: "u1",
rule_id: 4,
host_id: 12,
host_name: "db-01",
fired_at: "2026-06-30 12:00:00",
resolved_at: null,
value: 91,
message: "CPU threshold exceeded",
severity: "critical",
acknowledged: 0,
rule_name: "CPU",
}),
).toEqual({
id: 9,
userId: "u1",
ruleId: 4,
hostId: 12,
hostName: "db-01",
firedAt: "2026-06-30 12:00:00",
resolvedAt: null,
value: 91,
message: "CPU threshold exceeded",
severity: "critical",
acknowledged: false,
ruleName: "CPU",
});
});
it("uses safe defaults for malformed rows", () => {
const firing = mapAlertFiring({ acknowledged: 1, severity: "bad" });
expect(firing.id).toBe(0);
expect(firing.hostName).toBe("");
expect(firing.severity).toBe("warning");
expect(firing.acknowledged).toBe(true);
});
});
+3 -3
View File
@@ -16,8 +16,8 @@ describe("humanize", () => {
});
it("splits snake_case into words", () => {
expect(humanize("propose_create_alert_rule")).toBe(
"Propose create alert rule",
expect(humanize("propose_create_automation")).toBe(
"Propose create automation",
);
});
@@ -51,7 +51,7 @@ describe("fieldLabel", () => {
describe("toolLabel", () => {
it("phrases tools as actions rather than function names", () => {
expect(toolLabel("list_hosts")).toBe("Reading hosts");
expect(toolLabel("propose_create_alert_rule")).toBe("Create alert rule");
expect(toolLabel("propose_create_fleet")).toBe("Create fleet");
});
it("covers every tool in the catalog", () => {
-131
View File
@@ -1,131 +0,0 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import {
render,
screen,
waitFor,
cleanup,
fireEvent,
} from "@testing-library/react";
import type { NotificationChannel } from "@/api/alerts-api";
const alertsApi = vi.hoisted(() => ({
getAlertFirings: vi.fn(async () => []),
acknowledgeAlertFiring: vi.fn(async () => {}),
acknowledgeAllAlertFirings: vi.fn(async () => {}),
getAlertRules: vi.fn(async () => []),
getNotificationChannels: vi.fn(async () => [] as NotificationChannel[]),
deleteAlertRule: vi.fn(async () => {}),
deleteNotificationChannel: vi.fn(async () => {}),
testNotificationChannel: vi.fn(async () => {}),
}));
vi.mock("@/api/alerts-api", () => alertsApi);
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (_key: string, fallback?: string) => fallback ?? _key,
}),
}));
// A real (non-null) fake so tests can see what channels actually reach the
// rule dialog, instead of only asserting the API call count.
vi.mock("../../sidebar/AlertRuleDialog", () => ({
AlertRuleDialog: ({
open,
channels,
}: {
open: boolean;
channels: NotificationChannel[];
}) =>
open ? (
<div data-testid="rule-dialog-channels">
{channels.length === 0
? "no channels"
: channels.map((c) => c.name).join(",")}
</div>
) : null,
}));
vi.mock("../../sidebar/NotificationChannelDialog", () => ({
NotificationChannelDialog: () => null,
}));
import { AlertsPanel } from "../../sidebar/AlertsPanel";
function channel(
overrides: Partial<NotificationChannel> = {},
): NotificationChannel {
return {
id: 1,
userId: "user-1",
name: "ntfy",
type: "ntfy",
config: "{}",
enabled: true,
createdAt: new Date().toISOString(),
...overrides,
};
}
beforeEach(() => {
alertsApi.getAlertFirings.mockReset().mockResolvedValue([]);
alertsApi.getAlertRules.mockReset().mockResolvedValue([]);
alertsApi.getNotificationChannels.mockReset().mockResolvedValue([]);
});
afterEach(() => {
cleanup();
});
describe("AlertsPanel - notification channels", () => {
it("loads notification channels on mount, before the Channels tab is ever opened", async () => {
alertsApi.getNotificationChannels.mockResolvedValue([channel()]);
render(<AlertsPanel />);
// Regression: channels used to only load once the user switched to the
// "channels" tab, so editing a rule before ever visiting that tab showed
// an empty channel picker even though channels existed.
await waitFor(() => {
expect(alertsApi.getNotificationChannels).toHaveBeenCalledTimes(1);
});
});
it("populates the rule dialog's channel picker without ever visiting the Channels tab", async () => {
alertsApi.getNotificationChannels.mockResolvedValue([
channel({ id: 1, name: "ntfy-home" }),
channel({ id: 2, name: "webhook-slack" }),
]);
render(<AlertsPanel />);
await waitFor(() => {
expect(alertsApi.getNotificationChannels).toHaveBeenCalledTimes(1);
});
// Go straight to Rules and open the "new rule" dialog, skipping Channels
// entirely, this is exactly the sequence the original bug broke.
fireEvent.click(screen.getByText("Rules"));
fireEvent.click(screen.getByText("Add"));
expect(screen.getByTestId("rule-dialog-channels").textContent).toBe(
"ntfy-home,webhook-slack",
);
});
it("does not refetch channels again when switching to the Rules tab", async () => {
render(<AlertsPanel />);
await waitFor(() => {
expect(alertsApi.getNotificationChannels).toHaveBeenCalledTimes(1);
});
// Switching tabs only reloads rules here, not channels, since channels
// were already fetched on mount by the fix under test.
fireEvent.click(screen.getByText("Rules"));
await waitFor(() => {
expect(alertsApi.getAlertRules).toHaveBeenCalledTimes(1);
});
expect(alertsApi.getNotificationChannels).toHaveBeenCalledTimes(1);
});
});
@@ -29,10 +29,6 @@ vi.mock("react-i18next", () => ({
useTranslation: () => ({ t: (key: string) => key }),
}));
vi.mock("@/api/alerts-api", () => ({
getAlertFirings: vi.fn().mockResolvedValue([]),
}));
vi.mock("@/hooks/use-ai-availability", () => ({
useAiAvailability: () => ({ userEnabled: false }),
}));
-2
View File
@@ -92,7 +92,6 @@ describe("RAIL_ITEMS", () => {
"ai",
"history",
"session-logs",
"alerts",
]);
});
@@ -138,7 +137,6 @@ describe("RAIL_ITEMS", () => {
"ai",
"history",
"session-logs",
"alerts",
]);
});
});