collabSupport/tests/renderers.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

281 lines
12 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.

// tests/renderers.wan.test.js
//
// Pure-function coverage for services/renderers/wanDiagnosticsRenderer.js.
// Same style as tests/renderers.test.js — asserts on the returned
// markdown string. No mocking, no HTTP.
import test from 'node:test';
import assert from 'node:assert/strict';
import { renderWanDiagnosticsMarkdown } from '../services/renderers/wanDiagnosticsRenderer.js';
function baseData(overrides = {}) {
return {
storeNum: '782',
site: { id: 'site-1', name: 'CG00782', storeNum: '782', description: '' },
elements: [{ id: 'el-1', name: 'ION-A', model: 'ION1000', connected: true }],
healthscore: { value: 90, breakdown: {} },
links: [
{
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,
},
],
alarms: { last1h: { critical: 0, major: 0, minor: 0 }, samples: [] },
errors: [],
fetchedAt: new Date().toISOString(),
...overrides,
};
}
test('renderer: no site → empty string (caller no-ops)', () => {
const md = renderWanDiagnosticsMarkdown({ site: null, storeNum: '782' });
assert.equal(md, '');
});
test('renderer: null / non-object → empty string', () => {
assert.equal(renderWanDiagnosticsMarkdown(null), '');
assert.equal(renderWanDiagnosticsMarkdown('nope'), '');
});
test('renderer: healthy site — header + site + one path bullet', () => {
const md = renderWanDiagnosticsMarkdown(baseData(), { storeNum: '782' });
assert.match(md, /WAN Diagnostics.*Store 782/);
assert.match(md, /Site.*CG00782/);
assert.match(md, /Healthscore.*90/);
assert.match(md, /wan1/);
assert.match(md, /MPLS/);
assert.match(md, /latency 40ms/);
assert.match(md, /MOS 4\.4/);
});
test('renderer: degraded path — worst path floats to top', () => {
const data = baseData({
links: [
{ interfaceId: 'a', interfaceName: 'good', up: true, latencyMs: 30, jitterMs: 3, lossPct: 0, mos: 4.5 },
{ interfaceId: 'b', interfaceName: 'bad', up: true, latencyMs: 500, jitterMs: 60, lossPct: 5, mos: 3.0 },
],
});
const md = renderWanDiagnosticsMarkdown(data, { storeNum: '782' });
const goodIdx = md.indexOf('good');
const badIdx = md.indexOf('bad');
assert.ok(badIdx >= 0 && goodIdx >= 0);
assert.ok(badIdx < goodIdx, 'bad link should render before good link');
});
test('renderer: all links down — down status labelled per link', () => {
const data = baseData({
links: [
{ interfaceId: 'a', interfaceName: 'mpls', up: false, latencyMs: null, jitterMs: null, lossPct: null, mos: null },
{ interfaceId: 'b', interfaceName: 'broadband', up: false, latencyMs: null, jitterMs: null, lossPct: null, mos: null },
],
});
const md = renderWanDiagnosticsMarkdown(data, { storeNum: '782' });
assert.match(md, /mpls.*DOWN/);
assert.match(md, /broadband.*DOWN/);
});
test('renderer: partial-fetch errors surfaced under "Partial fetch"', () => {
const data = baseData({
errors: [
{ scope: 'lqm.latency', message: 'timeout' },
{ scope: 'alarms', message: 'unauthorized' },
],
});
const md = renderWanDiagnosticsMarkdown(data, { storeNum: '782' });
assert.match(md, /Partial fetch/);
assert.match(md, /lqm\.latency.*timeout/);
assert.match(md, /alarms.*unauthorized/);
});
test('renderer: alarms summary emitted when non-zero — sample lines show code, not raw JSON', () => {
const data = baseData({
alarms: {
last1h: { critical: 1, major: 2, minor: 3 },
samples: [
{ code: 'NETWORK_ANYNETLINK_DOWN', message: 'link flapping', severity: 'critical', ts: new Date().toISOString() },
],
},
});
const md = renderWanDiagnosticsMarkdown(data, { storeNum: '782' });
assert.match(md, /Alarms.*1 critical, 2 major, 3 minor/);
assert.match(md, /NETWORK_ANYNETLINK_DOWN/);
// Rendered rollup lines must NEVER include raw JSON blobs. If we
// regress and paste a stringified `info` dict into chat, this catches it.
assert.equal(md.includes('{"vpn_reasons'), false, 'no raw JSON in rendered alarm samples');
assert.equal(md.includes('[object Object]'), false, 'no ugly [object Object] in rendered alarm samples');
});
test('renderer: alarm rollup collapses N identical (code, severity) pairs to one line with ×N', () => {
// Regression: previously each of the 20 NETWORK_ANYNETLINK_DOWN
// events was pasted into chat as its own JSON blob line — noisy
// and unreadable. Rollup by (code, severity) prints one line.
const now = Date.now();
const dupes = Array.from({ length: 20 }, (_, i) => ({
code: 'NETWORK_ANYNETLINK_DOWN',
severity: 'major',
message: 'noise',
ts: new Date(now - i * 1000).toISOString(),
}));
const data = baseData({
alarms: {
last1h: { critical: 0, major: 20, minor: 0 },
samples: dupes,
},
});
const md = renderWanDiagnosticsMarkdown(data, { storeNum: '782' });
// Exactly one line containing the code with the ×20 count marker.
const matches = md.match(/NETWORK_ANYNETLINK_DOWN/g) || [];
assert.equal(matches.length, 1, 'code should appear exactly once after rollup');
assert.match(md, /×20/, 'should print a count marker for the rollup');
});
test('renderer: alarm rollup orders by severity (critical → major → minor)', () => {
const data = baseData({
alarms: {
last1h: { critical: 1, major: 1, minor: 1 },
samples: [
{ code: 'MINOR_CODE', severity: 'minor', ts: 't1' },
{ code: 'MAJOR_CODE', severity: 'major', ts: 't2' },
{ code: 'CRITICAL_CODE', severity: 'critical', ts: 't3' },
],
},
});
const md = renderWanDiagnosticsMarkdown(data, { storeNum: '782' });
const iCrit = md.indexOf('CRITICAL_CODE');
const iMaj = md.indexOf('MAJOR_CODE');
const iMinor = md.indexOf('MINOR_CODE');
assert.ok(iCrit > 0 && iMaj > 0 && iMinor > 0);
assert.ok(iCrit < iMaj, 'critical printed before major');
assert.ok(iMaj < iMinor, 'major printed before minor');
});
test('renderer: no alarms — no alarm section rendered', () => {
const md = renderWanDiagnosticsMarkdown(baseData(), { storeNum: '782' });
// The section header would be "Alarms (last 1h):"; the footer
// mentions wanAlarms as a --only check name which is fine.
assert.equal(md.includes('Alarms (last 1h)'), false);
});
test('renderer: no links — placeholder line', () => {
const md = renderWanDiagnosticsMarkdown(baseData({ links: [] }), { storeNum: '782' });
assert.match(md, /No WAN path metrics available/);
});
test('renderer: healthscore missing → "n/a" with unknown icon', () => {
const md = renderWanDiagnosticsMarkdown(baseData({ healthscore: null }), { storeNum: '782' });
assert.match(md, /Healthscore.*n\/a/);
});
// ─── Per-app "Voice Traffic Quality" section ────────────────────────
function appAudioSummary({ values, unit = '', interval = '5min' } = {}) {
const nums = values.filter((v) => Number.isFinite(v));
const avg = nums.length ? Math.round((nums.reduce((a, b) => a + b, 0) / nums.length) * 100) / 100 : null;
return {
unit, interval,
samples: values.length, validSamples: nums.length,
avg,
min: nums.length ? Math.min(...nums) : null,
max: nums.length ? Math.max(...nums) : null,
p95: nums.length ? nums.sort((a, b) => a - b)[Math.floor(0.95 * nums.length)] || nums[nums.length - 1] : null,
values,
};
}
test('renderer: renders Voice Traffic Quality section when appAudio present with samples', () => {
// Uses Webex_Calling_RTP as the configured app — the renderer must
// pick up appName from the fixture, not hardcode "rtp-base".
const data = baseData({
appAudio: {
appId: '1708539371717015196', appName: 'Webex_Calling_RTP',
mos: appAudioSummary({ values: [3.55, 3.55, 1.85, 4.41], unit: 'count' }),
loss: appAudioSummary({ values: [22, 0, 69, 0], unit: '%' }),
jitter: appAudioSummary({ values: [0, 0, 0], unit: 'ms' }),
bandwidth: appAudioSummary({ values: [0.5, 0.6, 0.4], unit: 'Mbps' }),
},
});
const md = renderWanDiagnosticsMarkdown(data, { storeNum: '127' });
assert.match(md, /Voice Traffic Quality \(Webex_Calling_RTP/,
'section header must reflect the tenant-configured app name');
assert.match(md, /DPI/, 'callout for "real DPI measurement" surfaces');
// Worst-window numbers must appear (that's the whole point).
assert.match(md, /1\.85/, 'worst MOS surfaced');
assert.match(md, /69%/, 'worst loss surfaced');
});
test('renderer: still renders correctly with legacy "rtp-base" appName (backwards-compat)', () => {
// Tenants that stayed on the legacy PRISMA_APP_ID_RTP_BASE env get
// appName='rtp-base' from resolveVoiceAppConfig() — must render
// identically to any other app.
const data = baseData({
appAudio: {
appId: '15932000365560116', appName: 'rtp-base',
mos: appAudioSummary({ values: [4.35], unit: 'count' }),
},
});
const md = renderWanDiagnosticsMarkdown(data, { storeNum: '127' });
assert.match(md, /Voice Traffic Quality \(rtp-base/);
});
test('renderer: omits Voice Traffic Quality section entirely when appAudio is null', () => {
const md = renderWanDiagnosticsMarkdown(baseData(), { storeNum: '782' });
assert.doesNotMatch(md, /Voice Traffic Quality/);
});
test('renderer: omits Voice Traffic Quality section when appAudio has no valid samples', () => {
const empty = appAudioSummary({ values: [null, null] });
const data = baseData({
appAudio: { appId: 'x', appName: 'Webex_Calling_RTP', mos: empty, loss: empty, jitter: empty, bandwidth: empty },
});
const md = renderWanDiagnosticsMarkdown(data, { storeNum: '782' });
assert.doesNotMatch(md, /Voice Traffic Quality/,
'no data across every metric → skip the header entirely rather than show empty rows');
});
test('renderer: MOS row uses worst-lowest label (not worst-highest)', () => {
const data = baseData({
appAudio: {
appId: 'x', appName: 'Webex_Calling_RTP',
mos: appAudioSummary({ values: [4.4, 4.3, 3.9], unit: '' }),
},
});
const md = renderWanDiagnosticsMarkdown(data, { storeNum: '782' });
assert.match(md, /worst \(lowest\)/, 'MOS is lowIsBad — must say "lowest" not "highest"');
});
test('renderer: "View in Prisma UI" deep link renders when appAudio.detailsUrl is present', () => {
const data = baseData({
appAudio: {
appId: '1708539371717015196', appName: 'Webex_Calling_RTP',
detailsUrl: 'https://stratacloudmanager.paloaltonetworks.com/insights/operational/sdwan-applications/1708539371717015196/site/16190109915660160/details',
mos: appAudioSummary({ values: [4.35] }),
},
});
const md = renderWanDiagnosticsMarkdown(data, { storeNum: '127' });
assert.match(
md,
/\[View in Prisma UI\]\(https:\/\/stratacloudmanager\.paloaltonetworks\.com\/insights\/operational\/sdwan-applications\/1708539371717015196\/site\/16190109915660160\/details\)/,
'deep link must be rendered as a markdown link inside the section header',
);
});
test('renderer: no deep link when appAudio.detailsUrl is missing (defensive)', () => {
const data = baseData({
appAudio: {
appId: 'x', appName: 'Webex_Calling_RTP',
mos: appAudioSummary({ values: [4.35] }),
},
});
const md = renderWanDiagnosticsMarkdown(data, { storeNum: '127' });
assert.doesNotMatch(md, /View in Prisma UI/,
'link line should be omitted entirely when the URL is null');
});
test('renderer: footer mentions the per-app --only shortcut', () => {
const md = renderWanDiagnosticsMarkdown(baseData(), { storeNum: '782', footer: true });
assert.match(md, /wanAppRtpMos,wanAppRtpLoss,wanAppRtpJitter/,
'footer should point operators at the per-app checks by id');
});