feat(infra): commit live infra-audit/cluster work to reconcile git with prod
This work (network_hosts inventory + infra_audit MCP tool, /api/cluster + Sacred Valley cluster card, topbar cluster-health pill + SW self-heal) was built in an earlier session and DEPLOYED to CT 311 as alpha.24–26, but was never committed to git — prod was running code absent from the repo. Commits it as-is (already prod-validated) so git matches the live state, and restores its alpha.24/25/26 CHANGELOG entries. Files are disjoint from the fold-in work; both now ship together under alpha.27. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
63
tests/proxmox/cluster.test.js
Normal file
63
tests/proxmox/cluster.test.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { normalizeCluster, clusterHealth } from '../../lib/proxmox/cluster.js';
|
||||
|
||||
// Fixtures mirror the real PVE payload shapes from this cluster.
|
||||
const STATUS = [
|
||||
{ type: 'cluster', name: 'HZ-cluster', quorate: 1, nodes: 2 },
|
||||
{ type: 'node', name: 'z', online: 1, local: 1, ip: '192.168.1.124' },
|
||||
{ type: 'node', name: 'Z3', online: 1, local: 0, ip: '192.168.1.125' }
|
||||
];
|
||||
const HA = [
|
||||
{ type: 'quorum', id: 'quorum', quorate: 1, status: 'OK' },
|
||||
{ type: 'master', id: 'master', node: 'Z3', status: 'Z3 (active, ...)' },
|
||||
{ type: 'fencing', id: 'fencing', 'armed-state': 'armed' },
|
||||
{ type: 'lrm', id: 'lrm:z', node: 'z' },
|
||||
{ type: 'service', id: 'service:ct:104', sid: 'ct:104', state: 'started', node: 'z' },
|
||||
{ type: 'service', id: 'service:ct:111', sid: 'ct:111', state: 'error', node: 'z' }
|
||||
];
|
||||
|
||||
describe('normalizeCluster', () => {
|
||||
it('reports quorate, node online counts, master and HA service errors', () => {
|
||||
const r = normalizeCluster(STATUS, HA);
|
||||
expect(r.name).toBe('HZ-cluster');
|
||||
expect(r.quorate).toBe(true);
|
||||
expect(r.nodes_total).toBe(2);
|
||||
expect(r.nodes_online).toBe(2);
|
||||
expect(r.nodes.map(n => n.name).sort()).toEqual(['Z3', 'z']); // both nodes present
|
||||
expect(r.ha.quorum_ok).toBe(true);
|
||||
expect(r.ha.master).toBe('Z3');
|
||||
expect(r.ha.fencing).toBe('armed');
|
||||
expect(r.ha.services_total).toBe(2);
|
||||
expect(r.ha.services_error).toBe(1); // the ct:111 'error'
|
||||
});
|
||||
|
||||
it('flags loss of quorum and an offline node', () => {
|
||||
const r = normalizeCluster(
|
||||
[{ type: 'cluster', name: 'HZ-cluster', quorate: 0, nodes: 2 },
|
||||
{ type: 'node', name: 'z', online: 0 }, { type: 'node', name: 'Z3', online: 1 }],
|
||||
[{ type: 'quorum', quorate: 0, status: 'No quorum!' }]
|
||||
);
|
||||
expect(r.quorate).toBe(false);
|
||||
expect(r.nodes_online).toBe(1);
|
||||
expect(r.ha.quorum_ok).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clusterHealth', () => {
|
||||
it('returns proxmox_not_configured without a token', async () => {
|
||||
const r = await clusterHealth({ apiUrl: '', token: '' });
|
||||
expect(r.error).toBe('proxmox_not_configured');
|
||||
});
|
||||
|
||||
it('fetches + normalizes via injected fetch', async () => {
|
||||
const fetchImpl = async (url) => ({
|
||||
ok: true,
|
||||
json: async () => ({ data: url.includes('ha/status') ? HA : STATUS })
|
||||
});
|
||||
const r = await clusterHealth({ apiUrl: 'https://pve:8006', token: 'tok', fetchImpl });
|
||||
expect(r.quorate).toBe(true);
|
||||
expect(r.nodes_online).toBe(2);
|
||||
expect(r.ha.master).toBe('Z3');
|
||||
expect(typeof r.at).toBe('number');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user