collabSupport/tests/voiceDiag.wan.test.js
jmcqueen 9d0dbb071e Add per-app DPI voice-quality checks + widen WAN window to 7d
Adds three new SD-WAN checks (wanAppRtpMos/Loss/Jitter) that measure
REAL voice-traffic quality on actual RTP frames via Prisma DPI, not
synthetic link probes. Graded against the WORST 5-minute window so
transient degradation the 24h link-probe averages smooth away
actually surfaces.

Voice-app selection is tenant-configurable via PRISMA_APP_ID_VOICE +
PRISMA_APP_NAME_VOICE (Webex_Calling_RTP recommended for Webex
Calling shops — the Webex-specific DPI signature excludes non-Webex
UDP noise). Legacy PRISMA_APP_ID_RTP_BASE still honored with a
one-time deprecation warning.

Widens the default WAN look-back from 24h to 7 days: per-app metrics
only get datapoints when calls actually happen, so sporadic Webex
Calling stores (3-4 calls/day) need a wider window for worst-window
statistics to be meaningful. Interval picker snaps 7d to 1hour
buckets (168 pts) to keep payloads bounded while preserving
worst-hour granularity. Hard-capped at 7d — beyond that Prisma
downsamples to 1-day buckets and the signal collapses.

Also:
- Client-side concurrency limiter (PRISMA_MAX_INFLIGHT, default 3)
  to prevent 429 cascades when /voicediag fans out 10+ parallel
  metric fetches
- "View in Prisma UI" deep links in both /phonestatus WAN follow-up
  and /voicediag details, threading through a new
  integrations/paloalto/urls.js builder
- humanizeMetricUnit maps raw API unit strings ("percentage",
  "milliseconds") to display symbols ("%", "ms") to fix
  "11.83percentage" leaking to the UI
- getAppAudio envelope distinguishes not-configured / fetch-failed /
  no-traffic states so misleading "set env var" messages don't fire
  when the real problem is a 429

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

688 lines
28 KiB
JavaScript
Raw Permalink 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.

