Files
Void-Homelab/tests/proxmox/storage.test.js
root 1b960ec52b feat(sv): Storage · capacity card — ZFS pools, dropped pools, per-CT disk
Read-only Proxmox storage health (same PROXMOX_RO_TOKEN as the cluster card):
ZFS pool health+usage, dropped zfspool storages (the donatello/leonardo SATA
signal), and per-LXC rootfs fill, with a HEALTHY/WATCH/ATTENTION roll-up.
Closes the monitoring gap from the 2026-06-09 audit (C1 + H2 were invisible).
Pure normalizeStorage() unit-tested (4 tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 03:27:15 +10:00

71 lines
3.3 KiB
JavaScript

import { describe, it, expect } from 'vitest';
import { normalizeStorage, storageHealth } from '../../lib/proxmox/storage.js';
// Fixtures mirror real PVE payload shapes from this cluster.
const STORAGE = [
{ storage: 'localzfs', node: 'z', status: 'available', plugintype: 'zfspool', disk: 37e9, maxdisk: 516e9 },
{ storage: 'donatello-vm', node: 'z', status: 'unknown', plugintype: 'zfspool', disk: 0, maxdisk: 0 },
{ storage: 'leonardo-vm', node: 'z', status: 'unknown', plugintype: 'zfspool', disk: 0, maxdisk: 0 },
{ storage: 'local', node: 'z', status: 'available', plugintype: 'dir', disk: 1e9, maxdisk: 100e9 }
];
const VMS = [
{ vmid: 100, name: 'mediastack', type: 'lxc', node: 'z', disk: 60e9, maxdisk: 63e9, status: 'running' }, // 95%
{ vmid: 311, name: 'void-app', type: 'lxc', node: 'z', disk: 4e9, maxdisk: 16e9, status: 'running' }, // 25%
{ vmid: 200, name: 'OpenClaw', type: 'qemu', node: 'z', disk: 0, maxdisk: 32e9, status: 'running' } // skipped (qemu/0)
];
const ZFS = { z: [{ name: 'localzfs', health: 'ONLINE', alloc: 37e9, size: 516e9, frag: 6 }] };
describe('normalizeStorage', () => {
it('flags a dropped zfspool, a hot container, and rolls up worst=crit', () => {
const r = normalizeStorage(STORAGE, VMS, ZFS);
// dropped pools (donatello/leonardo) surface in `down`
expect(r.down.map(d => d.name).sort()).toEqual(['donatello-vm', 'leonardo-vm']);
expect(r.down.every(d => d.status === 'crit')).toBe(true);
// imported pool present + healthy
expect(r.pools).toHaveLength(1);
expect(r.pools[0].name).toBe('localzfs');
// guests: qemu/0 skipped, sorted desc, CT100 at 95% is crit
expect(r.guests.map(g => g.vmid)).toEqual([100, 311]);
expect(r.guests[0].pct).toBe(95);
expect(r.guests[0].status).toBe('crit');
expect(r.worst).toBe('crit');
expect(r.alerts.some(a => a.includes('donatello-vm'))).toBe(true);
expect(r.alerts.some(a => a.includes('CT 100'))).toBe(true);
});
it('all-healthy rolls up to ok', () => {
const r = normalizeStorage(
[{ storage: 'localzfs', node: 'z', status: 'available', plugintype: 'zfspool' }],
[{ vmid: 311, name: 'void-app', type: 'lxc', node: 'z', disk: 4e9, maxdisk: 16e9 }],
{ z: [{ name: 'localzfs', health: 'ONLINE', alloc: 37e9, size: 516e9 }] }
);
expect(r.worst).toBe('ok');
expect(r.down).toHaveLength(0);
expect(r.alerts).toHaveLength(0);
});
});
describe('storageHealth', () => {
it('returns proxmox_not_configured without a token', async () => {
const r = await storageHealth({ apiUrl: '', token: '' });
expect(r.error).toBe('proxmox_not_configured');
});
it('fetches + normalizes via injected fetch', async () => {
const fetchImpl = async (url) => ({
ok: true,
json: async () => {
if (url.includes('type=storage')) return { data: STORAGE };
if (url.includes('type=vm')) return { data: VMS };
if (url.includes('/nodes/z/disks/zfs')) return { data: ZFS.z };
if (url.endsWith('/nodes')) return { data: [{ node: 'z', status: 'online' }] };
return { data: [] };
}
});
const r = await storageHealth({ apiUrl: 'https://pve:8006', token: 'tok', fetchImpl });
expect(r.worst).toBe('crit');
expect(r.down).toHaveLength(2);
expect(typeof r.at).toBe('number');
});
});