homeio
PublicForked from doctor-io/homeio
metrics-history.test.ts
import { describe, expect, it, vi } from "vitest";
vi.mock("@/lib/server/modules/system/service", () => ({
getSystemMetricsSnapshot: vi.fn(),
}));
import {
HISTORY_SAMPLE_MS,
createMetricsHistory,
toHistoryPoint,
} from "@/lib/server/modules/system/metrics-history";
import type { MetricsHistoryPoint, SystemMetricsSnapshot } from "@/lib/shared/contracts/system";
const MINUTE = 60_000;
const START = Date.UTC(2026, 8, 27, 10, 0, 0);
function point(t: number, cpu: number | null, temperature: number | null = null): MetricsHistoryPoint {
return {
t,
cpuPercent: cpu,
memoryPercent: 50,
temperatureCelsius: temperature,
downloadMbps: 1,
uploadMbps: 0.5,
diskReadMBps: 2,
diskWriteMBps: 1,
};
}
describe("metrics history", () => {
it("keeps one hour of 5 s samples and drops older ones", () => {
const history = createMetricsHistory();
const samples = (2 * 60 * 60 * 1000) / HISTORY_SAMPLE_MS;
for (let i = 0; i < samples; i += 1) history.record(point(START + i * HISTORY_SAMPLE_MS, i % 100));
const now = START + (samples - 1) * HISTORY_SAMPLE_MS;
const hour = history.get("1h", now);
expect(hour.intervalSeconds).toBe(5);
expect(hour.points).toHaveLength(720);
expect(hour.points[0].t).toBeGreaterThan(now - 60 * MINUTE);
expect(hour.points.at(-1)?.t).toBe(now);
});
it("serves the last 15 minutes of the hour at the same 5 s resolution", () => {
const history = createMetricsHistory();
for (let i = 0; i < 720; i += 1) history.record(point(START + i * HISTORY_SAMPLE_MS, 1));
const now = START + 719 * HISTORY_SAMPLE_MS;
const quarter = history.get("15m", now);
expect(quarter.intervalSeconds).toBe(5);
expect(quarter.points).toHaveLength(180);
expect(quarter.points[0].t).toBeGreaterThan(now - 15 * MINUTE);
});
it("averages each minute for the 24 h view and includes the minute in progress", () => {
const history = createMetricsHistory();
// First minute: 10, 20, 30. Second minute (in progress): 70.
history.record(point(START, 10));
history.record(point(START + 5_000, 20));
history.record(point(START + 10_000, 30));
history.record(point(START + MINUTE, 70));
const day = history.get("24h", START + MINUTE + 1_000);
expect(day.intervalSeconds).toBe(60);
expect(day.points.map((p) => [p.t, p.cpuPercent])).toEqual([
[START, 20],
[START + MINUTE, 70],
]);
});
it("averages only the readings a host has, and keeps null when it has none", () => {
const history = createMetricsHistory();
history.record(point(START, null, 40));
history.record(point(START + 5_000, 30, null));
history.record(point(START + 10_000, 60, 50));
const [minute] = history.get("24h", START + 20_000).points;
expect(minute.cpuPercent).toBe(45);
expect(minute.temperatureCelsius).toBe(45);
const noSensor = createMetricsHistory();
noSensor.record(point(START, 10, null));
expect(noSensor.get("24h", START + 1_000).points[0].temperatureCelsius).toBeNull();
});
it("forgets minutes older than 24 hours", () => {
const history = createMetricsHistory();
for (let minute = 0; minute < 25 * 60; minute += 1) history.record(point(START + minute * MINUTE, 1));
const now = START + (25 * 60 - 1) * MINUTE;
const points = history.get("24h", now).points;
expect(points.length).toBeLessThanOrEqual(24 * 60);
expect(points[0].t).toBeGreaterThan(now - 24 * 60 * MINUTE);
});
it("maps a metrics snapshot and turns missing or invalid readings into null", () => {
const snapshot = {
cpu: { normalizedPercent: 12.5 },
memory: { usedPercent: 40 },
temperature: { mainCelsius: null },
wifi: { downloadMbps: Number.NaN, uploadMbps: 2 },
diskIo: { readMBps: 12, writeMBps: null },
} as unknown as SystemMetricsSnapshot;
expect(toHistoryPoint(snapshot, START)).toEqual({
t: START,
cpuPercent: 12.5,
memoryPercent: 40,
temperatureCelsius: null,
downloadMbps: null,
uploadMbps: 2,
diskReadMBps: 12,
diskWriteMBps: null,
});
});
});