import { describe, expect, it } from "vitest"; import { compare, extractMetricValue, hasDwelled, isCoolingDown, metricStateKey, severityForValue, type MetricsSnapshot, } from "../../automations/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"); }); });