204 lines
12 KiB
JavaScript
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 "Title" <script>"/);
|
|
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 <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 <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 = "\[1\]"/);
|
|
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><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/);
|
|
});
|