Show overall tunnel status with downs only, list the last five alarms with times, and note when overlay/physical alarms may explain bad voice DPI. Co-authored-by: Cursor <cursoragent@cursor.com>
197 lines
7.5 KiB
JavaScript
197 lines
7.5 KiB
JavaScript
// Unit tests for services/enrichment/alarmSemantics.js
|
||
//
|
||
// Coverage targets:
|
||
// - Known codes map to their expected category
|
||
// - Unknown codes fall through to 'other' (never throw)
|
||
// - Humanizer returns a friendly string; unknown codes become
|
||
// Title-Cased words rather than a raw constant
|
||
// - rollupAlarms collapses (code+severity) tuples, preserves the
|
||
// newest timestamp, and orders critical → major → minor
|
||
// - countByCategory sums correctly and defaults missing fields to 0
|
||
// - humanizeAge produces the expected "just now / Nm / Nh / Nd" bands
|
||
// - humanizeWindow formats 15/60/360/1440 minutes as 15m/1h/6h/1d
|
||
|
||
import test from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
|
||
import {
|
||
categorizeAlarm,
|
||
humanizeAlarmCode,
|
||
rollupAlarms,
|
||
countByCategory,
|
||
humanizeAge,
|
||
humanizeWindow,
|
||
isVoiceRelevantAlarm,
|
||
coerceAlarmTsMs,
|
||
} from '../services/enrichment/alarmSemantics.js';
|
||
|
||
// ─── categorizeAlarm ─────────────────────────────────────────────────
|
||
|
||
test('categorizeAlarm: overlay codes bucket as overlay', () => {
|
||
for (const code of [
|
||
'NETWORK_ANYNETLINK_DOWN',
|
||
'NETWORK_VPNLINK_DOWN',
|
||
'NETWORK_VPNLINK_FLAP',
|
||
'NETWORK_SITE_UNREACHABLE',
|
||
'NETWORK_STANDBY_LINK_DOWN',
|
||
'SITE_CONNECTIVITY_DEGRADED',
|
||
]) {
|
||
assert.equal(categorizeAlarm(code), 'overlay', `${code} → overlay`);
|
||
}
|
||
});
|
||
|
||
test('categorizeAlarm: physical WAN codes bucket as physical', () => {
|
||
for (const code of [
|
||
'NETWORK_INTERNET_DOWN',
|
||
'DEVICE_INTERFACE_DOWN',
|
||
'DEVICE_INTERFACE_STATE_CHANGED',
|
||
'NETWORK_LTE_LINK_DOWN',
|
||
]) {
|
||
assert.equal(categorizeAlarm(code), 'physical', `${code} → physical`);
|
||
}
|
||
});
|
||
|
||
test('categorizeAlarm: device / ION codes bucket as device', () => {
|
||
for (const code of [
|
||
'DEVICE_HB_MISSED',
|
||
'DEVICE_UNREACHABLE',
|
||
'DEVICE_HIGH_CPU',
|
||
'DEVICE_REBOOT',
|
||
]) {
|
||
assert.equal(categorizeAlarm(code), 'device');
|
||
}
|
||
});
|
||
|
||
test('categorizeAlarm: unknown / falsy → other (never throws)', () => {
|
||
assert.equal(categorizeAlarm('SOMETHING_BRAND_NEW'), 'other');
|
||
assert.equal(categorizeAlarm(''), 'other');
|
||
assert.equal(categorizeAlarm(null), 'other');
|
||
assert.equal(categorizeAlarm(undefined), 'other');
|
||
});
|
||
|
||
test('categorizeAlarm: case-insensitive on input', () => {
|
||
assert.equal(categorizeAlarm('network_anynetlink_down'), 'overlay');
|
||
});
|
||
|
||
// ─── humanizeAlarmCode ───────────────────────────────────────────────
|
||
|
||
test('humanizeAlarmCode: known codes → friendly labels', () => {
|
||
assert.equal(humanizeAlarmCode('NETWORK_ANYNETLINK_DOWN'), 'SD-WAN overlay tunnel down');
|
||
assert.equal(humanizeAlarmCode('DEVICE_HB_MISSED'), 'ION heartbeat missed');
|
||
assert.equal(humanizeAlarmCode('NETWORK_INTERNET_DOWN'), 'Internet WAN circuit down');
|
||
});
|
||
|
||
test('humanizeAlarmCode: unknown code → Title Cased fallback (not raw constant)', () => {
|
||
assert.equal(humanizeAlarmCode('SOMETHING_BRAND_NEW_DOWN'), 'Something Brand New Down');
|
||
});
|
||
|
||
test('humanizeAlarmCode: falsy → generic fallback', () => {
|
||
assert.equal(humanizeAlarmCode(''), 'Unknown alarm');
|
||
assert.equal(humanizeAlarmCode(null), 'Unknown alarm');
|
||
});
|
||
|
||
// ─── rollupAlarms ────────────────────────────────────────────────────
|
||
|
||
test('rollupAlarms: collapses (code + severity) tuples, keeps newest ts', () => {
|
||
const samples = [
|
||
{ code: 'X', severity: 'major', ts: '2026-07-09T10:00:00Z' },
|
||
{ code: 'X', severity: 'major', ts: '2026-07-09T12:00:00Z' },
|
||
{ code: 'X', severity: 'major', ts: '2026-07-09T09:00:00Z' },
|
||
{ code: 'Y', severity: 'critical', ts: '2026-07-09T08:00:00Z' },
|
||
];
|
||
const rollups = rollupAlarms(samples);
|
||
assert.equal(rollups.length, 2);
|
||
// Critical severity floats to the top regardless of count.
|
||
assert.equal(rollups[0].code, 'Y');
|
||
assert.equal(rollups[0].count, 1);
|
||
assert.equal(rollups[1].code, 'X');
|
||
assert.equal(rollups[1].count, 3);
|
||
// Newest ts survives.
|
||
assert.equal(rollups[1].newestTs, '2026-07-09T12:00:00Z');
|
||
});
|
||
|
||
test('rollupAlarms: sort — critical > major > minor, ties broken by count desc', () => {
|
||
const rollups = rollupAlarms([
|
||
{ code: 'A', severity: 'minor', ts: 't1' },
|
||
{ code: 'B', severity: 'major', ts: 't2' },
|
||
{ code: 'C', severity: 'critical', ts: 't3' },
|
||
{ code: 'D', severity: 'major', ts: 't4' },
|
||
{ code: 'D', severity: 'major', ts: 't5' },
|
||
]);
|
||
assert.deepEqual(
|
||
rollups.map((r) => r.code),
|
||
['C', 'D', 'B', 'A'],
|
||
'critical (C), then major sorted by count (D×2, B×1), then minor',
|
||
);
|
||
});
|
||
|
||
test('rollupAlarms: null/empty → []', () => {
|
||
assert.deepEqual(rollupAlarms(null), []);
|
||
assert.deepEqual(rollupAlarms([]), []);
|
||
assert.deepEqual(rollupAlarms(undefined), []);
|
||
});
|
||
|
||
// ─── countByCategory ─────────────────────────────────────────────────
|
||
|
||
test('countByCategory: counts overlay/physical/device/other + total', () => {
|
||
const c = countByCategory([
|
||
{ code: 'NETWORK_ANYNETLINK_DOWN' },
|
||
{ code: 'NETWORK_ANYNETLINK_DOWN' },
|
||
{ code: 'NETWORK_INTERNET_DOWN' },
|
||
{ code: 'DEVICE_HB_MISSED' },
|
||
{ code: 'UNRECOGNIZED_FOO' },
|
||
]);
|
||
assert.deepEqual(c, {
|
||
overlay: 2, physical: 1, device: 1, other: 1, total: 5,
|
||
});
|
||
});
|
||
|
||
test('countByCategory: empty → all zeros', () => {
|
||
assert.deepEqual(countByCategory([]), {
|
||
overlay: 0, physical: 0, device: 0, other: 0, total: 0,
|
||
});
|
||
});
|
||
|
||
// ─── humanizeAge ─────────────────────────────────────────────────────
|
||
|
||
test('humanizeAge: coarse buckets', () => {
|
||
const now = Date.parse('2026-07-09T14:00:00Z');
|
||
const at = (offsetSec) =>
|
||
humanizeAge(new Date(now - offsetSec * 1000).toISOString(), now);
|
||
assert.equal(at(10), 'just now');
|
||
assert.equal(at(120), '2m ago');
|
||
assert.equal(at(3600 * 2), '2h ago');
|
||
assert.equal(at(86400 * 3), '3d ago');
|
||
});
|
||
|
||
test('humanizeAge: missing / invalid ts → empty string', () => {
|
||
assert.equal(humanizeAge(null), '');
|
||
assert.equal(humanizeAge(undefined), '');
|
||
assert.equal(humanizeAge('not-a-date'), '');
|
||
});
|
||
|
||
// ─── humanizeWindow ──────────────────────────────────────────────────
|
||
|
||
test('humanizeWindow: canonical values', () => {
|
||
assert.equal(humanizeWindow(15), '15m');
|
||
assert.equal(humanizeWindow(45), '45m');
|
||
assert.equal(humanizeWindow(60), '1h');
|
||
assert.equal(humanizeWindow(360), '6h');
|
||
assert.equal(humanizeWindow(1440), '1d');
|
||
assert.equal(humanizeWindow(2880), '2d');
|
||
assert.equal(humanizeWindow(10080), '7d',
|
||
'the new default WAN window must round-trip cleanly');
|
||
});
|
||
|
||
test('isVoiceRelevantAlarm: overlay/physical/device yes, other no', () => {
|
||
assert.equal(isVoiceRelevantAlarm('NETWORK_VPNLINK_DOWN'), true);
|
||
assert.equal(isVoiceRelevantAlarm('NETWORK_INTERNET_DOWN'), true);
|
||
assert.equal(isVoiceRelevantAlarm('DEVICE_REBOOT'), true);
|
||
assert.equal(isVoiceRelevantAlarm('DHCP_FAILURE'), false);
|
||
});
|
||
|
||
test('coerceAlarmTsMs: ISO and epoch', () => {
|
||
assert.equal(coerceAlarmTsMs('2025-01-01T00:00:00Z'), Date.parse('2025-01-01T00:00:00Z'));
|
||
assert.equal(coerceAlarmTsMs(1_700_000_000_000), 1_700_000_000_000);
|
||
assert.equal(coerceAlarmTsMs(null), null);
|
||
});
|