import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; vi.mock("../../src/backend/plugin-ctx.js", () => ({ pluginCtx: () => ({ log: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }, }), })); const collectProxmoxStats = vi.fn(); vi.mock("../../src/backend/proxmox/collect-proxmox-stats.js", () => ({ collectProxmoxStats: (...args: unknown[]) => collectProxmoxStats(...args), })); import { ProxmoxPollingManager, parseProxmoxStatsConfig, } from "../../src/backend/proxmox-stats-polling.js"; import type { Client } from "ssh2"; interface TestHost { id: number; userId: string; proxmoxStatsConfig?: string | null; } function snapshot(overrides: Partial> = {}) { return { node: { cpu: { percent: 10, cores: 4, load: [0.1, 0.2, 0.3] }, memory: { percent: 20, usedGiB: 2, totalGiB: 8 }, disk: { percent: 30, usedGiB: 30, totalGiB: 100 }, uptime: { seconds: 100, formatted: "0d 0h 1m" }, system: { hostname: "pve1", kernel: "6.8", pveVersion: "8.2" }, }, network: { interfaces: [{ name: "eth0", rxBytes: "10", txBytes: "20" }] }, guests: { guests: [], counts: { running: 0, stopped: 0, total: 0 } }, storage: { pools: [] }, cluster: { clustered: false }, lastChecked: new Date().toISOString(), ...overrides, }; } beforeEach(() => { vi.useFakeTimers(); collectProxmoxStats.mockReset(); }); afterEach(() => { vi.useRealTimers(); }); describe("parseProxmoxStatsConfig", () => { it("returns default poll interval when config is missing", () => { expect(parseProxmoxStatsConfig(null)).toEqual({ nodeName: null, pollInterval: 60, }); }); it("parses a JSON string config", () => { expect( parseProxmoxStatsConfig('{"nodeName":"pve1","pollInterval":30}'), ).toEqual({ nodeName: "pve1", pollInterval: 30 }); }); it("falls back to defaults on malformed JSON", () => { expect(parseProxmoxStatsConfig("{not json")).toEqual({ nodeName: null, pollInterval: 60, }); }); }); describe("ProxmoxPollingManager", () => { function makeManager(host: TestHost) { const fetchHostById = vi.fn(async () => host); const withSshConnection = vi.fn( async (_host: TestHost, fn: (client: Client) => Promise) => fn({} as Client), ); const historyRepository = { create: vi.fn(async () => {}), pruneOlderThan: vi.fn(async () => {}), listRange: vi.fn(async () => []), }; const manager = new ProxmoxPollingManager({ fetchHostById, withSshConnection, historyRepository, }); return { manager, fetchHostById, withSshConnection, historyRepository }; } it("starts polling and caches a snapshot when the first viewer registers", async () => { const host: TestHost = { id: 1, userId: "user-1" }; collectProxmoxStats.mockResolvedValue(snapshot()); const { manager, withSshConnection } = makeManager(host); manager.registerViewer(1, "viewer-1", "user-1"); // registerViewer kicks off polling via a fire-and-forget promise chain. await vi.waitFor(() => { expect(withSshConnection).toHaveBeenCalled(); }); const cached = manager.getStats(1); expect(cached?.data.node.cpu.percent).toBe(10); manager.destroy(); }); it("stops polling once the last viewer unregisters", async () => { const host: TestHost = { id: 2, userId: "user-1" }; collectProxmoxStats.mockResolvedValue(snapshot()); const { manager } = makeManager(host); manager.registerViewer(2, "viewer-a", "user-1"); manager.registerViewer(2, "viewer-b", "user-1"); await vi.waitFor(() => expect(manager.getStats(2)).toBeDefined()); manager.unregisterViewer(2, "viewer-a"); // one viewer left - stats stay cached expect(manager.getStats(2)).toBeDefined(); manager.unregisterViewer(2, "viewer-b"); // Cached snapshot is retained (not cleared) but the interval is stopped; // registering a new viewer must restart polling. manager.destroy(); }); it("updateHeartbeat returns false for an unknown session", () => { const { manager } = makeManager({ id: 3, userId: "user-1" }); expect(manager.updateHeartbeat("nope")).toBe(false); manager.destroy(); }); it("records an error snapshot when collection fails, without throwing", async () => { const host: TestHost = { id: 4, userId: "user-1" }; collectProxmoxStats.mockRejectedValue(new Error("pvesh not found")); const { manager } = makeManager(host); manager.registerViewer(4, "viewer-1", "user-1"); await vi.waitFor(() => { expect(manager.getError(4)?.error).toBe("pvesh not found"); }); expect(manager.getStats(4)).toBeUndefined(); manager.destroy(); }); });