const { test } = require('node:test'); const assert = require('node:assert/strict'); const path = require('node:path'); const { pathToFileURL } = require('node:url'); const { normalizeMcpSearchResponse } = require('../src/utils/clinicalRetrieval'); async function sourcesModule() { return import(pathToFileURL(path.join(__dirname, '..', 'public/js/assistant/sources.js')).href); } test('assistant source excerpts hide image markdown but keep OCR text', async () => { const { renderSourcesList } = await sourcesModule(); const sources = normalizeMcpSearchResponse({ results: [{ title: 'Nelson', excerpt: '![](/tmp/pdf-images/pdf-0132-02.png) AGE STREAMS OF DEVELOPMENT' }] }); const html = renderSourcesList(sources); assert.match(html, /AGE STREAMS OF DEVELOPMENT/); assert.doesNotMatch(html, /tmp\/pdf-images|!\[\]/); }); test('actual source rendering preserves full bounded tables, whitespace and escaping', async () => { const { JSDOM } = require('jsdom'); const { renderSourcesList } = await sourcesModule(); const rows = Array.from({ length: 70 }, (_, i) => '| Drug ' + i + ' | 2 mg/kg |'); const sources = normalizeMcpSearchResponse({ results: [{ id: 42, title: 'Fixture', page_number: 7, excerpt: 'Table 1. Dose\n\n| Drug | Dose |\n|---|---|\n' + rows.join('\n') + '\n\nNote: Synthetic values only.' }] }); assert.ok(sources[0].excerpt.length > 900); assert.ok(sources[0].excerpt.length <= 1800); const doc = new JSDOM(renderSourcesList(sources)).window.document; const excerpt = doc.querySelector('.assistant-source-excerpt p'); assert.equal(excerpt.textContent, sources[0].excerpt); assert.equal(excerpt.style.whiteSpace, 'pre-wrap'); assert.match(excerpt.textContent, /\| Drug \| Dose \|\n\|---\|---\|\n/); assert.match(excerpt.textContent, /Note: Synthetic values only\.$/); assert.match(doc.querySelector('.assistant-source-meta').textContent, /page 7/); const unsafe = '\n| A & B | |'; const escaped = new JSDOM(renderSourcesList([{ title: 'Title', excerpt: unsafe }])).window.document; assert.equal(escaped.querySelector('.assistant-source-excerpt').textContent, unsafe); assert.equal(escaped.querySelector('img, script, b'), null); }); test('legacy source numbers remain text and cannot inject attributes or markup', async () => { const { JSDOM } = require('jsdom'); const { renderSourcesList } = await sourcesModule(); for (const number of ['1" onclick="alert(1)', '1">', "1'>", '1', 2]) { const dom = new JSDOM(renderSourcesList([{ number, title: 'Legacy source' }])); const card = dom.window.document.querySelector('.assistant-source'); assert.equal(card.id, 'assistant-source-' + number); assert.equal(card.querySelector('strong').textContent, '[' + number + '] Legacy source'); assert.deepEqual(card.getAttributeNames().sort(), ['class', 'id']); assert.equal(dom.window.document.querySelector('img, script, [onclick], [onerror]'), null); dom.window.close(); } });