collabSupport/tests/renderers.wan.test.js
jmcqueen 26ae704dff Tighten WAN tunnel and alarm rendering for actionable follow-ups.
Show overall tunnel status with downs only, list the last five alarms
with times, and note when overlay/physical alarms may explain bad voice DPI.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-28 09:16:03 -04:00

374 lines
15 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');
});
test('renderer: overlay tunnels show overall status and only list downs', () => {
const md = renderWanDiagnosticsMarkdown(baseData({
tunnels: [
{ id: 't1', peerLabel: 'CG00001-HUB', up: false, state: 'down', recentAlarm: true },
{ id: 't2', peerLabel: 'CG00002-HUB', up: true, state: 'up' },
{ id: 't3', peerLabel: 'Cologix', up: true, state: 'up' },
],
}), { storeNum: '782', footer: false });
assert.match(md, /\*\*Overlay tunnels\*\* — ❌ 1 down \/ 3/);
assert.match(md, /CG00001-HUB/);
assert.doesNotMatch(md, /CG00002-HUB/);
assert.doesNotMatch(md, /Cologix/);
assert.match(md, /overlay down while physical/);
});
test('renderer: all-up tunnels collapse to summary only', () => {
const md = renderWanDiagnosticsMarkdown(baseData({
tunnels: [
{ id: 't1', peerLabel: 'Warrendale', up: true, state: 'up' },
{ id: 't2', peerLabel: 'Cologix', up: true, state: 'up' },
],
}), { storeNum: '782', footer: false });
assert.match(md, /\*\*Overlay tunnels\*\* — ✅ all 2 up/);
assert.doesNotMatch(md, /Warrendale/);
assert.doesNotMatch(md, /Cologix/);
});
test('renderer: all-unknown tunnels collapse to one inventory note', () => {
const md = renderWanDiagnosticsMarkdown(baseData({
tunnels: Array.from({ length: 5 }, (_, i) => ({
id: `t${i}`, peerLabel: 'peer', up: null, state: 'unknown',
})),
}), { storeNum: '782', footer: false });
assert.match(md, /status unknown/);
assert.match(md, /Tunnel inventory returned 5/);
assert.doesNotMatch(md, /\? peer — unknown/);
});
test('renderer: alarms include most-recent list with times and voice correlation', () => {
const now = Date.now();
const md = renderWanDiagnosticsMarkdown(baseData({
appAudio: {
appName: 'Webex_Calling_RTP',
mos: { avg: 4.0, min: 1.0, max: 4.4, samples: 10, validSamples: 10, interval: '5min' },
loss: { avg: 4, min: 0, max: 68, samples: 10, validSamples: 10, interval: '5min' },
jitter: { avg: 1, min: 0, max: 2, samples: 10, validSamples: 10, interval: '5min' },
bandwidth: { avg: 0.5, min: 0.1, max: 1, samples: 10, validSamples: 10, interval: '5min' },
},
alarms: {
last1h: { critical: 0, major: 2, minor: 0 },
samples: [
{
code: 'SITE_CONNECTIVITY_DEGRADED',
severity: 'major',
message: 'degraded',
ts: new Date(now - 9 * 3600 * 1000).toISOString(),
},
{
code: 'NETWORK_VPNLINK_DOWN',
severity: 'major',
message: 'down',
ts: new Date(now - 10 * 3600 * 1000).toISOString(),
},
],
},
}), { storeNum: '782', footer: false });
assert.match(md, /Most recent:/);
assert.match(md, /Site connectivity degraded/i);
assert.match(md, /may affect voice/);
assert.match(md, /Voice DPI looks degraded/);
assert.match(md, /may help explain it/);
});
test('renderer: voice DPI byPathType lines', () => {
const md = renderWanDiagnosticsMarkdown(baseData({
appAudio: {
appName: 'Webex_Calling_RTP',
mos: { avg: 4.2, min: 3.8, max: 4.5, samples: 10, validSamples: 10, interval: '5min' },
loss: { avg: 1, min: 0, max: 3, samples: 10, validSamples: 10, interval: '5min' },
jitter: { avg: 10, min: 5, max: 20, samples: 10, validSamples: 10, interval: '5min' },
bandwidth: { avg: 0.5, min: 0.1, max: 1, samples: 10, validSamples: 10, interval: '5min' },
byPathType: {
VPN: {
loss: { avg: 5, min: 1, max: 12, samples: 5, validSamples: 5 },
jitter: { avg: 30, min: 10, max: 45, samples: 5, validSamples: 5 },
},
},
},
}), { storeNum: '782', footer: false });
assert.match(md, /By path type/);
assert.match(md, /\*\*VPN:\*\*/);
});