collabSupport/tests/alarmSemantics.test.js
jmcqueen 21fa8f1436 Clean up SD-WAN alarm rendering + explain overlay vs physical
The wanAlarms check was pasting raw Prisma `info` JSON blobs (nested
`vpn_reasons` arrays with element/site/vpnlink ids) into the chat
message field. On a store with 20 NETWORK_ANYNETLINK_DOWN flaps this
produced a wall of unreadable stringified JSON where the actual
signal ("SD-WAN overlay tunnels are flapping") was lost.

Introduces a shared alarmSemantics module that:
  - Buckets each code into overlay / physical / device / other so
    the check + renderer stay consistent
  - Humanizes codes (NETWORK_ANYNETLINK_DOWN → "SD-WAN overlay
    tunnel down") with a Title-Cased fallback for unknown codes
  - Rolls up (code + severity) tuples so 20 identical alarms show as
    a single line with ×20 and a "just now / Nm / Nh / Nd" age

Rewrites wanAlarms.run() to use those helpers + cross-reference the
site's physical link state so operators aren't left wondering why 20
alarms fired while every metric shows green: overlay flaps get a
"physical WAN paths are all up per Link State" clarifier, and
physical alarms point back at the Link State check. The label loses
its hardcoded "(last 1h)" suffix since the alarm window is now
dynamic (defaults to 24h to match the WAN window).

The follow-up renderer used by /phonestatus imports the same helpers
so the two surfaces cannot drift.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 10:19:15 -04:00

179 lines
6.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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,
} 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',
]) {
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');
});