collabSupport/tests/voiceDiag.wan.test.js
jmcqueen b802383441 Add Prisma SD-WAN voice-quality enrichment for /phonestatus + /voicediag
Introduces a full Palo Alto Prisma SD-WAN integration (dual-mode SASE
OAuth 2.0 / legacy CloudGenix auth, pagination, 429 backoff, session
priming) that surfaces per-path latency/jitter/loss/MOS, site
healthscore, link state, and alarm data for a store. Wired into the
/phonestatus WAN follow-up and eight new /voicediag WAN checks graded
against ITU-T G.114 / RFC 3550 defaults (env-overridable via
WAN_STANDARD_*).

Also adds a shape-aware detail renderer for /voicediag (per-link
tables with verdict icons instead of a stringified JSON dump) and a
--window flag (15m / 1h / 6h / 24h / 1d, env default via
WAN_STANDARD_WINDOW_MINUTES) so operators can widen the look-back
without redeploying. scripts/prismaProbe.js is bundled as a CLI for
schema iteration against a live tenant.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 09:45:29 -04:00

348 lines
13 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', async () => {
const r = await wanAlarmsCheck.run(mkWanCtx({
alarms: { last1h: { critical: 0, major: 1, minor: 0 }, samples: [{ code: 'M1', message: 'wobble' }] },
}));
assert.equal(r.status, 'warn');
assert.match(r.message, /wobble/);
});
test('wanAlarms: critical → error', async () => {
const r = await wanAlarmsCheck.run(mkWanCtx({
alarms: { last1h: { critical: 2, major: 0, minor: 0 }, samples: [{ code: 'C1', message: 'boom' }] },
}));
assert.equal(r.status, 'error');
assert.match(r.message, /boom/);
});
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');
});
// ─── 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`);
}
});