collabSupport/tests/markdownToAdf.test.js
jmcqueen 351f89a9a4 Initial commit: CollabFinder Webex bot
Multi-integration Webex chat/HTTP bot that unifies phone, AV, and
network status for retail store support. Consolidates data from
Webex Calling, Meraki, Workspace ONE (MDM), Atlas AMP, RED digital
signage, and OptiSigns into rich per-store status commands.

Key surfaces:
- /phonestatus, /avstatus — per-store phone & AV device reports with
  clickable Meraki deep-links and per-port detail.
- /webexhost — check/assign Webex Meetings host licenses via the
  Service App; adaptive-card confirmation flow, HTTP-API-gated.
- /offboarduser — full Webex Admin offboarding (auth revoke, device
  wipe, license removal); adaptive-card confirmation.
- /jirapoll — on-demand trigger for the hourly Jira poller.
- /bulkavstatuscsv — bulk store CSV export with concurrency limits.

Automation:
- Hourly Jira poller (node-cron) with an X.AI (Grok) ticket classifier
  that categorizes unassigned tickets as phone/av/skip, extracts store
  numbers from free-text, and enriches Jira with the same detailed
  markdown the chat commands emit (converted to Jira ADF, preserves
  bold + Meraki links). Idempotent via a `bot-enriched` Jira label.

