const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const vm = require('node:vm'); const { JSDOM } = require('jsdom'); const { marked } = require('marked'); const read = file => fs.readFileSync(path.join(__dirname, '..', file), 'utf8'); function ui(t) { const dom = new JSDOM('
' + read('public/components/assistant.html') + '
', { url: 'https://example.test', runScripts: 'outside-only' }); const window = dom.window; window.eval(read('public/js/accountBoundary.js')); assert.equal(window.AccountBoundary.enter({ id: 'synthetic-rendering-owner' }, true), true); const style = window.document.createElement('style'); style.textContent = read('public/css/assistant.css'); window.document.head.appendChild(style); window.marked = marked; window.DOMPurify = require('dompurify')(window); window.matchMedia = () => ({ matches: true }); const context = { window, document: window.document, console, URL, Blob, TextDecoder, AbortController, setTimeout() {}, clearTimeout() {}, showToast() {}, EMPTY_PROMPT_SETS: [[]], createAssistantImageStore: () => ({ clear() {}, renderGeneratedImage: () => '' }), fetchSavedAssistantChats: async () => ({ success: true, chats: [] }), saveAssistantChat: async () => ({ success: true }) }; vm.createContext(context); for (const file of ['assistant/citations.js', 'assistant/sources.js', 'assistant/sharing.js', 'generatedImages.js', 'assistant/export.js', 'clinicalAssistant.js']) { vm.runInContext(read('public/js/' + file).replace(/^import[\s\S]*?from ['"][^'"]+['"];\s*/gm, '').replace(/^export /gm, ''), context); } t.after(() => window.close()); return { context, document: window.document, window }; } function userBubble(app) { return app.document.querySelector('.assistant-msg.user .assistant-bubble'); } test('user messages render markdown while the raw transcript stays canonical', t => { const app = ui(t); const md = '**Bold** plan\n\n- item one\n- item two\n\n`inline code` and 2+2=4'; app.context.appendMessage('user', md); const bubble = userBubble(app); assert.match(bubble.innerHTML, /Bold<\/strong>/); assert.match(bubble.innerHTML, /
  • item one<\/li>/); assert.match(bubble.innerHTML, /
  • item two<\/li>/); assert.match(bubble.innerHTML, /inline code<\/code>/); assert.equal(app.context.messages[0].content, md, 'raw user text is stored unchanged'); assert.equal(app.context.messages[0].role, 'user'); assert.deepEqual(JSON.parse(JSON.stringify(app.context.messages[0].sources)), [], 'user turns carry no source map'); assert.equal(bubble.querySelector('.assistant-cite'), null, 'user text cannot create citation links'); }); test('user tables render with the OWUI wrapper and keyboard-scroll accessibility attributes', t => { const app = ui(t); app.context.appendMessage('user', '| Item | Dose |\n| :--- | ---: |\n| Alpha | 2 |'); const scroll = userBubble(app).querySelector('.assistant-table-scroll'); assert.ok(scroll, 'user table gets the shared wrapper'); assert.equal(scroll.getAttribute('tabindex'), '0'); assert.equal(scroll.getAttribute('role'), 'region'); assert.equal(scroll.getAttribute('aria-label'), 'Scrollable table'); assert.equal(scroll.querySelectorAll('tbody tr').length, 1); }); test('inline markdown images respect the safe allowlist: own assets and data URIs only', t => { const app = ui(t); const id = '12345678-1234-1234-1234-123456789abc'; app.context.appendMessage('user', '![chart](/api/generated-images/' + id + ') and ![inline](data:image/png;base64,iVBORw0KGgo=)'); const imgs = userBubble(app).querySelectorAll('img'); assert.equal(imgs.length, 2); assert.ok(imgs[0].getAttribute('src').endsWith('/api/generated-images/' + id)); assert.ok(imgs[1].getAttribute('src').startsWith('data:image/png;base64,')); app.context.appendMessage('user', '![evil](https://evil.example/x.png) and a normal [link](https://example.test/page)'); const bubbles = app.document.querySelectorAll('.assistant-msg.user .assistant-bubble'); assert.equal(bubbles[1].querySelectorAll('img').length, 0, 'external image URLs never render as images'); assert.match(bubbles[1].textContent, /evil/); assert.equal(bubbles[1].querySelector('a[href="https://example.test/page"]') !== null, true, 'ordinary links stay links'); }); test('assistant markdown rendering and citation chips are unchanged by user-markdown mode', t => { const app = ui(t); const sources = [{ title: 'Synthetic A', page: 7 }]; app.context.appendMessage('user', 'What is the dose [1]?'); app.context.appendMessage('assistant', 'Dose [1].', sources); const assistantBubble = app.document.querySelector('.assistant-msg.assistant .assistant-bubble'); assert.ok(assistantBubble.querySelector('.assistant-cite[data-source-number="1"]')); assert.equal(userBubble(app).querySelector('.assistant-cite'), null); assert.deepEqual(app.context.messages[1].sources, sources, 'per-turn source maps preserved'); });