platform-health.test.ts108 lines · main
1/**
2 * Unit tests for the pure host-metrics logic in platform-health.ts —
3 * the Prometheus instant-query parser. This is the load-bearing parse
4 * that turns a node-exporter scrape into a number (or null), so it's
5 * pinned in isolation against real-shaped Prometheus payloads. The
6 * network + env-guard paths are exercised by the live deploy; this file
7 * proves the HARD honesty rule: malformed/empty → null, never a fake 0.
8 */
9
10import { describe, expect, test } from 'bun:test';
11
12import { parsePromSample } from './platform-health.js';
13
14describe('parsePromSample', () => {
15 test('parses a real instant-query vector → number + instance label', () => {
16 const payload = {
17 status: 'success',
18 data: {
19 resultType: 'vector',
20 result: [
21 {
22 metric: { __name__: 'node_memory_MemTotal_bytes', instance: 'node-exporter:9100' },
23 value: [1719500000, '16777216000'],
24 },
25 ],
26 },
27 };
28 expect(parsePromSample(payload)).toEqual({
29 value: 16777216000,
30 instance: 'node-exporter:9100',
31 });
32 });
33
34 test('parses a computed CPU-busy expression (decimal string, no instance)', () => {
35 const payload = {
36 status: 'success',
37 data: { resultType: 'vector', result: [{ metric: {}, value: [1719500000, '42.5'] }] },
38 };
39 expect(parsePromSample(payload)).toEqual({ value: 42.5 });
40 });
41
42 test('empty result set → null (Prometheus up, metric absent)', () => {
43 const payload = { status: 'success', data: { resultType: 'vector', result: [] } };
44 expect(parsePromSample(payload)).toEqual({ value: null });
45 });
46
47 test('non-success status → null (never trusts an errored response)', () => {
48 const payload = { status: 'error', errorType: 'bad_data', error: 'parse error' };
49 expect(parsePromSample(payload)).toEqual({ value: null });
50 });
51
52 test('unparseable value string → null, not NaN', () => {
53 const payload = {
54 status: 'success',
55 data: { resultType: 'vector', result: [{ metric: {}, value: [1719500000, 'NaN'] }] },
56 };
57 expect(parsePromSample(payload)).toEqual({ value: null });
58 });
59
60 test('garbage / non-object inputs → null without throwing', () => {
61 expect(parsePromSample(null)).toEqual({ value: null });
62 expect(parsePromSample(undefined)).toEqual({ value: null });
63 expect(parsePromSample('not json')).toEqual({ value: null });
64 expect(parsePromSample(42)).toEqual({ value: null });
65 expect(parsePromSample({})).toEqual({ value: null });
66 });
67
68 test('uses the FIRST series when multiple hosts report (single-host summary)', () => {
69 const payload = {
70 status: 'success',
71 data: {
72 resultType: 'vector',
73 result: [
74 { metric: { instance: 'host-a:9100' }, value: [1, '100'] },
75 { metric: { instance: 'host-b:9100' }, value: [1, '200'] },
76 ],
77 },
78 };
79 expect(parsePromSample(payload)).toEqual({ value: 100, instance: 'host-a:9100' });
80 });
81});
82
83describe('host metric threshold → tone (HARD honesty: null stays "—")', () => {
84 // Mirror of the cockpit's usage-tone mapping (see admin/health/page.tsx).
85 // Kept side-by-side with a test so a threshold change shows up red.
86 function usageTone(percent: number | null, redAt: number, amberAt: number) {
87 if (percent === null) return 'muted';
88 if (percent >= redAt) return 'error';
89 if (percent >= amberAt) return 'warning';
90 return 'success';
91 }
92
93 test('cpu/disk: >85 red, >70 amber, else green', () => {
94 expect(usageTone(92, 85, 70)).toBe('error');
95 expect(usageTone(85, 85, 70)).toBe('error');
96 expect(usageTone(78, 85, 70)).toBe('warning');
97 expect(usageTone(12, 85, 70)).toBe('success');
98 });
99
100 test('null → muted (renders "—", never a fake colour)', () => {
101 expect(usageTone(null, 85, 70)).toBe('muted');
102 });
103
104 test('steal: >25 red', () => {
105 expect(usageTone(40, 25, 10)).toBe('error');
106 expect(usageTone(2, 25, 10)).toBe('success');
107 });
108});