collabSupport/tests/voiceDiagRenderer.test.js
jmcqueen 21fa8f1436 Clean up SD-WAN alarm rendering + explain overlay vs physical
The wanAlarms check was pasting raw Prisma `info` JSON blobs (nested
`vpn_reasons` arrays with element/site/vpnlink ids) into the chat
message field. On a store with 20 NETWORK_ANYNETLINK_DOWN flaps this
produced a wall of unreadable stringified JSON where the actual
signal ("SD-WAN overlay tunnels are flapping") was lost.

Introduces a shared alarmSemantics module that:
  - Buckets each code into overlay / physical / device / other so
    the check + renderer stay consistent
  - Humanizes codes (NETWORK_ANYNETLINK_DOWN → "SD-WAN overlay
    tunnel down") with a Title-Cased fallback for unknown codes
  - Rolls up (code + severity) tuples so 20 identical alarms show as
    a single line with ×20 and a "just now / Nm / Nh / Nd" age

Rewrites wanAlarms.run() to use those helpers + cross-reference the
site's physical link state so operators aren't left wondering why 20
alarms fired while every metric shows green: overlay flaps get a
"physical WAN paths are all up per Link State" clarifier, and
physical alarms point back at the Link State check. The label loses
its hardcoded "(last 1h)" suffix since the alarm window is now
dynamic (defaults to 24h to match the WAN window).

The follow-up renderer used by /phonestatus imports the same helpers
so the two surfaces cannot drift.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 10:19:15 -04:00

330 lines
12 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.

// 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 an ISO timestamp line', () => {
const md = renderVoiceDiagMarkdown([R('c1', 'ok', 'good')], {
storeNum: '99',
detailed: true,
});
assert.match(md, /_Last checked: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
});
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', () => {
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: 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}`);
}
});