collabSupport/tests/renderers.test.js
Joseph McQueen 96b26a5aca DECT relay Phase 1: WSS hub + agent + /phonestatus follow-up
The bot runs in the public cloud and can't reach the 10.x/8 network
where DBS-210 bases live. This phase adds a data-center-resident relay
agent that dials outbound over WSS to the bot, and lets /phonestatus
post a follow-up message with per-base health after its main output
has already shipped.

Bot side (services/):
- dectRelayHub.js: WebSocket upgrade handler on /dect-relay/ws with
  bearer-token auth (constant-time compare, header + Sec-WebSocket-
  Protocol fallback for header-stripping proxies). Promise-based RPC
  API with per-call timeouts, mid-flight-disconnect rejection, and
  clean replacement of a stale agent socket when a newer one connects.
- dectDiscovery.js: pure filter that turns a phoneService result into
  a list of reachable bases. Enforces the "must be on 10.0.0.0/8"
  guardrail per requirements, dedups by IP + MAC, prefers Meraki-live
  IP over Webex-cached IP.
- dectCollectorService.js: fan-out layer over the hub. collectAll()
  runs one RPC per base in parallel with per-base error isolation —
  one bad base never fails the batch.

Phone-status integration:
- Renderer gets a dectFollowUpBaseCount opt that emits an italic
  "diagnostics loading for N base(s)..." hint inside the DECT section
  of the main message.
- New exported renderDectDiagnosticsMarkdown() renders the follow-up
  message: healthy/warning icon per base, uptime + firmware summary,
  structured Power Loss reboot line, and per-base failure hints (e.g.
  "relay accepted the request but the base did not respond in time").
- commands/phoneStatus.js discovers reachable bases synchronously
  (pure), sends the main message, then fires collectAll() and posts
  the follow-up as a separate message. Failures logged, never thrown
  back to the user.
- Chat only: HTTP callers keep their single-message contract.

Agent side (dect-relay-agent/):
- Standalone Node process with its own package.json (only ws, axios,
  dotenv). Reuses the shared integrations/cisco-dect/{client,probes,
  statusXml}.js modules from the parent workspace so there's no code
  duplication.
- Auto-reconnect with exponential backoff + jitter.
- Dispatches collect / reboot / force-reboot / reboot-chain /
  force-reboot-chain / factory-reset / reconfigure-tree.
- DECT admin credentials live ONLY on the agent (never on the bot).
  Shared bearer token gates the WSS handshake.
- README.md covers install, config, wire protocol, and safety model.

Env / infra:
- .env.example: adds DECT_RELAY_AGENT_TOKEN + optional DECT_RELAY_PATH
  and DECT_COLLECT_TIMEOUT_MS. Reframes DECT_TEST_* as the local-dev
  test harness rather than the production path.
- index.js: captures the http.Server from app.listen() and attaches
  the relay hub when DECT_RELAY_AGENT_TOKEN is set; graceful shutdown
  now closes the hub so in-flight RPCs get rejected cleanly.
- Adds "ws" to bot dependencies.

Tests (99 -> 113):
- tests/dectDiscovery.test.js: 13 cases covering the 10.x guardrail,
  MAC normalization, IP source preference, dedup, and warning shape.
- tests/dectRelayHub.test.js: 14 integration cases using a real
  ws pair on an ephemeral 127.0.0.1 port — auth (missing / wrong /
  correct via header / correct via protocol fallback), hello frame,
  RPC round-trip with correlation, agent error surfacing, concurrent
  out-of-order replies, timeout, mid-flight disconnect, replacement
  of a stale socket, and execAction routing.
- tests/renderers.test.js: 8 new cases for the DECT-follow-up loading
  hint (plural / singular / off) and the diagnostics renderer (empty,
  healthy, warning, power-loss dedup, active RTP, error hint, footer).
2026-07-02 17:03:32 -04:00

407 lines
16 KiB
JavaScript

