// 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'); });