Architecture:
- Node.js 20+, ESM, Express 5, webex-node-bot-framework.
- Layered integrations (integrations/*), services (services/*),
  commands (commands/*), utils (utils/*).
- Shared markdown renderers (services/renderers/*) feed both chat
  handlers and the Jira poller so the two surfaces stay in sync.
- Hand-rolled markdown-to-ADF converter (utils/markdownToAdf.js) —
  no new npm dependency.
- Node built-in test runner (`node --test tests/*.test.js`), 30 tests
  covering the converter, renderers, and poller ADF assembly.

Docker + docker-compose deployment. Config via .env
(see .env.example for the full option surface).
2026-07-01 16:55:03 -04:00

166 lines
5.6 KiB
JavaScript

// Unit tests for utils/markdownToAdf.js — uses Node's built-in test
// runner (`node --test tests/markdownToAdf.test.js`) so we don't need
// a separate test framework in package.json for this scope.
import test from 'node:test';
import assert from 'node:assert/strict';
import {
markdownToAdf,
markdownToAdfContent,
tokenizeLine,
} from '../utils/markdownToAdf.js';
test('empty string produces an empty doc', () => {
assert.deepEqual(markdownToAdf(''), { version: 1, type: 'doc', content: [] });
assert.deepEqual(markdownToAdfContent(''), []);
});
test('whitespace-only input produces an empty doc', () => {
assert.deepEqual(markdownToAdfContent(' \n\n '), []);
});
test('non-string input is treated as empty', () => {
assert.deepEqual(markdownToAdfContent(null), []);
assert.deepEqual(markdownToAdfContent(undefined), []);
assert.deepEqual(markdownToAdfContent(42), []);
});
test('plain single-paragraph text becomes one paragraph', () => {
const out = markdownToAdfContent('hello world');
assert.deepEqual(out, [
{ type: 'paragraph', content: [{ type: 'text', text: 'hello world' }] },
]);
});
test('bold: **text**', () => {
assert.deepEqual(tokenizeLine('a **bold** b'), [
{ type: 'text', text: 'a ' },
{ type: 'text', text: 'bold', marks: [{ type: 'strong' }] },
{ type: 'text', text: ' b' },
]);
});
test('italic: *text*', () => {
assert.deepEqual(tokenizeLine('a *italic* b'), [
{ type: 'text', text: 'a ' },
{ type: 'text', text: 'italic', marks: [{ type: 'em' }] },
{ type: 'text', text: ' b' },
]);
});
test('link: [label](url)', () => {
assert.deepEqual(tokenizeLine('see [here](https://x.y)'), [
{ type: 'text', text: 'see ' },
{
type: 'text',
text: 'here',
marks: [{ type: 'link', attrs: { href: 'https://x.y' } }],
},
]);
});
test('all three marks combined in one line', () => {
const out = tokenizeLine('**bold** and *em* and [click](https://x.y) done');
assert.deepEqual(out, [
{ type: 'text', text: 'bold', marks: [{ type: 'strong' }] },
{ type: 'text', text: ' and ' },
{ type: 'text', text: 'em', marks: [{ type: 'em' }] },
{ type: 'text', text: ' and ' },
{
type: 'text',
text: 'click',
marks: [{ type: 'link', attrs: { href: 'https://x.y' } }],
},
{ type: 'text', text: ' done' },
]);
});
test('emoji at line start survives as plain text', () => {
assert.deepEqual(tokenizeLine('✅ ok'), [
{ type: 'text', text: '✅ ok' },
]);
});
test('arrow → and bullet • characters are plain text', () => {
assert.deepEqual(tokenizeLine(' → foo • bar'), [
{ type: 'text', text: ' → foo • bar' },
]);
});
test('blank line splits into two paragraphs', () => {
const out = markdownToAdfContent('one\n\ntwo');
assert.equal(out.length, 2);
assert.equal(out[0].type, 'paragraph');
assert.deepEqual(out[0].content, [{ type: 'text', text: 'one' }]);
assert.deepEqual(out[1].content, [{ type: 'text', text: 'two' }]);
});
test('single newline becomes hardBreak inside one paragraph', () => {
const out = markdownToAdfContent('line1\nline2');
assert.equal(out.length, 1);
assert.equal(out[0].type, 'paragraph');
assert.deepEqual(out[0].content, [
{ type: 'text', text: 'line1' },
{ type: 'hardBreak' },
{ type: 'text', text: 'line2' },
]);
});
test('multiple blank lines collapse to a single paragraph split', () => {
const out = markdownToAdfContent('a\n\n\n\nb');
assert.equal(out.length, 2);
assert.deepEqual(out[0].content, [{ type: 'text', text: 'a' }]);
assert.deepEqual(out[1].content, [{ type: 'text', text: 'b' }]);
});
test('malformed markdown does not throw', () => {
// Never throws is the only hard guarantee — the exact token shape
// for ambiguous fragments is best-effort. The parser scans strong
// before em, so an unclosed `**` frequently gets rescued as an em
// starting at the second `*`. That's fine, just verify the concrete
// behavior stays stable.
assert.doesNotThrow(() => tokenizeLine('**unclosed and *also* here'));
const out = tokenizeLine('**unclosed and *also* here');
// Something got marked em — the middle `*...*` window matches.
assert.ok(out.some((n) => n.marks?.[0]?.type === 'em'));
});
test('empty link body [](url) is treated as plain text', () => {
// LINK_RE requires at least one char inside brackets; the empty case
// won't match, so the raw text passes through unmarked.
const out = tokenizeLine('a [](https://x.y) b');
const rendered = out.map((n) => n.text).join('');
assert.equal(rendered, 'a [](https://x.y) b');
assert.ok(!out.some((n) => n.marks?.[0]?.type === 'link'));
});
test('*a**b*c does not throw and produces plausible tokens', () => {
// Nasty edge case — verify we don't crash. Exact shape is not
// contract, just "well-formed ADF inline nodes with no exception".
assert.doesNotThrow(() => tokenizeLine('*a**b*c'));
});
test('markdownToAdf full doc wraps content correctly', () => {
const doc = markdownToAdf('**hi**\n\n[go](https://x)');
assert.equal(doc.version, 1);
assert.equal(doc.type, 'doc');
assert.equal(doc.content.length, 2);
assert.deepEqual(doc.content[0].content, [
{ type: 'text', text: 'hi', marks: [{ type: 'strong' }] },
]);
assert.deepEqual(doc.content[1].content, [
{
type: 'text',
text: 'go',
marks: [{ type: 'link', attrs: { href: 'https://x' } }],
},
]);
});
test('adjacent plain-text runs collapse into one text node', () => {
// Verifies the pushText coalescing behavior — cleaner ADF output.
const out = tokenizeLine('abc def ghi');
assert.equal(out.length, 1);
assert.deepEqual(out[0], { type: 'text', text: 'abc def ghi' });
});