mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 22:25:24 +00:00
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:
1 parent
e0f14f2ecf
commit
e372b0eda7
101 files changed
+29118
-5539
No files matched your search
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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 }),
|
||||
}));
|
||||
|
||||
@@ -92,7 +92,6 @@ describe("RAIL_ITEMS", () => {
|
||||
"ai",
|
||||
"history",
|
||||
"session-logs",
|
||||
"alerts",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -138,7 +137,6 @@ describe("RAIL_ITEMS", () => {
|
||||
"ai",
|
||||
"history",
|
||||
"session-logs",
|
||||
"alerts",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user