// Golden-ish tests for services/renderers/{phone,av}StatusRenderer.js
//
// True byte-for-byte golden strings aren't practical because both
// renderers call `simpleTimeAgo` (which uses `Date.now()`). Instead we
// assert on structural properties AND on the exact assembly of key
// lines — enough to catch a regression like "we lost the Meraki
// `[Meraki↗](url)` link" or "the header format changed" but robust to
// time-of-day drift.
import test from 'node:test';
import assert from 'node:assert/strict';
import {
renderPhoneStatusMarkdown,
renderDectDiagnosticsMarkdown,
} from '../services/renderers/phoneStatusRenderer.js';
import { renderAvStatusMarkdown } from '../services/renderers/avStatusRenderer.js';
// Timestamp exactly 3 hours in the past — makes `simpleTimeAgo`
// deterministic to "3 hours ago" for the duration of this test run.
const threeHrAgo = () => new Date(Date.now() - 3 * 60 * 60 * 1000).toISOString();
test('phone renderer: header and empty-store message', () => {
const md = renderPhoneStatusMarkdown({}, { storeNum: '782' });
assert.match(md, /^\*\*Phone Status - Store 782\*\*/);
assert.match(md, /No phones or DECT basestations found/);
});
test('phone renderer: full desk-phone entry preserves Meraki link', () => {
const data = {
phones: {
data: [{
displayName: 'PHONE 782-1',
status: 'connected',
lastSeen: threeHrAgo(),
firmware: '12.0.4',
serial: 'ABC12345',
meraki: {
switchName: 'STORE-782-SW1',
status: 'Online',
port: '17',
vlan: '20',
ip: '10.1.1.5',
lastSeen: threeHrAgo(),
clientUrl: 'https://n123.meraki.com/example/manage/clients/abc/overview',
usage: { sent: 1024, recv: 4096 },
},
}],
},
telephonyProfile: { timeZone: 'America/New_York' },
locationMainNumber: '+14125550100',
};
const md = renderPhoneStatusMarkdown(data, { storeNum: '782', footer: false });
// Header lines the poller cares about
assert.match(md, /\*\*Timezone:\*\* America\/New_York/);
assert.match(md, /\*\*PhoneNumber:\*\* \+14125550100/);
// Bold device name
assert.match(md, /\*\*PHONE 782-1\*\*/);
// Firmware / serial line
assert.match(md, /FW: 12\.0\.4 • Serial: ABC12345/);
// Meraki port info + link — the whole reason we ripped out the codeBlock
assert.match(md, /\*\*STORE-782-SW1\*\*/);
assert.match(md, /\[Meraki↗\]\(https:\/\/n123\.meraki\.com\/example\/manage\/clients\/abc\/overview\)/);
// Data usage
assert.match(md, /Data \(recent\): 1 KB sent \/ 4 KB recv/);
// No footer when opts.footer is false — Jira uses this mode
assert.doesNotMatch(md, /Last checked/);
});
test('phone renderer: footer appears when opts.footer=true (default) on non-empty data', () => {
// The empty-store branch legitimately returns early with no footer
// — that matches the chat handler's historical behavior. Feed a
// minimal populated fixture to exercise the footer path.
const data = {
phones: {
data: [{ displayName: 'X', status: 'connected', lastSeen: threeHrAgo() }],
},
};
const md = renderPhoneStatusMarkdown(data, { storeNum: '782' });
assert.match(md, /Last checked:/);
});
test('phone renderer: detailed mode reveals SIP details', () => {
const data = {
phones: {
data: [{
displayName: 'PHONE 782-2',
status: 'connected',
lastSeen: threeHrAgo(),
primarySipUrl: 'sip:782-2@aeo2go.webex.com',
sipUrls: ['sip:a@x', 'sip:b@x', 'sip:c@x'],
}],
},
};
const compact = renderPhoneStatusMarkdown(data, { storeNum: '782', detailed: false, footer: false });
const detailed = renderPhoneStatusMarkdown(data, { storeNum: '782', detailed: true, footer: false });
assert.doesNotMatch(compact, /SIP: sip:782-2/);
assert.match(detailed, /SIP: sip:782-2@aeo2go\.webex\.com/);
assert.match(detailed, /Alt SIPs: sip:a@x, sip:b@x…/);
});
// ─────────────────────────────────────────────────────────────
// IGMP snooping / DECT-safe multicast warning line
// ─────────────────────────────────────────────────────────────
test('phone renderer: multicast warning line includes every deviation kind', () => {
const data = {
dectBasestations: [
{ mac: 'aa:bb:cc:dd:ee:ff', meraki: { status: 'Online' } },
],
dectHandsets: [],
multicast: {
needsFix: true,
defaultSnoopOn: true,
defaultFloodOff: true,
deviatingOverrides: [{}, {}],
},
};
const md = renderPhoneStatusMarkdown(data, { storeNum: '782', footer: false });
assert.match(md, /\*\*Multicast:\*\* ⚠️/);
assert.match(md, /IGMP snoop=ON default/);
assert.match(md, /flood-unknown=OFF default/);
assert.match(md, /2 switch override\(s\) deviate/);
assert.match(md, /may disrupt DECT/);
});
test('phone renderer: multicast line names only the snoop deviation when that is the only problem', () => {
const data = {
dectBasestations: [
{ mac: 'aa:bb:cc:dd:ee:ff', meraki: { status: 'Online' } },
],
dectHandsets: [],
multicast: {
needsFix: true,
defaultSnoopOn: true,
defaultFloodOff: false,
deviatingOverrides: [],
},
};
const md = renderPhoneStatusMarkdown(data, { storeNum: '782', footer: false });
assert.match(md, /\*\*Multicast:\*\* ⚠️ IGMP snoop=ON default — may disrupt DECT/);
assert.doesNotMatch(md, /flood-unknown/);
assert.doesNotMatch(md, /switch override/);
});
test('phone renderer: multicast line is ABSENT when needsFix is false', () => {
const data = {
dectBasestations: [
{ mac: 'aa:bb:cc:dd:ee:ff', meraki: { status: 'Online' } },
],
dectHandsets: [],
multicast: {
needsFix: false,
defaultSnoopOn: false,
defaultFloodOff: false,
deviatingOverrides: [],
},
};
const md = renderPhoneStatusMarkdown(data, { storeNum: '782', footer: false });
assert.doesNotMatch(md, /\*\*Multicast:\*\*/);
});
test('phone renderer: multicast line is ABSENT when there are no DECT basestations', () => {
// Even if needsFix is true, the warning lives INSIDE the DECT section
// which itself is gated on dectBasestations.length > 0. Stores with
// no DECT get no multicast noise — it's simply not their problem.
const data = {
phones: {
data: [{ displayName: 'DESK', status: 'connected', lastSeen: threeHrAgo() }],
},
dectBasestations: [],
multicast: {
needsFix: true,
defaultSnoopOn: true,
defaultFloodOff: true,
deviatingOverrides: [{}],
},
};
const md = renderPhoneStatusMarkdown(data, { storeNum: '782', footer: false });
assert.doesNotMatch(md, /\*\*Multicast:\*\*/);
});
// ─────────────────────────────────────────────────────────────
test('av renderer: header always says (Mode: detailed)', () => {
const md = renderAvStatusMarkdown({}, { storeNum: '782', footer: false });
assert.match(md, /^\*\*Device Status - Store 782\*\* \(Mode: detailed\)/);
});
test('av renderer: MDM entry with Meraki wired client preserves link', () => {
const data = {
mdm: {
data: [{
friendlyName: 'STORE-782-KIOSK-1',
lastSeen: threeHrAgo(),
meraki: {
deviceName: 'STORE-782-SW2',
recentDeviceConnection: 'Wired',
status: 'Online',
clientStatus: 'Online',
port: '4',
vlan: '30',
ip: '10.1.2.20',
mac: 'aa:bb:cc:dd:ee:ff',
lastSeen: threeHrAgo(),
clientUrl: 'https://n123.meraki.com/example/manage/clients/xyz/overview',
},
}],
},
};
const md = renderAvStatusMarkdown(data, { storeNum: '782', footer: false });
assert.match(md, /\*\*STORE-782-KIOSK-1\*\*/);
assert.match(md, /\*\*STORE-782-SW2 \(Wired - Online\)\*\*/);
assert.match(md, /\[Meraki↗\]\(https:\/\/n123\.meraki\.com\/example\/manage\/clients\/xyz\/overview\)/);
// Port line with double-arrow indent
assert.match(md, / → → Port: \*\*4\*\*/);
});
test('av renderer: absent MDM devices prints friendly message', () => {
const md = renderAvStatusMarkdown({ mdm: { data: [] } }, { storeNum: '782', footer: false });
assert.match(md, /No MDM devices found for this store/);
});
test('av renderer: Atlas AMP with vitals renders temps + fan + amps', () => {
const data = {
mdm: { data: [] },
atlas: {
data: [{
name: 'US000782AMP',
status: 'online',
last_seen_at: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
model: { name: 'Atlas-4M' },
firmware: { version: '1.9.3' },
sn: 'SN-782-AMP',
state: {
voltageMonitor: '120.4',
faultStatus: 0,
tempCpu: '40', tempPsu: '35', tempIo: '38',
fanSpeed: 45.7,
ampStatus_1: 'Active',
ampStatus_2: 'Ready',
IpAddress: '10.1.9.9',
},
}],
},
};
const md = renderAvStatusMarkdown(data, { storeNum: '782', footer: false });
assert.match(md, /\*\*Atlas Devices:\*\*/);
assert.match(md, /\*\*US000782AMP\*\* \(online\)/);
assert.match(md, /Atlas-4M • FW 1\.9\.3 • SN SN-782-AMP • IP 10\.1\.9\.9/);
assert.match(md, /CPU: 104°F • PSU: 95°F • Io: 100°F • Voltage: 120\.4V • Fan: 46%/);
assert.match(md, /Amps: Amp1: Active, Amp2: Ready/);
});
// ─────────────────────────────────────────────────────────────
// DECT follow-up "loading" hint (main /phonestatus output)
// ─────────────────────────────────────────────────────────────
test('phone renderer: dectFollowUpBaseCount > 0 emits a loading hint inside the DECT section', () => {
const data = {
dectBasestations: [
{ mac: 'aa:bb:cc:dd:ee:01', meraki: { status: 'Online' } },
{ mac: 'aa:bb:cc:dd:ee:02', meraki: { status: 'Online' } },
],
dectHandsets: [],
};
const md = renderPhoneStatusMarkdown(data, {
storeNum: '782', footer: false, dectFollowUpBaseCount: 2,
});
assert.match(md, /Base-station diagnostics loading for 2 bases/);
});
test('phone renderer: dectFollowUpBaseCount === 1 uses singular "base"', () => {
const data = {
dectBasestations: [{ mac: 'aa:bb:cc:dd:ee:01', meraki: { status: 'Online' } }],
dectHandsets: [],
};
const md = renderPhoneStatusMarkdown(data, {
storeNum: '782', footer: false, dectFollowUpBaseCount: 1,
});
assert.match(md, /loading for 1 base —/);
});
test('phone renderer: dectFollowUpBaseCount === 0 emits no loading hint (default state)', () => {
const data = {
dectBasestations: [{ mac: 'aa:bb:cc:dd:ee:01', meraki: { status: 'Online' } }],
dectHandsets: [],
};
const md = renderPhoneStatusMarkdown(data, { storeNum: '782', footer: false });
assert.doesNotMatch(md, /diagnostics loading/);
});
// ─────────────────────────────────────────────────────────────
// DECT follow-up message (renderDectDiagnosticsMarkdown)
// ─────────────────────────────────────────────────────────────
const okResult = (overrides = {}) => ({
base: { mac: '6c:ab:05:f6:28:19', ip: '10.4.11.87', name: 'Basestation A' },
ok: true,
data: {
time: { operatingTime: '02:15:00 (H:M:S)' },
firmware: { version: '05-01-03-0101-09' },
multiCell: { role: 'primary' },
conflictInfo: 'No Conflict',
rebootLog: [],
rtp: { current: 0 },
},
verdict: { healthy: true, warnings: [], info: [] },
elapsedMs: 812,
...overrides,
});
test('dect diagnostics renderer: empty input returns empty string (caller should not send)', () => {
assert.equal(renderDectDiagnosticsMarkdown([], { storeNum: '782' }), '');
assert.equal(renderDectDiagnosticsMarkdown(null, { storeNum: '782' }), '');
});
test('dect diagnostics renderer: healthy base renders check + uptime + firmware', () => {
const md = renderDectDiagnosticsMarkdown([okResult()], { storeNum: '782', footer: false });
assert.match(md, /\*\*DECT Base Station Diagnostics — Store 782\*\*/);
assert.match(md, /✅ \*\*Basestation A\*\* \(10\.4\.11\.87\)/);
assert.match(md, /uptime 02:15:00/);
assert.match(md, /fw 05-01-03-0101-09/);
assert.match(md, /role: primary/);
});
test('dect diagnostics renderer: warnings from verdict are surfaced under the header', () => {
const md = renderDectDiagnosticsMarkdown([
okResult({
verdict: {
healthy: false,
warnings: ['Rx errors: 42 since last boot'],
info: [],
},
}),
], { storeNum: '782', footer: false });
assert.match(md, /⚠️ \*\*Basestation A\*\*/);
assert.match(md, /⚠️ Rx errors: 42 since last boot/);
});
test('dect diagnostics renderer: recent Power Loss reboot gets its own bolt line + suppresses duplicate warning', () => {
const md = renderDectDiagnosticsMarkdown([
okResult({
data: {
time: { operatingTime: '02:15:00' },
firmware: { version: '05-01-03-0101-09' },
multiCell: { role: 'primary' },
conflictInfo: 'No Conflict',
rebootLog: [
{ sequence: 164, at: '2026-07-02T12:54:12', reasonName: 'Power Loss', reasonCode: 80 },
],
rtp: { current: 0 },
},
verdict: {
healthy: false,
warnings: ['1 recent power-loss reboot(s); most recent at 2026-07-02T12:54:12'],
info: [],
},
}),
], { storeNum: '782', footer: false });
// The structured line survives …
assert.match(md, /⚡ Recent power loss: 2026-07-02T12:54:12 \(reboot #164\)/);
// … but the summary warning about power-loss is filtered out to
// avoid duplication under the same header.
assert.doesNotMatch(md, /⚠️ 1 recent power-loss/);
});
test('dect diagnostics renderer: active RTP session gets a call icon', () => {
const md = renderDectDiagnosticsMarkdown([
okResult({
data: {
time: { operatingTime: '02:15:00' },
firmware: { version: '05-01-03-0101-09' },
multiCell: { role: 'primary' },
conflictInfo: 'No Conflict',
rebootLog: [],
rtp: { current: 2 },
},
}),
], { storeNum: '782', footer: false });
assert.match(md, /📞 2 active RTP session/);
});
test('dect diagnostics renderer: base with error renders remediation hint', () => {
const md = renderDectDiagnosticsMarkdown([
{
base: { mac: '6c:ab:05:f6:28:19', ip: '10.4.11.87', name: 'Basestation A' },
ok: false,
data: null,
verdict: null,
elapsedMs: 15003,
error: {
code: 'RELAY_RPC_TIMEOUT',
message: 'timed out after 15000ms',
hint: 'Relay accepted the request but the base did not respond in time.',
},
},
], { storeNum: '782', footer: false });
assert.match(md, /⚠️ \*\*Basestation A\*\* \(10\.4\.11\.87\) — collect failed: timed out after 15000ms/);
assert.match(md, /Relay accepted the request but the base did not respond in time\./);
});
test('dect diagnostics renderer: footer references /dectstatus command by store', () => {
const md = renderDectDiagnosticsMarkdown([okResult()], { storeNum: '782' });
assert.match(md, /Use `\/dectstatus 782`/);
});