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>
427 lines
16 KiB
JavaScript
427 lines
16 KiB
JavaScript
// tests/voiceDiag.wan.test.js
|
||
//
|
||
// Unit coverage for the 8 WAN checks under services/voiceDiag/checks/wan/.
|
||
// Same pattern as tests/voiceDiag.checks.test.js — build a stub
|
||
// ctx with a `sdwanSite` + `sdwanData` (no HTTP, no Prisma) and
|
||
// assert each check's verdict.
|
||
//
|
||
// Every check gets:
|
||
// - happy path (data present, in the compliant range)
|
||
// - threshold breach (warn and/or error where applicable)
|
||
// - missing data (skipped with a clear reason)
|
||
// - kill-switch skip (WAN_STANDARD_ENABLED=false)
|
||
|
||
import test from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
|
||
import { wanSiteCheck } from '../services/voiceDiag/checks/wan/wanSite.js';
|
||
import { wanHealthscoreCheck } from '../services/voiceDiag/checks/wan/wanHealthscore.js';
|
||
import { wanLinkStateCheck } from '../services/voiceDiag/checks/wan/wanLinkState.js';
|
||
import { wanLatencyCheck } from '../services/voiceDiag/checks/wan/wanLatency.js';
|
||
import { wanJitterCheck } from '../services/voiceDiag/checks/wan/wanJitter.js';
|
||
import { wanLossCheck } from '../services/voiceDiag/checks/wan/wanLoss.js';
|
||
import { wanMosCheck } from '../services/voiceDiag/checks/wan/wanMos.js';
|
||
import { wanAlarmsCheck } from '../services/voiceDiag/checks/wan/wanAlarms.js';
|
||
import { CHECKS } from '../services/voiceDiag/checks/index.js';
|
||
|
||
// ─── Helpers ────────────────────────────────────────────────────────
|
||
|
||
function mkWanCtx({ site = { id: 'site-1', name: 'CG00782' }, links = [], healthscore = null, alarms = null, elements = [] } = {}) {
|
||
return {
|
||
storeNum: '782',
|
||
personId: null,
|
||
personLabel: 'store 782',
|
||
telephonyProfile: {},
|
||
phoneStatus: null,
|
||
sdwanSite: site,
|
||
sdwanData: site ? {
|
||
storeNum: '782',
|
||
site,
|
||
elements,
|
||
links,
|
||
healthscore,
|
||
alarms: alarms || { last1h: { critical: 0, major: 0, minor: 0 }, samples: [] },
|
||
errors: [],
|
||
} : null,
|
||
};
|
||
}
|
||
|
||
function link(overrides = {}) {
|
||
return {
|
||
interfaceId: 'if-mpls',
|
||
interfaceName: 'wan1',
|
||
elementId: 'el-1',
|
||
elementName: 'ION-A',
|
||
transportType: 'MPLS',
|
||
up: true,
|
||
latencyMs: 40,
|
||
jitterMs: 5,
|
||
lossPct: 0.1,
|
||
mos: 4.4,
|
||
...overrides,
|
||
};
|
||
}
|
||
|
||
async function withKillSwitchOn(fn) {
|
||
const prev = process.env.WAN_STANDARD_ENABLED;
|
||
process.env.WAN_STANDARD_ENABLED = 'false';
|
||
try { await fn(); }
|
||
finally {
|
||
if (prev === undefined) delete process.env.WAN_STANDARD_ENABLED;
|
||
else process.env.WAN_STANDARD_ENABLED = prev;
|
||
}
|
||
}
|
||
|
||
// ─── wanSite ────────────────────────────────────────────────────────
|
||
|
||
test('wanSite: happy path → ok with site + element + link counts in message', async () => {
|
||
const ctx = mkWanCtx({
|
||
elements: [{ id: 'el-1', connected: true }, { id: 'el-2', connected: false }],
|
||
links: [link(), link({ interfaceId: 'if-bb' })],
|
||
});
|
||
const r = await wanSiteCheck.run(ctx);
|
||
assert.equal(r.status, 'ok');
|
||
assert.match(r.message, /CG00782/);
|
||
assert.equal(r.details.elementCount, 2);
|
||
assert.equal(r.details.connectedElementCount, 1);
|
||
assert.equal(r.details.linkCount, 2);
|
||
});
|
||
|
||
test('wanSite: kill-switch skips', async () => {
|
||
await withKillSwitchOn(async () => {
|
||
const r = await wanSiteCheck.run(mkWanCtx({ elements: [{ id: 'x', connected: true }] }));
|
||
assert.equal(r.status, 'skipped');
|
||
assert.match(r.message, /WAN_STANDARD_ENABLED/);
|
||
});
|
||
});
|
||
|
||
// ─── wanHealthscore ─────────────────────────────────────────────────
|
||
|
||
test('wanHealthscore: 92/100 → ok', async () => {
|
||
const r = await wanHealthscoreCheck.run(mkWanCtx({ healthscore: { value: 92, breakdown: {} } }));
|
||
assert.equal(r.status, 'ok');
|
||
});
|
||
|
||
test('wanHealthscore: 70/100 → warn (< default warn 80)', async () => {
|
||
const r = await wanHealthscoreCheck.run(mkWanCtx({ healthscore: { value: 70, breakdown: {} } }));
|
||
assert.equal(r.status, 'warn');
|
||
assert.equal(r.details.value, 70);
|
||
});
|
||
|
||
test('wanHealthscore: 40/100 → error (< default error 60)', async () => {
|
||
const r = await wanHealthscoreCheck.run(mkWanCtx({ healthscore: { value: 40, breakdown: {} } }));
|
||
assert.equal(r.status, 'error');
|
||
});
|
||
|
||
test('wanHealthscore: env override adjusts thresholds', async () => {
|
||
const prev = process.env.WAN_STANDARD_HEALTHSCORE_WARN;
|
||
process.env.WAN_STANDARD_HEALTHSCORE_WARN = '95';
|
||
try {
|
||
const r = await wanHealthscoreCheck.run(mkWanCtx({ healthscore: { value: 92 } }));
|
||
assert.equal(r.status, 'warn', '92 should warn once threshold is raised to 95');
|
||
} finally {
|
||
if (prev === undefined) delete process.env.WAN_STANDARD_HEALTHSCORE_WARN;
|
||
else process.env.WAN_STANDARD_HEALTHSCORE_WARN = prev;
|
||
}
|
||
});
|
||
|
||
test('wanHealthscore: missing → skipped', async () => {
|
||
const r = await wanHealthscoreCheck.run(mkWanCtx({ healthscore: null }));
|
||
assert.equal(r.status, 'skipped');
|
||
});
|
||
|
||
test('wanHealthscore: kill-switch skips', async () => {
|
||
await withKillSwitchOn(async () => {
|
||
const r = await wanHealthscoreCheck.run(mkWanCtx({ healthscore: { value: 40 } }));
|
||
assert.equal(r.status, 'skipped');
|
||
assert.match(r.message, /WAN_STANDARD_ENABLED/);
|
||
});
|
||
});
|
||
|
||
// ─── wanLinkState ───────────────────────────────────────────────────
|
||
|
||
test('wanLinkState: all up → ok', async () => {
|
||
const r = await wanLinkStateCheck.run(mkWanCtx({
|
||
links: [link({ interfaceId: 'a', up: true }), link({ interfaceId: 'b', up: true })],
|
||
}));
|
||
assert.equal(r.status, 'ok');
|
||
assert.equal(r.details.up, 2);
|
||
});
|
||
|
||
test('wanLinkState: one down → error, offender named', async () => {
|
||
const r = await wanLinkStateCheck.run(mkWanCtx({
|
||
links: [
|
||
link({ interfaceId: 'a', up: true }),
|
||
link({ interfaceId: 'b', up: false, interfaceName: 'bb1', transportType: 'BROADBAND' }),
|
||
],
|
||
}));
|
||
assert.equal(r.status, 'error');
|
||
assert.equal(r.details.down, 1);
|
||
assert.match(r.message, /bb1/);
|
||
});
|
||
|
||
test('wanLinkState: all unknown, none up → warn', async () => {
|
||
const r = await wanLinkStateCheck.run(mkWanCtx({
|
||
links: [link({ up: null }), link({ interfaceId: 'x', up: null })],
|
||
}));
|
||
assert.equal(r.status, 'warn');
|
||
assert.equal(r.details.unknown, 2);
|
||
});
|
||
|
||
test('wanLinkState: no links → skipped', async () => {
|
||
const r = await wanLinkStateCheck.run(mkWanCtx({ links: [] }));
|
||
assert.equal(r.status, 'skipped');
|
||
});
|
||
|
||
// ─── wanLatency ─────────────────────────────────────────────────────
|
||
|
||
test('wanLatency: all under 150ms → ok', async () => {
|
||
const r = await wanLatencyCheck.run(mkWanCtx({ links: [link({ latencyMs: 40 }), link({ latencyMs: 80 })] }));
|
||
assert.equal(r.status, 'ok');
|
||
});
|
||
|
||
test('wanLatency: one path 220ms → warn (> 150, < 400)', async () => {
|
||
const r = await wanLatencyCheck.run(mkWanCtx({
|
||
links: [link({ latencyMs: 40 }), link({ interfaceId: 'x', interfaceName: 'x1', latencyMs: 220 })],
|
||
}));
|
||
assert.equal(r.status, 'warn');
|
||
assert.match(r.message, /x1/);
|
||
assert.match(r.message, /220ms/);
|
||
});
|
||
|
||
test('wanLatency: one path 500ms → error (> 400)', async () => {
|
||
const r = await wanLatencyCheck.run(mkWanCtx({
|
||
links: [link({ latencyMs: 40 }), link({ interfaceId: 'x', interfaceName: 'x1', latencyMs: 500 })],
|
||
}));
|
||
assert.equal(r.status, 'error');
|
||
});
|
||
|
||
test('wanLatency: env override adjusts thresholds', async () => {
|
||
const prev = process.env.WAN_STANDARD_LATENCY_WARN_MS;
|
||
process.env.WAN_STANDARD_LATENCY_WARN_MS = '50';
|
||
try {
|
||
const r = await wanLatencyCheck.run(mkWanCtx({ links: [link({ latencyMs: 80 })] }));
|
||
assert.equal(r.status, 'warn', '80ms should warn when threshold is 50');
|
||
} finally {
|
||
if (prev === undefined) delete process.env.WAN_STANDARD_LATENCY_WARN_MS;
|
||
else process.env.WAN_STANDARD_LATENCY_WARN_MS = prev;
|
||
}
|
||
});
|
||
|
||
test('wanLatency: no data → skipped', async () => {
|
||
const r = await wanLatencyCheck.run(mkWanCtx({ links: [link({ latencyMs: null })] }));
|
||
assert.equal(r.status, 'skipped');
|
||
});
|
||
|
||
test('wanLatency: kill-switch skips', async () => {
|
||
await withKillSwitchOn(async () => {
|
||
const r = await wanLatencyCheck.run(mkWanCtx({ links: [link({ latencyMs: 500 })] }));
|
||
assert.equal(r.status, 'skipped');
|
||
});
|
||
});
|
||
|
||
// ─── wanJitter ──────────────────────────────────────────────────────
|
||
|
||
test('wanJitter: 5ms → ok', async () => {
|
||
const r = await wanJitterCheck.run(mkWanCtx({ links: [link({ jitterMs: 5 })] }));
|
||
assert.equal(r.status, 'ok');
|
||
});
|
||
|
||
test('wanJitter: 40ms → warn (> 30, < 50)', async () => {
|
||
const r = await wanJitterCheck.run(mkWanCtx({ links: [link({ jitterMs: 40 })] }));
|
||
assert.equal(r.status, 'warn');
|
||
});
|
||
|
||
test('wanJitter: 100ms → error (> 50)', async () => {
|
||
const r = await wanJitterCheck.run(mkWanCtx({ links: [link({ jitterMs: 100 })] }));
|
||
assert.equal(r.status, 'error');
|
||
});
|
||
|
||
// ─── wanLoss ────────────────────────────────────────────────────────
|
||
|
||
test('wanLoss: 0.1% → ok', async () => {
|
||
const r = await wanLossCheck.run(mkWanCtx({ links: [link({ lossPct: 0.1 })] }));
|
||
assert.equal(r.status, 'ok');
|
||
});
|
||
|
||
test('wanLoss: 2% → warn (> 1, < 3)', async () => {
|
||
const r = await wanLossCheck.run(mkWanCtx({ links: [link({ lossPct: 2 })] }));
|
||
assert.equal(r.status, 'warn');
|
||
});
|
||
|
||
test('wanLoss: 5% → error (> 3)', async () => {
|
||
const r = await wanLossCheck.run(mkWanCtx({ links: [link({ lossPct: 5 })] }));
|
||
assert.equal(r.status, 'error');
|
||
});
|
||
|
||
// ─── wanMos ─────────────────────────────────────────────────────────
|
||
// MOS is inverted — low is bad.
|
||
|
||
test('wanMos: 4.4 → ok (>= 4.0)', async () => {
|
||
const r = await wanMosCheck.run(mkWanCtx({ links: [link({ mos: 4.4 })] }));
|
||
assert.equal(r.status, 'ok');
|
||
});
|
||
|
||
test('wanMos: 3.8 → warn (< 4.0, >= 3.5)', async () => {
|
||
const r = await wanMosCheck.run(mkWanCtx({ links: [link({ mos: 3.8 })] }));
|
||
assert.equal(r.status, 'warn');
|
||
});
|
||
|
||
test('wanMos: 3.0 → error (< 3.5)', async () => {
|
||
const r = await wanMosCheck.run(mkWanCtx({ links: [link({ mos: 3.0 })] }));
|
||
assert.equal(r.status, 'error');
|
||
});
|
||
|
||
// ─── wanAlarms ──────────────────────────────────────────────────────
|
||
|
||
test('wanAlarms: none → ok', async () => {
|
||
const r = await wanAlarmsCheck.run(mkWanCtx());
|
||
assert.equal(r.status, 'ok');
|
||
});
|
||
|
||
test('wanAlarms: minor only → ok (informational)', async () => {
|
||
const r = await wanAlarmsCheck.run(mkWanCtx({
|
||
alarms: { last1h: { critical: 0, major: 0, minor: 3 }, samples: [] },
|
||
}));
|
||
assert.equal(r.status, 'ok');
|
||
assert.equal(r.details.minor, 3);
|
||
});
|
||
|
||
test('wanAlarms: major → warn (message cites humanized code, never raw info blob)', async () => {
|
||
const r = await wanAlarmsCheck.run(mkWanCtx({
|
||
alarms: {
|
||
last1h: { critical: 0, major: 1, minor: 0 },
|
||
samples: [{
|
||
code: 'NETWORK_ANYNETLINK_DOWN',
|
||
severity: 'major',
|
||
message: '{"vpn_reasons":[{"code":"NETWORK_VPNLINK_DOWN","element_id":"…"}]}',
|
||
ts: new Date().toISOString(),
|
||
}],
|
||
},
|
||
}));
|
||
assert.equal(r.status, 'warn');
|
||
// Humanized code appears — no raw JSON blob.
|
||
assert.match(r.message, /SD-WAN overlay tunnel down/);
|
||
assert.equal(r.message.includes('vpn_reasons'), false,
|
||
'raw Prisma info JSON must never leak into the chat message');
|
||
assert.equal(r.message.includes('{"'), false,
|
||
'no stringified object leakage');
|
||
// Category count captured in details.
|
||
assert.equal(r.details.byCategory.overlay, 1);
|
||
});
|
||
|
||
test('wanAlarms: critical → error, cites the highest-severity rollup', async () => {
|
||
const r = await wanAlarmsCheck.run(mkWanCtx({
|
||
alarms: {
|
||
last1h: { critical: 2, major: 0, minor: 0 },
|
||
samples: [
|
||
{ code: 'DEVICE_UNREACHABLE', severity: 'critical', ts: '2026-07-09T14:00:00Z' },
|
||
{ code: 'DEVICE_UNREACHABLE', severity: 'critical', ts: '2026-07-09T14:05:00Z' },
|
||
],
|
||
},
|
||
}));
|
||
assert.equal(r.status, 'error');
|
||
assert.match(r.message, /2 critical alarms/);
|
||
assert.match(r.message, /ION unreachable/);
|
||
assert.match(r.message, /×2/);
|
||
});
|
||
|
||
test('wanAlarms: rollup groups near-identical alarms in details.recentSamples', async () => {
|
||
const samples = Array.from({ length: 20 }).map((_, i) => ({
|
||
code: 'NETWORK_ANYNETLINK_DOWN',
|
||
severity: 'major',
|
||
ts: new Date(Date.now() - i * 60_000).toISOString(),
|
||
}));
|
||
const r = await wanAlarmsCheck.run(mkWanCtx({
|
||
alarms: { last1h: { critical: 0, major: 20, minor: 0 }, samples },
|
||
}));
|
||
assert.equal(r.status, 'warn');
|
||
assert.equal(r.details.recentSamples.length, 1,
|
||
'20 identical alarms should collapse to a single rolled-up row');
|
||
assert.equal(r.details.recentSamples[0].count, 20);
|
||
assert.equal(r.details.recentSamples[0].category, 'overlay');
|
||
assert.equal(r.details.recentSamples[0].humanized, 'SD-WAN overlay tunnel down');
|
||
});
|
||
|
||
test('wanAlarms: overlay-only + all physical links up → adds clarifying hint', async () => {
|
||
const r = await wanAlarmsCheck.run(mkWanCtx({
|
||
links: [link(), link({ interfaceId: 'if-bb', up: true })],
|
||
alarms: {
|
||
last1h: { critical: 0, major: 5, minor: 0 },
|
||
samples: Array.from({ length: 5 }).map(() => ({
|
||
code: 'NETWORK_VPNLINK_DOWN', severity: 'major',
|
||
})),
|
||
},
|
||
}));
|
||
assert.equal(r.status, 'warn');
|
||
assert.match(r.message, /physical WAN paths are all up/);
|
||
});
|
||
|
||
test('wanAlarms: physical alarms + a physical link down → different clarifier', async () => {
|
||
const r = await wanAlarmsCheck.run(mkWanCtx({
|
||
links: [link({ up: false })],
|
||
alarms: {
|
||
last1h: { critical: 1, major: 0, minor: 0 },
|
||
samples: [{ code: 'NETWORK_INTERNET_DOWN', severity: 'critical' }],
|
||
},
|
||
}));
|
||
assert.equal(r.status, 'error');
|
||
assert.match(r.message, /Physical WAN interfaces are affected/);
|
||
});
|
||
|
||
test('wanAlarms: no alarms feed → skipped', async () => {
|
||
const ctx = mkWanCtx();
|
||
ctx.sdwanData.alarms = null;
|
||
const r = await wanAlarmsCheck.run(ctx);
|
||
assert.equal(r.status, 'skipped');
|
||
});
|
||
|
||
test('wanAlarms: message reflects the effective alarm window (not hardcoded 1h)', async () => {
|
||
const ctx = mkWanCtx({
|
||
alarms: {
|
||
last1h: { critical: 0, major: 1, minor: 0 },
|
||
samples: [{ code: 'NETWORK_ANYNETLINK_DOWN', severity: 'major' }],
|
||
},
|
||
});
|
||
// Simulate a 24h alarm window (matches the shipping default)
|
||
ctx.sdwanData.window = { minutes: 1440, alarmMinutes: 1440 };
|
||
const r = await wanAlarmsCheck.run(ctx);
|
||
assert.match(r.message, /in the last 1d/);
|
||
});
|
||
|
||
// ─── standards regression + registry ordering ──────────────────────
|
||
|
||
test('every WAN check exposes a standards object', () => {
|
||
const wanChecks = CHECKS.filter((c) => c.id.startsWith('wan'));
|
||
assert.equal(wanChecks.length, 8, 'expected 8 registered WAN checks');
|
||
const missing = wanChecks.filter((c) => !c.standards || typeof c.standards !== 'object');
|
||
assert.deepEqual(missing.map((c) => c.id), []);
|
||
});
|
||
|
||
test('WAN checks registered in the expected order after port bucket', () => {
|
||
const ids = CHECKS.map((c) => c.id);
|
||
const wanIds = ids.filter((id) => id.startsWith('wan'));
|
||
assert.deepEqual(wanIds, [
|
||
'wanSite',
|
||
'wanHealthscore',
|
||
'wanLinkState',
|
||
'wanLatency',
|
||
'wanJitter',
|
||
'wanLoss',
|
||
'wanMos',
|
||
'wanAlarms',
|
||
]);
|
||
// WAN bucket sits after the port bucket and before phoneOnline.
|
||
const portEnabledIdx = ids.indexOf('portEnabled');
|
||
const wanSiteIdx = ids.indexOf('wanSite');
|
||
const phoneOnlineIdx = ids.indexOf('phoneOnline');
|
||
assert.ok(portEnabledIdx < wanSiteIdx);
|
||
assert.ok(wanSiteIdx < phoneOnlineIdx);
|
||
});
|
||
|
||
test('no WAN check declares a remediation (diagnostic-only)', () => {
|
||
const wanChecks = CHECKS.filter((c) => c.id.startsWith('wan'));
|
||
for (const c of wanChecks) {
|
||
assert.equal(c.remediations, undefined, `${c.id} should not expose remediations`);
|
||
}
|
||
});
|