mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-29 19:57:12 +00:00
c3c6c1b0c1
* fix(dashboard): use balloon-adjusted memory percent on the Memory tile When balloon fields are present, the Memory tile hero, tone, and gauge bar now use effectiveUsagePercent so they match the used/total bytes. Health and host RAM alerts still use working-set usagePercent. * fix(dashboard): align health banner and Memory tile on balloon-adjusted memory The health banner read the raw working-set percent while the Memory tile showed the balloon-adjusted one, so a hypervisor ballooning an otherwise healthy VM could flag it critical. Both now use effectiveUsagePercent, falling back to the raw percent when balloon fields are absent. Host RAM alerts keep the working-set percent: ballooned pages are reclaimed by the hypervisor and cannot be recovered by the guest on demand. Adds guarded context lines to the Memory tile: Current VM Memory (guest retained), Current pressure (effective used over retained), Balloon reclaimable, and, for ZFS nodes, Current memory in use (used plus ARC reclaimable) above ZFS ARC reclaimable. Validated with unit tests for both components including boundary cases, type checks, lint, and visual regression comparison confirming no change outside the Memory tile.
115 lines
4.0 KiB
TypeScript
115 lines
4.0 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { deriveHealth } from './deriveHealth';
|
|
import type { Stats, SystemStats, NotificationItem } from './types';
|
|
|
|
const stats = (over: Partial<Stats> = {}): Stats => ({
|
|
active: 5, managed: 5, unmanaged: 0, exited: 0, total: 5, ...over,
|
|
});
|
|
|
|
// deriveHealth only reads cpu.usage, memory.effectiveUsagePercent (falling back
|
|
// to memory.usagePercent) and disk?.usagePercent.
|
|
const sys = (cpu: string, ram: string, disk: string | null): SystemStats => ({
|
|
cpu: { usage: cpu, cores: 8 },
|
|
memory: { total: 100, used: 50, free: 50, usagePercent: ram },
|
|
disk: disk === null ? null : { fs: '/', mount: '/', total: 100, used: 50, free: 50, usagePercent: disk },
|
|
});
|
|
|
|
const note = (over: Partial<NotificationItem> = {}): NotificationItem => ({
|
|
id: 1, level: 'error', message: 'x', timestamp: 0, is_read: 0, ...over,
|
|
});
|
|
|
|
describe('deriveHealth', () => {
|
|
it('reports healthy when all metrics are low and nothing is wrong', () => {
|
|
const r = deriveHealth(stats(), sys('10', '20', '30'), []);
|
|
expect(r.level).toBe('healthy');
|
|
expect(r.reasons).toEqual(['All systems nominal']);
|
|
});
|
|
|
|
it('escalates to degraded at the 80 boundary but not at 79', () => {
|
|
expect(deriveHealth(stats(), sys('80', '20', '30'), []).level).toBe('degraded');
|
|
expect(deriveHealth(stats(), sys('79', '20', '30'), []).level).toBe('healthy');
|
|
});
|
|
|
|
it('escalates to critical at the 90 boundary', () => {
|
|
expect(deriveHealth(stats(), sys('20', '90', '30'), []).level).toBe('critical');
|
|
});
|
|
|
|
it('treats exited containers as degraded with a reason', () => {
|
|
const r = deriveHealth(stats({ exited: 2 }), sys('10', '20', '30'), []);
|
|
expect(r.level).toBe('degraded');
|
|
expect(r.reasons).toContain('2 exited');
|
|
});
|
|
|
|
it('escalates to critical when exits AND unread errors coincide below 90', () => {
|
|
const r = deriveHealth(stats({ exited: 1 }), sys('10', '20', '30'), [note()]);
|
|
expect(r.level).toBe('critical');
|
|
});
|
|
|
|
it('counts only unread error-level notifications, with pluralization', () => {
|
|
// A read error and an unread warning must both be ignored.
|
|
const ignored = deriveHealth(stats(), sys('10', '20', '30'), [
|
|
note({ is_read: 1 }),
|
|
note({ level: 'warning' }),
|
|
]);
|
|
expect(ignored.level).toBe('healthy');
|
|
|
|
const single = deriveHealth(stats(), sys('10', '20', '30'), [note()]);
|
|
expect(single.level).toBe('degraded');
|
|
expect(single.reasons).toContain('1 unread error');
|
|
|
|
const many = deriveHealth(stats(), sys('10', '20', '30'), [note(), note({ id: 2 })]);
|
|
expect(many.reasons).toContain('2 unread errors');
|
|
});
|
|
|
|
it('treats missing system stats and disk as zero usage', () => {
|
|
expect(deriveHealth(stats(), null, []).level).toBe('healthy');
|
|
expect(deriveHealth(stats(), sys('10', '20', null), []).level).toBe('healthy');
|
|
});
|
|
|
|
it('prefers the balloon-adjusted percent so health matches the Memory tile', () => {
|
|
const system = sys('10', '90', '30');
|
|
const r = deriveHealth(
|
|
stats(),
|
|
{
|
|
...system,
|
|
memory: {
|
|
...system.memory,
|
|
ballooned: 76,
|
|
effectiveUsed: 14,
|
|
effectiveTotal: 100,
|
|
effectiveUsagePercent: '14',
|
|
},
|
|
},
|
|
[],
|
|
);
|
|
expect(r.level).toBe('healthy');
|
|
expect(r.reasons).toEqual(['All systems nominal']);
|
|
});
|
|
|
|
it('escalates on a high balloon-adjusted percent and reports the adjusted number', () => {
|
|
const system = sys('10', '20', '30');
|
|
const r = deriveHealth(
|
|
stats(),
|
|
{
|
|
...system,
|
|
memory: {
|
|
...system.memory,
|
|
ballooned: 10,
|
|
effectiveUsed: 95,
|
|
effectiveTotal: 100,
|
|
effectiveUsagePercent: '95',
|
|
},
|
|
},
|
|
[],
|
|
);
|
|
expect(r.level).toBe('critical');
|
|
expect(r.reasons).toContain('RAM 95%');
|
|
});
|
|
|
|
it('falls back to raw usagePercent when balloon fields are absent', () => {
|
|
const r = deriveHealth(stats(), sys('10', '90', '30'), []);
|
|
expect(r.level).toBe('critical');
|
|
expect(r.reasons).toContain('RAM 90%');
|
|
});
|
|
});
|