// 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 { wanAppRtpMosCheck } from '../services/voiceDiag/checks/wan/wanAppRtpMos.js';
import { wanAppRtpLossCheck } from '../services/voiceDiag/checks/wan/wanAppRtpLoss.js';
import { wanAppRtpJitterCheck } from '../services/voiceDiag/checks/wan/wanAppRtpJitter.js';
import { wanAlarmsCheck } from '../services/voiceDiag/checks/wan/wanAlarms.js';
import { humanizeMetricUnit } from '../services/voiceDiag/checks/wan/_helpers.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, 11, 'expected 11 registered WAN checks (8 link + 3 app-DPI)');
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',
// Per-app DPI checks land after the link-probe checks — the
// link-probe pass/fail is the coarse signal, then the per-app
// check refines it. This ordering shows up in the /voicediag
// output as "link is up + green" followed by "but actual RTP
// saw..." which reads naturally for an operator.
'wanAppRtpMos',
'wanAppRtpLoss',
'wanAppRtpJitter',
'wanAlarms',
]);
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`);
}
});
// ─── wanAppRtp* (per-app DPI voice-quality checks) ──────────────────
//
// These grade against the WORST-window value in the series rather
// than the average — the whole point is to catch transient
// degradation the 24h link-probe average smooths away. Every case
// below spells out which value the check MUST grade against (min for
// MOS, max for loss/jitter) because if the accessor picks the wrong
// side of the summary a well-averaged store would silently pass
// while its calls sound terrible.
function mkAppCtx({ mos, loss, jitter, bandwidth } = {}) {
const ctx = mkWanCtx({});
// Uses Webex_Calling_RTP as the exemplar in fixtures since it's the
// recommended production choice — but the checks are app-agnostic
// so any app id/name should behave identically.
ctx.sdwanData.appAudio = {
appId: '1708539371717015196',
appName: 'Webex_Calling_RTP',
mos, loss, jitter, bandwidth,
};
return ctx;
}
function seriesSummary({ values, unit = '', interval = '5min' } = {}) {
// Build a shape identical to summarizeAppSeries() output so the
// check exercises the real threshold path rather than a stub.
const nums = values.filter((v) => typeof v === 'number' && Number.isFinite(v));
let min = null, max = null, avg = null, p95 = null;
if (nums.length > 0) {
min = Math.min(...nums);
max = Math.max(...nums);
avg = Math.round((nums.reduce((a, b) => a + b, 0) / nums.length) * 100) / 100;
const sorted = [...nums].sort((a, b) => a - b);
p95 = sorted[Math.min(sorted.length - 1, Math.floor(0.95 * sorted.length))];
}
return {
unit, interval,
samples: values.length, validSamples: nums.length,
avg, min, max, p95, values,
};
}
test('wanAppRtpMos: skipped when appAudio missing (feature not configured)', async () => {
const ctx = mkWanCtx({}); // no appAudio
const r = await wanAppRtpMosCheck.run(ctx);
assert.equal(r.status, 'skipped');
// Message must point operators at the canonical env var name.
// Backwards-compat handling for the legacy PRISMA_APP_ID_RTP_BASE
// lives in resolveVoiceAppConfig(); the surface message points at
// the new name only.
assert.match(r.message, /PRISMA_APP_ID_VOICE/);
});
test('wanAppRtpMos: ok when worst-window MOS >= warn (default 4.0)', async () => {
const ctx = mkAppCtx({ mos: seriesSummary({ values: [4.1, 4.3, 4.5, 4.0] }) });
const r = await wanAppRtpMosCheck.run(ctx);
assert.equal(r.status, 'ok');
});
test('wanAppRtpMos: warn when worst-window MOS < 4.0 but >= 3.5 (avg irrelevant)', async () => {
// Avg = 4.05 (looks fine) but one 5-min window dipped to 3.8 →
// must warn, not pass. This is the whole reason the check exists.
const ctx = mkAppCtx({ mos: seriesSummary({ values: [4.3, 4.2, 3.8, 4.5] }) });
const r = await wanAppRtpMosCheck.run(ctx);
assert.equal(r.status, 'warn');
assert.match(r.message, /3\.8/, 'worst-window value must appear in message');
});
test('wanAppRtpMos: error when worst-window MOS < 3.5 (matches HAR failure case)', async () => {
// Real Webex_Calling_RTP numbers from CG00127
// (webex-base-metricCG00127.har, 2026-07-09): min=1.60, avg=4.27.
// Even though avg is fine, the min alone must fire error since
// those 5-min windows rendered calls unintelligible.
const ctx = mkAppCtx({ mos: seriesSummary({ values: [4.4, 4.3, 3.92, 1.60, 4.41, 4.03] }) });
const r = await wanAppRtpMosCheck.run(ctx);
assert.equal(r.status, 'error');
assert.match(r.message, /1\.6/);
assert.match(r.message, /worst-window/i);
});
test('wanAppRtpLoss: ok when worst-window loss <= 5%', async () => {
const ctx = mkAppCtx({ loss: seriesSummary({ values: [0, 0.5, 1, 2, 4] }) });
const r = await wanAppRtpLossCheck.run(ctx);
assert.equal(r.status, 'ok');
});
test('wanAppRtpLoss: warn when worst-window loss > 5% but <= 15%', async () => {
const ctx = mkAppCtx({ loss: seriesSummary({ values: [0, 0, 10, 0] }) });
const r = await wanAppRtpLossCheck.run(ctx);
assert.equal(r.status, 'warn');
assert.match(r.message, /10%/);
});
test('wanAppRtpLoss: error when worst-window loss > 15% (matches HAR failure)', async () => {
// Real Webex_Calling_RTP numbers from CG00127: max=26.88% loss.
// Must error even though avg was only ~1.37%.
const ctx = mkAppCtx({ loss: seriesSummary({ values: [0, 5, 26.88, 0, 3] }) });
const r = await wanAppRtpLossCheck.run(ctx);
assert.equal(r.status, 'error');
assert.match(r.message, /26\.88%/);
});
test('wanAppRtpJitter: ok when all-zero series (nothing to grade badly)', async () => {
// The HAR shows AppPerfUDPAudioJitter often returns all zeros for
// this tenant. Must be treated as "clean throughout", NOT skipped
// and NOT bad — the metric legitimately reports zero and that IS
// an ok result.
const ctx = mkAppCtx({ jitter: seriesSummary({ values: [0, 0, 0, 0, 0] }) });
const r = await wanAppRtpJitterCheck.run(ctx);
assert.equal(r.status, 'ok');
});
test('wanAppRtpJitter: error when worst-window > 50ms', async () => {
const ctx = mkAppCtx({ jitter: seriesSummary({ values: [1, 2, 75, 3, 0] }) });
const r = await wanAppRtpJitterCheck.run(ctx);
assert.equal(r.status, 'error');
assert.match(r.message, /75ms/);
});
test('wanAppRtp*: skipped when validSamples=0 (Prisma returned data but all-null)', async () => {
const emptyish = seriesSummary({ values: [null, null, null] });
const ctx = mkAppCtx({ mos: emptyish, loss: emptyish, jitter: emptyish });
for (const check of [wanAppRtpMosCheck, wanAppRtpLossCheck, wanAppRtpJitterCheck]) {
const r = await check.run(ctx);
assert.equal(r.status, 'skipped', `${check.id} → skipped on all-null`);
assert.match(r.message, /no voice traffic/i);
}
});
test('wanAppRtp*: kill switch (WAN_STANDARD_ENABLED=false) silences all three', async () => {
await withKillSwitchOn(async () => {
const ctx = mkAppCtx({ mos: seriesSummary({ values: [1.5] }) });
for (const check of [wanAppRtpMosCheck, wanAppRtpLossCheck, wanAppRtpJitterCheck]) {
const r = await check.run(ctx);
assert.equal(r.status, 'skipped', `${check.id} kill-switch triggered`);
assert.match(r.message, /WAN_STANDARD_ENABLED=false/);
}
});
});
test('wanAppRtp*: badSamplePct exposes how often the metric was in warn/error', async () => {
// 4 of 10 samples in error range → 40% bad.
const values = [0, 0, 20, 22, 0, 30, 0, 0, 50, 0]; // 4 > 15%
const ctx = mkAppCtx({ loss: seriesSummary({ values }) });
const r = await wanAppRtpLossCheck.run(ctx);
assert.equal(r.status, 'error');
assert.equal(r.details.badSampleCount, 4);
assert.equal(r.details.badSamplePct, 40);
});
test('wanAppRtp*: fetch-failure surfaces the underlying error message (not "set env var")', async () => {
// Regression for the 429-cascade UX bug: previously, a fetch
// failure with the env var CONFIGURED caused the check to say
// "set PRISMA_APP_ID_RTP_BASE" — actively misleading. Now the
// check tells the operator the real reason (429, timeout, etc).
//
// Error-scope names are app-agnostic ("app.voice.*") so this
// still routes correctly regardless of which voice app the tenant
// configured.
const ctx = mkWanCtx({});
ctx.sdwanData.appAudio = {
appId: '1708539371717015196',
appName: 'Webex_Calling_RTP',
mos: null, // fetch failed
loss: null, // fetch failed
jitter: seriesSummary({ values: [0, 0, 0] }), // succeeded
bandwidth: seriesSummary({ values: [0.5] }),
};
ctx.sdwanData.errors = [
{ scope: 'app.voice.mos', message: 'Request failed with status code 429 (HTTP 429)' },
{ scope: 'app.voice.loss', message: 'timeout of 20000ms exceeded' },
];
const mosResult = await wanAppRtpMosCheck.run(ctx);
assert.equal(mosResult.status, 'skipped');
assert.match(mosResult.message, /429/, 'must surface the underlying 429');
assert.doesNotMatch(mosResult.message, /PRISMA_APP_ID_VOICE/,
'must NOT say "set env var" — the env IS set, the fetch just failed');
assert.match(mosResult.message, /Retry in ~30/, 'includes actionable "try again" hint');
const lossResult = await wanAppRtpLossCheck.run(ctx);
assert.match(lossResult.message, /timeout/);
// Jitter succeeded so it should grade normally, not be dragged
// down by its siblings' failures.
const jitterResult = await wanAppRtpJitterCheck.run(ctx);
assert.equal(jitterResult.status, 'ok',
'per-metric fetch failures must not cascade into sibling metrics');
});
test('humanizeMetricUnit: maps raw Prisma unit strings to concise display suffixes', () => {
// The important cases — everything we've seen in a real Prisma
// response. These are what caused "11.83percentage" / "50milliseconds"
// to leak into the UI before this was added.
assert.equal(humanizeMetricUnit('percentage'), '%');
assert.equal(humanizeMetricUnit('percent'), '%');
assert.equal(humanizeMetricUnit('milliseconds'), 'ms');
assert.equal(humanizeMetricUnit('ms'), 'ms');
assert.equal(humanizeMetricUnit('count'), '',
'MOS unit "count" renders as empty — MOS numbers are self-explanatory');
assert.equal(humanizeMetricUnit('gauge'), '');
assert.equal(humanizeMetricUnit('Mbps'), 'Mbps');
assert.equal(humanizeMetricUnit('kbps'), 'kbps');
assert.equal(humanizeMetricUnit(''), '');
assert.equal(humanizeMetricUnit(null), '');
// Fallback path: unknown unit — return with a leading space so
// "12 widgets" reads better than "12widgets".
assert.equal(humanizeMetricUnit('widgets'), ' widgets');
});
test('wanAppRtp*: details.detailsUrl threads the SCM deep-link from appAudio through the check', async () => {
const ctx = mkWanCtx({});
ctx.sdwanData.appAudio = {
appId: '1708539371717015196', appName: 'Webex_Calling_RTP',
detailsUrl: 'https://stratacloudmanager.paloaltonetworks.com/insights/operational/sdwan-applications/1708539371717015196/site/16190109915660160/details',
mos: seriesSummary({ values: [1.85, 4.4] }),
loss: seriesSummary({ values: [0.5] }),
jitter: seriesSummary({ values: [0] }),
};
const r = await wanAppRtpMosCheck.run(ctx);
assert.equal(
r.details.detailsUrl,
'https://stratacloudmanager.paloaltonetworks.com/insights/operational/sdwan-applications/1708539371717015196/site/16190109915660160/details',
'detailsUrl must be present in the check result so the renderer can emit the link',
);
// Missing URL on the container → check details have detailsUrl:null,
// so the renderer's conditional cleanly drops the link row.
delete ctx.sdwanData.appAudio.detailsUrl;
const r2 = await wanAppRtpMosCheck.run(ctx);
assert.equal(r2.details.detailsUrl, null,
'missing container URL propagates as null (renderer no-ops on that)');
});
test('wanAppRtp*: details.unit is the humanized display unit (not raw Prisma "percentage")', async () => {
// Regression for the "11.83percentage" rendering bug — the raw
// API unit string ("percentage", "milliseconds") is unreadable
// when concatenated to a value. The check-args unit ('%', 'ms')
// MUST override the raw unit in the details payload so the
// renderer displays "11.83%" not "11.83percentage".
const ctx = mkAppCtx({
loss: seriesSummary({ values: [11.83], unit: 'percentage' }),
});
const r = await wanAppRtpLossCheck.run(ctx);
assert.equal(r.details.unit, '%', 'display unit humanized in details');
assert.equal(r.details.rawUnit, 'percentage',
'raw Prisma unit retained for debugging (as rawUnit)');
});