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