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

170 lines
6.7 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/);
});