pediatric-ai-scribe-v3/test/assistant-citations.test.js

204 lines
12 KiB
JavaScript

const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs/promises');
const path = require('node:path');
const MarkdownIt = require('markdown-it');
let modulePromise;
async function loadCitationModule() {
if (!modulePromise) {
modulePromise = fs.readFile(path.join(__dirname, '..', 'public', 'js', 'assistant', 'citations.js'), 'utf8')
.then((source) => import('data:text/javascript;charset=utf-8,' + encodeURIComponent(source)));
}
return modulePromise;
}
const sources = [
{ title: 'Nelson Textbook of Pediatrics' },
{ title: 'Pediatric Asthma Guideline' },
{ resource: 'Emergency Medicine Review' }
];
test('renders citation clusters as links to matching source cards', async () => {
const { renderAssistantMarkdown } = await loadCitationModule();
const html = renderAssistantMarkdown('Give magnesium for severe exacerbation [1, 2].', sources);
assert.match(html, /href="#assistant-source-1"/);
assert.match(html, /href="#assistant-source-2"/);
assert.match(html, /title="Source 1: Nelson Textbook of Pediatrics"/);
assert.match(html, /data-source-number="1"/);
assert.match(html, /data-source-number="2"/);
assert.match(html, /<a class="assistant-cite"[^>]*>src<\/a> <a class="assistant-cite"[^>]*>src<\/a>/);
});
test('can render citation labels as numbers for PDF export', async () => {
const { renderAssistantMarkdown } = await loadCitationModule();
const html = renderAssistantMarkdown('Give magnesium for severe exacerbation [1, 2].', sources, { citationLabel: 'number' });
assert.match(html, /<a class="assistant-cite"[^>]*>1<\/a> <a class="assistant-cite"[^>]*>2<\/a>/);
});
test('leaves unknown citation clusters untouched rather than guessing', async () => {
const { renderAssistantMarkdown } = await loadCitationModule();
const html = renderAssistantMarkdown('Dose statement [4].', sources);
assert.match(html, /\[4\]/);
assert.doesNotMatch(html, /assistant-source-4/);
});
test('does not merge separate citations across separate claims', async () => {
const { renderAssistantMarkdown } = await loadCitationModule();
const html = renderAssistantMarkdown('Use oxygen [1]. Give bronchodilator [2].', sources);
assert.match(html, /oxygen <a class="assistant-cite"[^>]*data-source-number="1"[^>]*>src<\/a>/);
assert.match(html, /bronchodilator <a class="assistant-cite"[^>]*data-source-number="2"[^>]*>src<\/a>/);
assert.doesNotMatch(html, /data-source-number="1"[^>]*>src<\/a><a class="assistant-cite"[^>]*data-source-number="2"/);
});
test('sorts adjacent citation tokens into one safe Vancouver cluster', async () => {
const { renderAssistantMarkdown } = await loadCitationModule();
const html = renderAssistantMarkdown('Deteriorating course [1][4][2][3].', [
{ title: 'A' }, { title: 'B' }, { title: 'C' }, { title: 'D' }
]);
assert.match(html, /data-source-number="1"[^>]*>src<\/a> <a class="assistant-cite"[^>]*data-source-number="2"[^>]*>src<\/a> <a class="assistant-cite"[^>]*data-source-number="3"[^>]*>src<\/a> <a class="assistant-cite"[^>]*data-source-number="4"[^>]*>src<\/a>/);
assert.doesNotMatch(html, /\]<span|\]\[/);
});
test('repairs a clearly adjacent trailing citation missing its closing bracket', async () => {
const { renderAssistantMarkdown } = await loadCitationModule();
const html = renderAssistantMarkdown('Deteriorating course [1][4][2][3', [
{ title: 'A' }, { title: 'B' }, { title: 'C' }, { title: 'D' }
]);
assert.match(html, /data-source-number="1"[^>]*>src<\/a> <a class="assistant-cite"[^>]*data-source-number="2"[^>]*>src<\/a> <a class="assistant-cite"[^>]*data-source-number="3"[^>]*>src<\/a> <a class="assistant-cite"[^>]*data-source-number="4"[^>]*>src<\/a>/);
});
test('does not normalize adjacent citations if any source number is unknown', async () => {
const { renderAssistantMarkdown } = await loadCitationModule();
const html = renderAssistantMarkdown('Unsupported claim [1][9].', sources);
assert.match(html, /\[9\]/);
assert.doesNotMatch(html, /assistant-source-9/);
});
test('escapes source titles inside citation link titles', async () => {
const { renderAssistantMarkdown } = await loadCitationModule();
const html = renderAssistantMarkdown('Unsafe title [1].', [{ title: 'Bad "Title" <script>' }]);
assert.match(html, /title="Source 1: Bad &quot;Title&quot; &lt;script&gt;"/);
assert.doesNotMatch(html, /<script>/);
});
test('strips model-generated Sources and References sections', async () => {
const { renderAssistantMarkdown } = await loadCitationModule();
const html = renderAssistantMarkdown('Answer body [1].\n\n## Sources\n[1] duplicate source', sources);
assert.match(html, /Answer body/);
assert.doesNotMatch(html, /duplicate source/);
});
test('strips colon-style References sections', async () => {
const { renderAssistantMarkdown } = await loadCitationModule();
const html = renderAssistantMarkdown('Answer body [1].\n\n--- References:\n[1] duplicate source', sources);
assert.match(html, /Answer body/);
assert.doesNotMatch(html, /duplicate source/);
});
test('normalizes inline markdown headings before rendering', async () => {
const { renderAssistantMarkdown } = await loadCitationModule();
const html = renderAssistantMarkdown('Intro ### Management\n- oxygen', sources);
assert.match(html, /<h3>Management<\/h3>/);
assert.match(html, /<li>oxygen<\/li>/);
});
test('normalizes old saved assistant answer formatting', async () => {
const { renderAssistantMarkdown } = await loadCitationModule();
const html = renderAssistantMarkdown('Comparison of Bronchiolitis and Asthma Management in Infants ### Bronchiolitis Management - Mainstay: Supportive care [1, 2]. - Medications: bronchodilators are not routine [2]. ### Key Differences | Aspect | Bronchiolitis | Asthma | |---|---|---| | Therapy | Supportive | SABA | --- References: [1] duplicate', sources);
assert.match(html, /<h3>Bronchiolitis Management<\/h3>/);
assert.match(html, /<li>Mainstay: Supportive care/);
assert.match(html, /<table>/);
assert.doesNotMatch(html, /duplicate/);
});
test('strips inline model References after a cited sentence', async () => {
const { renderAssistantMarkdown } = await loadCitationModule();
const html = renderAssistantMarkdown('Infants <2 years often do not respond [2, 3]. --- References: [2] Book p.1 [3] Other p.2', sources);
assert.match(html, /Infants &lt;2 years/);
assert.doesNotMatch(html, /References/);
assert.doesNotMatch(html, /Book p\.1/);
});
test('repairs smashed admission bullet list after citations', async () => {
const { renderAssistantMarkdown } = await loadCitationModule();
const html = renderAssistantMarkdown('Admission is indicated for: Apnoea [1, 2]- **Persistent oxygen saturation <90%** [1, 2]- **Severe respiratory distress** [1]', sources);
assert.match(html, /<li><strong>Persistent oxygen saturation &lt;90%<\/strong>/);
assert.match(html, /<li><strong>Severe respiratory distress<\/strong>/);
assert.doesNotMatch(html, /\]-/);
});
test('repairs smashed head injury red flag bullets after citation clusters', async () => {
const { renderAssistantMarkdown } = await loadCitationModule();
const html = renderAssistantMarkdown('CT is indicated for: [1, 2, 3]- Focal neurologic deficits [1]- Signs of skull fracture [2]- Recurrent vomiting [3]', sources);
assert.match(html, /<li>Focal neurologic deficits/);
assert.match(html, /<li>Signs of skull fracture/);
assert.match(html, /<li>Recurrent vomiting/);
assert.doesNotMatch(html, /\]-/);
});
test('preserves code block contents without creating citation links inside code', async () => {
const { renderAssistantMarkdown } = await loadCitationModule();
const html = renderAssistantMarkdown('```js\nconst ref = "[1]";\n```', sources);
assert.match(html, /<pre><code>const ref = &quot;\[1\]&quot;/);
assert.doesNotMatch(html, /assistant-source-1/);
});
test('repairs model-split markdown tables before rendering', async () => {
const { renderAssistantMarkdown } = await loadCitationModule();
const html = renderAssistantMarkdown('Summary Table\n| Indication for Use | Dose (IV) | Max Dose\n\n| Infusion Time | Monitoring/Precautions |\n|-----------------------------------------|----------------------------|----------|--------------|---------------------------------------|\n| Severe exacerbation unresponsive to SABA/anticholinergic | 25-75 mg/kg | 2 g\n| 20 min | BP, reflexes, respiratory status[1, 2] |', sources);
assert.match(html, /<table>/);
assert.match(html, /Max Dose Infusion Time/);
assert.match(html, /2 g 20 min/);
assert.doesNotMatch(html, /<p>\| Infusion Time/);
});
test('breaks inline numbered clinical sections onto new lines', async () => {
const { renderAssistantMarkdown } = await loadCitationModule();
const html = renderAssistantMarkdown('Monitor with serial abdominal exams [1]. 2. Imaging\nImmediate abdominal X-ray for obstruction [1, 2]. 3. Laboratory Studies\nAssess electrolytes if ill-appearing [3]. 4. Surgical Consultation\nUrgent pediatric surgical consultation [1].', sources);
assert.match(html, /2\. Imaging/);
assert.match(html, /3\. Laboratory Studies/);
assert.match(html, /4\. Surgical Consultation/);
assert.doesNotMatch(html, /exams[^<]*2\. Imaging/);
assert.doesNotMatch(html, /obstruction[^<]*3\. Laboratory Studies/);
});
test('strips orphan trailing markdown emphasis markers', async () => {
const { renderAssistantMarkdown } = await loadCitationModule();
const html = renderAssistantMarkdown('Summary Table\n\nAge Group\tEmpiric Antibiotics\n<1 month\tAmpicillin + cefotaxime\n**', sources);
assert.match(html, /Summary Table/);
assert.doesNotMatch(html, /\*\*/);
});
test('uses markdown-it stack for GFM-style tables when provided', async () => {
const { renderAssistantMarkdown } = await loadCitationModule();
const markdownIt = new MarkdownIt({ html: false, linkify: true, typographer: true, breaks: true });
const html = renderAssistantMarkdown('| Age | Antibiotics |\n|---|---|\n| <1 month | Ampicillin + cefotaxime [1] |', sources, { markdownIt });
assert.match(html, /<table>/);
assert.match(html, /<td>&lt;1 month<\/td>/);
assert.match(html, /assistant-cite/);
});
test('renders tab-separated summary tables and removes trailing emphasis junk', async () => {
const { renderAssistantMarkdown } = await loadCitationModule();
const markdownIt = new MarkdownIt({ html: false, linkify: true, typographer: true, breaks: true });
const input = 'Summary Table\nSituation\tImaging Recommended\tTiming/Notes\nFirst febrile UTI\tRenal/bladder ultrasound\tAfter recovery\nRoutine use of DMSA scan\tNot recommended\t\n**';
const html = renderAssistantMarkdown(input, sources, { markdownIt });
assert.match(html, /<table>/);
assert.match(html, /<th>Situation<\/th>/);
assert.match(html, /<td>Routine use of DMSA scan<\/td>/);
assert.doesNotMatch(html, /\*\*/);
});
test('repairs split tab-separated clinical summary rows', async () => {
const { renderAssistantMarkdown } = await loadCitationModule();
const markdownIt = new MarkdownIt({ html: false, linkify: true, typographer: true, breaks: true });
const input = 'Summary Table\nStep\tDetails\tSource\nRed Flags\tShock, peritonitis, distension\t3 4 7\nInitial Imaging\tAbdominal X-ray if perforation suspected, then ultrasound\n2 3 7\t\tDiagnostic Gold Std\nAbdominal ultrasound target sign\t2 3 7 Nonoperative Reduction\tAir or contrast enema if stable\nSurgical Indications\tShock, peritonitis, perforation, failed reduction\n1 3 7\t\tRecurrence Rate\n~10% after nonsurgical reduction\t5 6 7\tIf you need details on recurrence management, please specify.';
const html = renderAssistantMarkdown(input, sources, { markdownIt, citationLabel: 'number' });
assert.match(html, /<table>/);
assert.match(html, /<td>Initial Imaging<\/td>/);
assert.match(html, /<td>Diagnostic Gold Std<\/td>/);
assert.match(html, /<td>Recurrence Rate<\/td>/);
assert.doesNotMatch(html, /If you need details/);
});