collabSupport/tests/voiceDiagRenderer.test.js
Joseph McQueen 1117be40cc Format chat footers in DISPLAY_TIMEZONE instead of UTC.
Docker hosts default to UTC, so bare toLocaleTimeString() showed
wrong "Last checked" times in avstatus and other commands. Add
formatDisplayTime() (default America/New_York, overridable via
DISPLAY_TIMEZONE) and use it across renderers and command footers.
2026-07-21 14:41:26 -04:00

455 lines
18 KiB
JavaScript
Raw Permalink 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.

// Unit tests for services/renderers/voiceDiagRenderer.js. Renderer
// is pure — no I/O — so the tests just assert on the produced
// markdown string. The interesting cases are:
//
// - severity buckets appear in the right order
// - the OK bucket is hidden by default and shown under `detailed`
// - the fixable-issues footer counts and lists only remediable
// results (an OK check with a remediation still doesn't count,
// since we filter status !== 'ok')
// - the details block is only rendered under `detailed`
// - empty result list yields a benign single-line message
import test from 'node:test';
import assert from 'node:assert/strict';
import { renderVoiceDiagMarkdown } from '../services/renderers/voiceDiagRenderer.js';
const R = (id, status, message, remediation = null, details = null, label = null) => ({
id,
label: label || `Check ${id}`,
status,
message,
details,
remediation,
});
const REMEDIATION = {
action: 'disable_dnd',
title: 'Disable DND',
summary: 'Turn DND off.',
payload: {},
};
test('renderer: empty results → benign message', () => {
const md = renderVoiceDiagMarkdown([], { storeNum: '12345' });
assert.match(md, /Voice Diagnostic - Store 12345/);
assert.match(md, /No checks were executed/);
});
test('renderer: header includes personLabel + email when provided', () => {
const md = renderVoiceDiagMarkdown([], {
storeNum: '12345',
personLabel: 'Store 12345',
email: 'ae12345@ae.com',
});
assert.match(md, /user: Store 12345 — ae12345@ae\.com/);
});
test('renderer: mixed severities render in error → warn → skipped order (default hides ok)', () => {
const results = [
R('c1', 'ok', 'all good'),
R('c2', 'warn', 'watch this', REMEDIATION),
R('c3', 'error', 'boom'),
R('c4', 'skipped', '403 missing scope'),
R('c5', 'ok', 'also good'),
];
const md = renderVoiceDiagMarkdown(results, { storeNum: '99' });
const orderIdx = ['**ERRORS**', '**WARNINGS**', '**SKIPPED**'].map((h) => md.indexOf(h));
assert.ok(orderIdx.every((i) => i > -1), `all severity headings present, got ${orderIdx}`);
assert.ok(orderIdx[0] < orderIdx[1], 'ERRORS before WARNINGS');
assert.ok(orderIdx[1] < orderIdx[2], 'WARNINGS before SKIPPED');
assert.equal(md.includes('**OK**'), false, 'OK bucket hidden by default');
assert.match(md, /OK check\(s\) hidden — pass `detailed`/);
});
test('renderer: summary line reports every bucket count', () => {
const results = [
R('c1', 'ok', 'a'),
R('c2', 'warn', 'b'),
R('c3', 'warn', 'c'),
R('c4', 'error', 'd'),
R('c5', 'skipped', 'e'),
R('c6', 'ok', 'f'),
R('c7', 'ok', 'g'),
];
const md = renderVoiceDiagMarkdown(results, { storeNum: '99' });
assert.match(md, /Errors \(1\) · Warnings \(2\) · Skipped \(1\) · OK \(3\)/);
});
test('renderer: detailed mode surfaces OK bucket + details block', () => {
const results = [
R('c1', 'ok', 'all good', null, { enabled: false, mwiEnabled: true }),
];
const md = renderVoiceDiagMarkdown(results, { storeNum: '99', detailed: true });
assert.match(md, /\*\*OK\*\*/);
assert.match(md, /enabled: false, mwiEnabled: true/);
});
test('renderer: fixable footer counts non-ok results with a remediation only', () => {
const results = [
R('c1', 'warn', 'w1', REMEDIATION),
R('c2', 'error', 'e1', REMEDIATION),
R('c3', 'ok', 'o1', REMEDIATION), // OK w/ remediation should NOT count
R('c4', 'warn', 'w2'), // warn w/o remediation should NOT count
];
const md = renderVoiceDiagMarkdown(results, { storeNum: '99' });
assert.match(md, /Fixable issues \(2\)/);
const footerLineCount = (md.match(/^- Check c[12]: Disable DND/gm) || []).length;
assert.equal(footerLineCount, 2, 'footer lists exactly the fixable ones');
});
test('renderer: no fixable-footer emitted when nothing is fixable', () => {
const results = [
R('c1', 'warn', 'no fix'),
R('c2', 'error', 'no fix'),
];
const md = renderVoiceDiagMarkdown(results, { storeNum: '99' });
assert.equal(md.includes('Fixable issues'), false);
});
test('renderer: emitFooter=false suppresses trailing timestamp', () => {
const md = renderVoiceDiagMarkdown([R('c1', 'ok', 'good')], {
storeNum: '99',
detailed: true,
emitFooter: false,
});
assert.equal(md.includes('Last checked'), false);
});
test('renderer: emitFooter=true (default) adds a display-timezone timestamp line', () => {
const md = renderVoiceDiagMarkdown([R('c1', 'ok', 'good')], {
storeNum: '99',
detailed: true,
});
assert.match(md, /_Last checked: .+ (AM|PM) [A-Z]{2,5}_/);
});
test('renderer: details values — arrays truncated past 3 items, nested objects JSON-ified', () => {
const results = [
R('c1', 'warn', 'x', null, {
long: ['a', 'b', 'c', 'd', 'e'],
short: ['a', 'b'],
nested: { foo: 'bar', n: 1 },
nada: null,
}),
];
const md = renderVoiceDiagMarkdown(results, { storeNum: '99', detailed: true });
assert.match(md, /long: \[a, b, c, …\+2\]/);
assert.match(md, /short: \[a, b\]/);
assert.match(md, /nested: \{"foo":"bar","n":1\}/);
assert.match(md, /nada: —/);
});
test('renderer: --detail off does not include details even when present', () => {
const results = [
R('c1', 'warn', 'x', null, { foo: 'bar' }),
];
const md = renderVoiceDiagMarkdown(results, { storeNum: '99' });
assert.equal(md.includes('foo: bar'), false);
});
test('renderer: skipped bucket shown even without detail', () => {
const results = [
R('c1', 'skipped', '403 missing scope'),
];
const md = renderVoiceDiagMarkdown(results, { storeNum: '99' });
assert.match(md, /\*\*SKIPPED\*\*/);
assert.match(md, /403 missing scope/);
});
// ─── Shape-aware detail formatters ──────────────────────────────────
test('renderer: perLink shape → clean per-link list with verdict icons + threshold + roll-up', () => {
const results = [
R('wanLatency', 'ok', 'All 3 paths ok', null, {
total: 3, ok: 3, warn: 0, error: 0,
warnThresh: 150, errorThresh: 400,
standardLabel: 'warn > 150ms, error > 400ms',
perLink: [
{ link: 'Inet1-00782', value: 22.2, interfaceId: '1', verdict: 'ok' },
{ link: 'Inet2-00782', value: 13.5, interfaceId: '2', verdict: 'ok' },
{ link: '5G-LTE-00782', value: 52.4, interfaceId: '3', verdict: 'ok' },
],
}),
];
const md = renderVoiceDiagMarkdown(results, { storeNum: '782', detailed: true });
// Clean per-link list — one line per link with verdict icon
assert.match(md, /Threshold: warn > 150ms, error > 400ms/);
assert.match(md, /Per link:/);
assert.match(md, /✅ Inet1-00782: 22\.2/);
assert.match(md, /✅ Inet2-00782: 13\.5/);
assert.match(md, /✅ 5G-LTE-00782: 52\.4/);
assert.match(md, /Roll-up: 3 total · 3 ok · 0 warn · 0 error/);
// Must NOT dump the raw perLink JSON in a stringified form.
assert.equal(md.includes('"link":'), false);
assert.equal(md.includes('interfaceId'), false);
});
test('renderer: perLink shape — surfaces per-link warn/error icons', () => {
const results = [
R('wanLatency', 'warn', '1 path in warning', null, {
total: 3, ok: 2, warn: 1, error: 0,
standardLabel: 'warn > 150ms, error > 400ms',
perLink: [
{ link: 'Inet1', value: 22, verdict: 'ok' },
{ link: 'Inet2', value: 175, verdict: 'warn' },
{ link: '5G-LTE', value: 450, verdict: 'error' },
],
}),
];
const md = renderVoiceDiagMarkdown(results, { storeNum: '782', detailed: true });
assert.match(md, /✅ Inet1: 22/);
assert.match(md, /⚠️ Inet2: 175/);
assert.match(md, /❌ 5G-LTE: 450/);
});
test('renderer: link-state shape → up/down/unknown roll-up + offender list', () => {
const results = [
R('wanLinkState', 'error', '1 WAN path down', null, {
total: 3, up: 2, down: 1, unknown: 0,
offenders: ['5G-LTE-00782'],
unknownLabels: [],
}),
];
const md = renderVoiceDiagMarkdown(results, { storeNum: '782', detailed: true });
assert.match(md, /Roll-up: 3 total · 2 up · 1 down · 0 unknown/);
assert.match(md, /Down: 5G-LTE-00782/);
// No stringified offenders array
assert.equal(md.includes('"offenders"'), false);
});
test('renderer: alarm shape → counts one-liner + recent alarm list (raw code fallback)', () => {
const results = [
R('wanAlarms', 'warn', '2 major alarms in last hour', null, {
critical: 0, major: 2, minor: 1,
recentSamples: [
{ type: 'NETWORK_ANYNETLINK_DOWN', severity: 'major' },
{ code: 'DEVICE_HB_MISSED', severity: 'minor' },
],
}),
];
const md = renderVoiceDiagMarkdown(results, { storeNum: '782', detailed: true });
assert.match(md, /Counts: 🔴 0 critical · 🟠 2 major · 🟡 1 minor/);
assert.match(md, /Recent:/);
assert.match(md, /NETWORK_ANYNETLINK_DOWN \(major\)/);
assert.match(md, /DEVICE_HB_MISSED \(minor\)/);
});
test('renderer: alarm shape → new byCategory + humanized rollup format', () => {
const results = [
R('wanAlarms', 'warn', '20 major alarms in the last 1d — most common: SD-WAN overlay tunnel down (×20, latest 3h ago). These affect SD-WAN overlay/VPN tunnels between sites; physical WAN paths are all up per the Link State check.', null, {
critical: 0, major: 20, minor: 0,
window: '1d',
byCategory: { overlay: 20, physical: 0, device: 0, other: 0, total: 20 },
recentSamples: [
{
code: 'NETWORK_ANYNETLINK_DOWN',
humanized: 'SD-WAN overlay tunnel down',
category: 'overlay',
severity: 'major',
count: 20,
age: '3h ago',
},
],
}),
];
const md = renderVoiceDiagMarkdown(results, { storeNum: '782', detailed: true });
// Counts line still present.
assert.match(md, /Counts: 🔴 0 critical · 🟠 20 major · 🟡 0 minor/);
// Category breakdown appears when byCategory is populated.
assert.match(md, /Category: 20 overlay\/VPN/);
// Humanized label first, raw code in backticks, count, and age.
assert.match(md, /SD-WAN overlay tunnel down/);
assert.match(md, /`NETWORK_ANYNETLINK_DOWN`/);
assert.match(md, /×20/);
assert.match(md, /3h ago/);
});
test('renderer: healthscore shape → breakdown only (value already in message)', () => {
const results = [
R('wanHealthscore', 'ok', 'Healthscore 100/100 (>=80).', null, {
value: 100, warnThresh: 80, errorThresh: 60, breakdown: {},
}),
];
const md = renderVoiceDiagMarkdown(results, { storeNum: '782', detailed: true });
// Breakdown is empty → no useless "- value: 100, warnThresh: 80..." line
assert.equal(md.includes('warnThresh'), false, 'threshold constants already in the message');
assert.equal(md.includes('errorThresh'), false);
});
test('renderer: healthscore shape → surfaces breakdown when populated', () => {
const results = [
R('wanHealthscore', 'warn', 'Score 75', null, {
value: 75, warnThresh: 80, errorThresh: 60,
breakdown: { link_health: 60, device_health: 85 },
}),
];
const md = renderVoiceDiagMarkdown(results, { storeNum: '782', detailed: true });
assert.match(md, /Breakdown: link_health: 60 · device_health: 85/);
});
test('renderer: WAN window banner shown when wanWindowMinutes provided AND a wan* check is present', () => {
const results = [
R('wanHealthscore', 'ok', 'ok', null, { value: 100, warnThresh: 80, errorThresh: 60 }),
];
const md = renderVoiceDiagMarkdown(results, {
storeNum: '782', detailed: false, wanWindowMinutes: 1440,
});
assert.match(md, /WAN window: 1d/);
});
test('renderer: WAN window banner hidden when no wan* check runs', () => {
const results = [R('dnd', 'ok', 'ok')];
const md = renderVoiceDiagMarkdown(results, {
storeNum: '782', wanWindowMinutes: 60,
});
assert.equal(md.includes('WAN window'), false,
'no need to advertise a WAN window when no WAN check ran');
});
test('renderer: WAN window banner formats: 15m / 1h / 6h / 1d / 7d', () => {
const wanResult = [
R('wanLatency', 'ok', 'ok', null, {
total: 1, ok: 1, warn: 0, error: 0,
perLink: [{ link: 'A', value: 20, verdict: 'ok' }],
}),
];
const cases = [
{ min: 15, expect: 'WAN window: 15m' },
{ min: 60, expect: 'WAN window: 1h' },
{ min: 360, expect: 'WAN window: 6h' },
{ min: 1440, expect: 'WAN window: 1d' },
{ min: 10080, expect: 'WAN window: 7d' }, // new default
{ min: 45, expect: 'WAN window: 45m' },
];
for (const c of cases) {
const md = renderVoiceDiagMarkdown(wanResult, { storeNum: '1', wanWindowMinutes: c.min });
assert.ok(md.includes(c.expect), `${c.min} minutes → "${c.expect}", got:\n${md}`);
}
});
// ─── Per-app audio details renderer ─────────────────────────────────
//
// renderAppAudioDetails is triggered by shape detection on
// {appName, worst, validSamples} — the fixture below carries all
// three so the shape-aware branch fires (rather than the JSON dump
// fallback which is what we're guarding against). appName is asserted
// verbatim from the fixture (not hardcoded to a specific app) — see
// wanAppRtpMos.js for the tenant-configurable contract.
test('renderer: per-app audio MOS details render worst-window + threshold + range (Webex_Calling_RTP)', () => {
const wanResult = R(
'wanAppRtpMos',
'error',
'Actual voice traffic (Webex_Calling_RTP) worst-window MOS: 1.60 < 3.5 (avg 4.27 over 33 valid samples)…',
null,
{
appName: 'Webex_Calling_RTP',
worst: 1.60, avg: 4.27, min: 1.60, max: 4.41, p95: 4.28,
samples: 55, validSamples: 33, interval: '5min',
warnThresh: 4.0, errorThresh: 3.5,
standardLabel: 'warn < 4, error < 3.5',
unit: '',
badSampleCount: 4, badSamplePct: 12,
},
'SD-WAN Voice Traffic MOS (worst window)',
);
const md = renderVoiceDiagMarkdown([wanResult], { storeNum: '127', detailed: true });
// The app name is threaded from the fixture (appAudio.appName) — the
// renderer must not hardcode "rtp-base" so tenants using
// Webex_Calling_RTP, MS_Teams_RTP, etc. render correctly.
assert.match(md, /App: Webex_Calling_RTP \(voice traffic, DPI\)/);
assert.match(md, /Threshold: warn < 4, error < 3\.5/);
assert.match(md, /Worst window: \*\*1\.6\*\*/);
assert.match(md, /Avg: 4\.27/);
assert.match(md, /p95: 4\.28/);
assert.match(md, /Range: 1\.6 4\.41 across 33\/55 samples @ 5min/);
assert.match(md, /Time in warn\/error: 4 samples/);
// Absence check — the fallback JSON key:value dump must NOT appear.
assert.doesNotMatch(md, /"values":/, 'shape-aware formatter must not dump raw JSON');
});
test('renderer: per-app audio details fall back to "voice" when appName absent (defensive)', () => {
// If a fixture / integration test forgets to set appName (or a
// future refactor drops it), the renderer must not crash or render
// "undefined" — fall back to the generic "voice" label.
const wanResult = R(
'wanAppRtpMos', 'ok', 'ok',
null,
{
appName: null,
worst: 4.4, avg: 4.5, min: 4.4, max: 4.6, p95: 4.5,
samples: 12, validSamples: 12, interval: '5min',
warnThresh: 4.0, errorThresh: 3.5,
unit: '', badSampleCount: 0, badSamplePct: 0,
},
'SD-WAN Voice Traffic MOS (worst window)',
);
const md = renderVoiceDiagMarkdown([wanResult], { storeNum: '127', detailed: true });
assert.match(md, /App: voice \(voice traffic, DPI\)/,
'null appName must fall back to generic "voice" label');
});
test('renderer: per-app audio details renders "View in Prisma UI" deep link when detailsUrl is set', () => {
const wanResult = R(
'wanAppRtpMos', 'error', 'msg',
null,
{
appName: 'Webex_Calling_RTP',
worst: 1.85, avg: 3.55, min: 1.85, max: 4.41, p95: 4.28,
samples: 288, validSamples: 288, interval: '5min',
warnThresh: 4.0, errorThresh: 3.5,
standardLabel: 'warn < 4, error < 3.5',
unit: '',
badSampleCount: 42, badSamplePct: 15,
detailsUrl: 'https://stratacloudmanager.paloaltonetworks.com/insights/operational/sdwan-applications/1708539371717015196/site/16190109915660160/details',
},
'SD-WAN Voice Traffic MOS (worst window)',
);
const md = renderVoiceDiagMarkdown([wanResult], { storeNum: '127', detailed: true });
assert.match(
md,
/\[View in Prisma UI\]\(https:\/\/stratacloudmanager\.paloaltonetworks\.com\/insights\/operational\/sdwan-applications\/1708539371717015196\/site\/16190109915660160\/details\)/,
'deep link must render as a markdown link so Webex chats it as a clickable URL',
);
});
test('renderer: per-app audio details omits Prisma link when detailsUrl is not set', () => {
const wanResult = R(
'wanAppRtpMos', 'error', 'msg',
null,
{
appName: 'Webex_Calling_RTP',
worst: 1.85, avg: 3.55, min: 1.85, max: 4.41, p95: 4.28,
samples: 288, validSamples: 288, interval: '5min',
warnThresh: 4.0, errorThresh: 3.5,
unit: '',
badSampleCount: 42, badSamplePct: 15,
},
'SD-WAN Voice Traffic MOS (worst window)',
);
const md = renderVoiceDiagMarkdown([wanResult], { storeNum: '127', detailed: true });
assert.doesNotMatch(md, /View in Prisma UI/);
});
test('renderer: per-app audio details — clean window says "0 samples (clean throughout window)"', () => {
const wanResult = R(
'wanAppRtpLoss', 'ok', 'ok',
null,
{
appName: 'Webex_Calling_RTP',
worst: 0, avg: 0, min: 0, max: 0, p95: 0,
samples: 288, validSamples: 288, interval: '5min',
warnThresh: 5, errorThresh: 15,
standardLabel: 'warn > 5%, error > 15%',
unit: '%',
badSampleCount: 0, badSamplePct: 0,
},
'SD-WAN Voice Traffic Packet Loss (worst window)',
);
const md = renderVoiceDiagMarkdown([wanResult], { storeNum: '127', detailed: true });
assert.match(md, /Time in warn\/error: 0 samples \(clean throughout window\)/);
});