mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 22:25:24 +00:00
193 lines
6.1 KiB
TypeScript
193 lines
6.1 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
compare,
|
|
extractMetricValue,
|
|
hasDwelled,
|
|
isCoolingDown,
|
|
metricStateKey,
|
|
severityForValue,
|
|
type MetricsSnapshot,
|
|
} from "../../src/backend/conditions.js";
|
|
|
|
const metrics: MetricsSnapshot = {
|
|
cpu: { percent: 42.5, load: [1.5, 1.2, 0.9] },
|
|
memory: { percent: 61, usedGiB: 7.5 },
|
|
disk: {
|
|
percent: 30,
|
|
filesystems: [
|
|
{ mount: "/", percent: 30, availableBytes: 100 },
|
|
{ mount: "/data", percent: 93.4, availableBytes: 25 },
|
|
],
|
|
},
|
|
network: {
|
|
interfaces: [
|
|
{ name: "eth0", rxBytes: "1000", txBytes: "2000" },
|
|
{ name: "eth1", rxBytes: "50", txBytes: "60" },
|
|
],
|
|
},
|
|
temperature: { highestCelsius: 71 },
|
|
uptime: { seconds: 86400 },
|
|
processes: { total: 210 },
|
|
};
|
|
|
|
describe("extractMetricValue", () => {
|
|
it("reads simple scalar paths", () => {
|
|
expect(extractMetricValue(metrics, { path: "cpu.percent" })).toBe(42.5);
|
|
expect(extractMetricValue(metrics, { path: "memory.percent" })).toBe(61);
|
|
expect(
|
|
extractMetricValue(metrics, { path: "temperature.highestCelsius" }),
|
|
).toBe(71);
|
|
expect(extractMetricValue(metrics, { path: "uptime.seconds" })).toBe(86400);
|
|
expect(extractMetricValue(metrics, { path: "processes.total" })).toBe(210);
|
|
});
|
|
|
|
it("reads load averages positionally", () => {
|
|
expect(extractMetricValue(metrics, { path: "cpu.load1" })).toBe(1.5);
|
|
expect(extractMetricValue(metrics, { path: "cpu.load15" })).toBe(0.9);
|
|
});
|
|
|
|
it("reads a specific mount rather than the aggregate", () => {
|
|
// The motivating case: /data is nearly full while / is fine.
|
|
expect(
|
|
extractMetricValue(metrics, { path: "disk.percent", mount: "/data" }),
|
|
).toBe(93.4);
|
|
expect(extractMetricValue(metrics, { path: "disk.percent" })).toBe(30);
|
|
});
|
|
|
|
it("returns null for a mount that is not present", () => {
|
|
expect(
|
|
extractMetricValue(metrics, { path: "disk.percent", mount: "/nope" }),
|
|
).toBeNull();
|
|
});
|
|
|
|
it("selects a named interface and coerces string counters", () => {
|
|
expect(
|
|
extractMetricValue(metrics, { path: "network.rxBytes", iface: "eth1" }),
|
|
).toBe(50);
|
|
expect(extractMetricValue(metrics, { path: "network.rxBytes" })).toBe(1000);
|
|
});
|
|
|
|
it("extracts per-interface network rates for bandwidth alerts", () => {
|
|
const rateMetrics = {
|
|
network: {
|
|
interfaces: [
|
|
{ name: "eth0", rxRateBps: 1024, txRateBps: 2048 },
|
|
{ name: "eth1", rxRateBps: 4096, txRateBps: 8192 },
|
|
],
|
|
},
|
|
};
|
|
|
|
expect(
|
|
extractMetricValue(rateMetrics, {
|
|
path: "network.rxRateBps",
|
|
iface: "eth1",
|
|
}),
|
|
).toBe(4096);
|
|
expect(
|
|
extractMetricValue(rateMetrics, {
|
|
path: "network.txRateBps",
|
|
iface: "eth0",
|
|
}),
|
|
).toBe(2048);
|
|
});
|
|
|
|
it("returns null for missing metrics rather than throwing", () => {
|
|
expect(extractMetricValue(null, { path: "cpu.percent" })).toBeNull();
|
|
expect(extractMetricValue({}, { path: "cpu.percent" })).toBeNull();
|
|
expect(
|
|
extractMetricValue({ cpu: { percent: null } }, { path: "cpu.percent" }),
|
|
).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("metricStateKey", () => {
|
|
it("scopes state per mount so dwell tracks one filesystem", () => {
|
|
expect(metricStateKey(7, { path: "disk.percent" })).toBe("7");
|
|
expect(metricStateKey(7, { path: "disk.percent", mount: "/data" })).toBe(
|
|
"7:/data",
|
|
);
|
|
expect(metricStateKey(7, { path: "network.rxBytes", iface: "eth1" })).toBe(
|
|
"7:eth1",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("compare", () => {
|
|
it("handles numeric operators", () => {
|
|
expect(compare(93, ">", 90)).toBe(true);
|
|
expect(compare(90, ">", 90)).toBe(false);
|
|
expect(compare(90, ">=", 90)).toBe(true);
|
|
expect(compare(10, "<", 90)).toBe(true);
|
|
expect(compare(90, "<=", 90)).toBe(true);
|
|
});
|
|
|
|
it("compares numeric strings numerically", () => {
|
|
expect(compare("93", ">", "90")).toBe(true);
|
|
// Lexically "9" > "10", so this would be wrong as a string compare.
|
|
expect(compare("9", "<", "10")).toBe(true);
|
|
});
|
|
|
|
it("falls back to string equality for non-numeric values", () => {
|
|
expect(compare("running", "==", "running")).toBe(true);
|
|
expect(compare("running", "!=", "exited")).toBe(true);
|
|
});
|
|
|
|
it("handles containment", () => {
|
|
expect(compare("disk full", "contains", "full")).toBe(true);
|
|
expect(compare("disk full", "not_contains", "full")).toBe(false);
|
|
expect(compare("all good", "not_contains", "error")).toBe(true);
|
|
});
|
|
|
|
it("treats changed as inequality of the rendered values", () => {
|
|
expect(compare("online", "changed", "offline")).toBe(true);
|
|
expect(compare("online", "changed", "online")).toBe(false);
|
|
});
|
|
|
|
it("is false when a numeric comparison has a non-numeric side", () => {
|
|
expect(compare("abc", ">", 5)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("isCoolingDown", () => {
|
|
const now = Date.parse("2026-01-01T12:00:00.000Z");
|
|
|
|
it("is false when nothing has fired yet", () => {
|
|
expect(isCoolingDown(null, 15, now)).toBe(false);
|
|
});
|
|
|
|
it("is true inside the window and false outside it", () => {
|
|
expect(isCoolingDown("2026-01-01T11:50:00.000Z", 15, now)).toBe(true);
|
|
expect(isCoolingDown("2026-01-01T11:40:00.000Z", 15, now)).toBe(false);
|
|
});
|
|
|
|
it("treats a zero cooldown as always ready", () => {
|
|
expect(isCoolingDown("2026-01-01T11:59:59.000Z", 0, now)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("hasDwelled", () => {
|
|
const now = Date.parse("2026-01-01T12:00:00.000Z");
|
|
|
|
it("fires immediately when no window is configured", () => {
|
|
expect(hasDwelled(null, undefined, now)).toBe(true);
|
|
expect(hasDwelled(null, 0, now)).toBe(true);
|
|
});
|
|
|
|
it("requires the window to have elapsed", () => {
|
|
expect(hasDwelled("2026-01-01T11:49:00.000Z", 600, now)).toBe(true);
|
|
expect(hasDwelled("2026-01-01T11:55:00.000Z", 600, now)).toBe(false);
|
|
});
|
|
|
|
it("is false when a window is set but no breach is open", () => {
|
|
expect(hasDwelled(null, 600, now)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("severityForValue", () => {
|
|
it("escalates at 95 and honours an explicit override", () => {
|
|
expect(severityForValue(96)).toBe("critical");
|
|
expect(severityForValue(90)).toBe("warning");
|
|
expect(severityForValue(96, "info")).toBe("info");
|
|
});
|
|
});
|