mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
149 lines
4.7 KiB
TypeScript
149 lines
4.7 KiB
TypeScript
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<Record<string, unknown>> = {}) {
|
|
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<unknown>) =>
|
|
fn({} as Client),
|
|
);
|
|
const historyRepository = {
|
|
create: vi.fn(async () => {}),
|
|
pruneOlderThan: vi.fn(async () => {}),
|
|
listRange: vi.fn(async () => []),
|
|
};
|
|
const manager = new ProxmoxPollingManager<TestHost>({
|
|
fetchHostById,
|
|
withSshConnection: withSshConnection as never,
|
|
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();
|
|
});
|
|
});
|