112 lines
5.9 KiB
JavaScript
112 lines
5.9 KiB
JavaScript
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('<div id="assistant-tab">' + read('public/components/assistant.html') + '</div>', { 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);
|
|
window.marked = marked;
|
|
window.DOMPurify = require('dompurify')(window);
|
|
window.matchMedia = () => ({ matches: true });
|
|
const toasts = [];
|
|
const context = { window, document: window.document, console, URL, Blob, TextDecoder, AbortController,
|
|
setTimeout() {}, clearTimeout() {}, showToast: (...args) => toasts.push(args), 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);
|
|
}
|
|
appBinding(t, context);
|
|
t.after(() => window.close());
|
|
return { context, document: window.document, window, toasts };
|
|
}
|
|
|
|
// The production page binds delegated clicks in bindEvents; the vm harness mirrors it.
|
|
function appBinding(t, context) {
|
|
context.bindEvents();
|
|
}
|
|
|
|
const longExcerpt = 'Synthetic excerpt. ' + 'x'.repeat(15000);
|
|
|
|
function openModal(app, n = 1) {
|
|
const cite = app.document.querySelector('.assistant-cite[data-source-number="' + n + '"]');
|
|
assert.ok(cite, 'citation chip exists');
|
|
cite.click();
|
|
return app.document.querySelector('.assistant-source-modal');
|
|
}
|
|
|
|
test('citation chips open an OWUI-style source modal with title, page and 10k excerpt preview', t => {
|
|
const app = ui(t);
|
|
const sources = [{ number: 1, title: 'Nelson Textbook of Pediatrics', page: 7, excerpt: longExcerpt }];
|
|
app.context.restoreSavedChat({ version: 2, messages: [
|
|
{ role: 'user', content: 'Question' },
|
|
{ role: 'assistant', content: 'Answer [1].', sources }
|
|
], lastAnswer: 'Answer [1].', sources });
|
|
const modal = openModal(app);
|
|
assert.ok(modal, 'modal opens');
|
|
assert.equal(modal.getAttribute('role'), 'dialog');
|
|
assert.match(modal.textContent, /Nelson Textbook of Pediatrics/);
|
|
assert.match(modal.textContent, /page 7/);
|
|
const excerpt = modal.querySelector('[data-assistant-source-excerpt]');
|
|
assert.ok(excerpt);
|
|
assert.ok(excerpt.textContent.length >= 10000 && excerpt.textContent.length <= 10200, '10k preview shown');
|
|
const showAll = modal.querySelector('[data-assistant-source-show-all]');
|
|
assert.ok(showAll, 'Show all offered for long excerpts');
|
|
showAll.click();
|
|
assert.ok(excerpt.textContent.length >= 15000, 'Show all reveals the full excerpt');
|
|
assert.equal(showAll.hidden, true, 'toggle hides once expanded');
|
|
// Sidebar switching is preserved alongside the modal.
|
|
assert.match(app.document.querySelector('#assistant-source-1').textContent, /Nelson Textbook of Pediatrics.*page 7/is);
|
|
});
|
|
|
|
test('source modal closes via button, backdrop and Escape', t => {
|
|
const app = ui(t);
|
|
const sources = [{ number: 1, title: 'Synthetic source', page: 3, excerpt: 'Short excerpt.' }];
|
|
app.context.restoreSavedChat({ version: 2, messages: [
|
|
{ role: 'user', content: 'Question' },
|
|
{ role: 'assistant', content: 'Answer [1].', sources }
|
|
], lastAnswer: 'Answer [1].', sources });
|
|
let modal = openModal(app);
|
|
assert.equal(modal.querySelector('[data-assistant-source-show-all]'), null, 'no Show all for short excerpts');
|
|
modal.querySelector('.modal-close').click();
|
|
assert.equal(app.document.querySelector('.assistant-source-modal'), null);
|
|
modal = openModal(app);
|
|
modal.click(); // backdrop is the modal element itself
|
|
assert.equal(app.document.querySelector('.assistant-source-modal'), null);
|
|
modal = openModal(app);
|
|
app.window.document.dispatchEvent(new app.window.KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
|
assert.equal(app.document.querySelector('.assistant-source-modal'), null);
|
|
});
|
|
|
|
test('per-turn source maps select the correct source for reused citation numbers', t => {
|
|
const app = ui(t);
|
|
const first = [{ number: 1, title: 'First turn reference', page: 11, excerpt: 'First turn context.' }];
|
|
const second = [{ number: 1, title: 'Second turn reference', page: 23, excerpt: 'Second turn context.' }];
|
|
app.context.restoreSavedChat({ version: 2, messages: [
|
|
{ role: 'user', content: 'First question' },
|
|
{ role: 'assistant', content: 'First answer. [1]', sources: first },
|
|
{ role: 'user', content: 'Second question' },
|
|
{ role: 'assistant', content: 'Second answer. [1]', sources: second }
|
|
], lastAnswer: 'Second answer. [1]', sources: second });
|
|
const citations = app.document.querySelectorAll('#assistant-messages .assistant-cite');
|
|
assert.equal(citations.length, 2);
|
|
citations[0].click();
|
|
assert.match(app.document.querySelector('.assistant-source-modal').textContent, /First turn reference/);
|
|
assert.match(app.document.querySelector('.assistant-source-modal').textContent, /page 11/);
|
|
app.document.querySelector('.assistant-source-modal .modal-close').click();
|
|
citations[1].click();
|
|
const modal = app.document.querySelector('.assistant-source-modal');
|
|
assert.match(modal.textContent, /Second turn reference/);
|
|
assert.match(modal.textContent, /page 23/);
|
|
assert.doesNotMatch(modal.textContent, /First turn reference/);
|
|
});
|