Borrowed from the quiz app's AI Mode, where validating citations and ordering them fall out of the same pass: it collects the sources an answer actually used into an insertion-ordered map, so the list comes back in first-citation order for free. Ours listed sources in retrieval order — an order the reader never sees and has no way to follow. An answer whose first citation was [7] opened a list that began at [1], so matching a marker to a source meant hunting. Reference lists in published writing are numbered by first appearance for exactly this reason. Cited sources now come first, renumbered by first appearance, and the markers in the text are rewritten to match. Anything retrieved and not cited keeps its place after them, labelled "not cited" — the panel is also a view of what the search returned, which is worth keeping, but it should not sit among the numbers the answer used. The marker itself now shows its number instead of the word "src". Every citation read identically, so the only way to tell one from another was to hover it — which made the numbered list beneath useless to match against. The export has shown numbers since the day "src" was introduced, with no recorded reason for the difference. Renumbering happens once the whole answer is known, never while streaming: the order is the order of first citation, so a citation that has not arrived yet cannot take its place, and numbers would shuffle under the reader mid-sentence. The text is rewritten in a single pass — number by number would turn 2 into 1 and then that 1 into whatever 1 maps to. An invented citation reserves no position and is left exactly as it was. It is still not turned into a link, and citation_audit still records it; what matters here is that it cannot push a real source down the list. Accuracy was already held: a marker with no matching source never becomes a link. This changes what a reader can do with the ones that are real. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
346 lines
21 KiB
JavaScript
346 lines
21 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"[^>]*>\d+<\/a> <a class="assistant-cite"[^>]*>\d+<\/a>/);
|
|
});
|
|
|
|
test('renders escaped citation tokens as source links, not display math', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const katex = { renderToString: function () { throw new Error('citation sent to KaTeX'); } };
|
|
const html = renderAssistantMarkdown('Acquired hypothyroidism is uncommon. \\[1, 2\\].', sources, { katex });
|
|
assert.match(html, /<a class="assistant-cite"[^>]*>\d+<\/a> <a class="assistant-cite"[^>]*>\d+<\/a>/);
|
|
assert.doesNotMatch(html, /katex-display/);
|
|
});
|
|
|
|
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('strips [src]/[source] placeholder tokens instead of inventing numbers', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const html = renderAssistantMarkdown('Give fluids [src] and review in 24h [source].', []);
|
|
assert.doesNotMatch(html, /\[src\]|\[source\]/i);
|
|
assert.match(html, /Give fluids/);
|
|
const kept = renderAssistantMarkdown('Give fluids [1] and review.', [{ number: 1, title: 'S', page: 2 }]);
|
|
assert.match(kept, /data-source-number="1"/, 'real citation numbers still render');
|
|
});
|
|
|
|
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"[^>]*>\d+<\/a>/);
|
|
assert.match(html, /bronchodilator <a class="assistant-cite"[^>]*data-source-number="2"[^>]*>\d+<\/a>/);
|
|
assert.doesNotMatch(html, /data-source-number="1"[^>]*>\d+<\/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"[^>]*>\d+<\/a> <a class="assistant-cite"[^>]*data-source-number="2"[^>]*>\d+<\/a> <a class="assistant-cite"[^>]*data-source-number="3"[^>]*>\d+<\/a> <a class="assistant-cite"[^>]*data-source-number="4"[^>]*>\d+<\/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"[^>]*>\d+<\/a> <a class="assistant-cite"[^>]*data-source-number="2"[^>]*>\d+<\/a> <a class="assistant-cite"[^>]*data-source-number="3"[^>]*>\d+<\/a> <a class="assistant-cite"[^>]*data-source-number="4"[^>]*>\d+<\/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('does not strip sentences that mention available sources', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const html = renderAssistantMarkdown('Pneumonia can cause parapneumonic effusion [1]. However, the available sources do not specifically identify adenovirus as a typical cause [2]. In summary, evidence is insufficient [3].', sources);
|
|
assert.match(html, /available sources do not specifically identify adenovirus/);
|
|
assert.match(html, /In summary, evidence is insufficient/);
|
|
});
|
|
|
|
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, /Key Differences/);
|
|
});
|
|
|
|
test('does not turn citation-delimited prose into a list', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const html = renderAssistantMarkdown('Admission is indicated for: Apnoea [1, 2]- **Persistent oxygen saturation <90%** [1, 2].', sources);
|
|
assert.match(html, /<\/a>- <strong>Persistent oxygen saturation <90%<\/strong>/);
|
|
assert.doesNotMatch(html, /<ul>/);
|
|
});
|
|
|
|
test('joins a citation-only paragraph back to its claim', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const html = renderAssistantMarkdown('Acquired hypothyroidism is uncommon.\n\n[1, 2]\n\n.\n\n- Next point', sources);
|
|
assert.match(html, /uncommon\. <a class="assistant-cite"[^>]*>\d+<\/a> <a class="assistant-cite"[^>]*>\d+<\/a>\./);
|
|
assert.doesNotMatch(html, /<p>\s*<a class="assistant-cite"/);
|
|
});
|
|
|
|
test('keeps citations inline before clinical terms', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const html = renderAssistantMarkdown('Assess for lethargy [1] and dehydration [2].', sources);
|
|
assert.match(html, /lethargy <a class="assistant-cite"[^>]*>\d+<\/a> and dehydration <a class="assistant-cite"[^>]*>\d+<\/a>/);
|
|
assert.doesNotMatch(html, /<\/a><br>\s*and dehydration/);
|
|
});
|
|
|
|
test('keeps citation-delimited clinical prose inline', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const html = renderAssistantMarkdown('CT is indicated for: [1, 2, 3]- Focal neurologic deficits [1].', sources);
|
|
assert.match(html, /<\/a>- Focal neurologic deficits/);
|
|
assert.doesNotMatch(html, /<br>|<ul>/);
|
|
});
|
|
|
|
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('does not repair source tables inside code blocks', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const html = renderAssistantMarkdown('```md\n| Feature | Source(s) |\n|---|---|\n| Example | 1, 2 |\n```', sources);
|
|
assert.match(html, /\| Example \| 1, 2 \|/);
|
|
assert.doesNotMatch(html, /assistant-source-1/);
|
|
});
|
|
|
|
test('does not repair model-split markdown tables', 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.doesNotMatch(html, /Max Dose Infusion Time/);
|
|
assert.match(html, /Infusion Time/);
|
|
assert.match(html, /assistant-cite/);
|
|
});
|
|
|
|
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('strips orphan trailing markdown marker after tab-separated text', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const markdownIt = new MarkdownIt({ html: false, linkify: true, typographer: true, breaks: true });
|
|
const input = 'Summary Table\nStep\tRecommendation\nBlood culture\tObtain before antibiotics\nHospitalization\tYes, for all febrile neonates <=28 days old\n**';
|
|
const html = renderAssistantMarkdown(input, sources, { markdownIt });
|
|
assert.doesNotMatch(html, /<table>/);
|
|
assert.match(html, /Hospitalization/);
|
|
assert.doesNotMatch(html, /\*\*/);
|
|
});
|
|
|
|
test('keeps summary paragraph outside tab-separated text', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const markdownIt = new MarkdownIt({ html: false, linkify: true, typographer: true, breaks: true });
|
|
const input = 'Summary Table\nStep/Indication\tRecommendation/Threshold\tCitation\nInitial assessment\tABCs and focused exam\t[1]\nActivated charcoal window\tWithin 1 hour\t[2]\nIn summary: rapidly assess ABCs and consider activated charcoal when appropriate [1, 2].';
|
|
const html = renderAssistantMarkdown(input, sources, { markdownIt });
|
|
assert.doesNotMatch(html, /<table>/);
|
|
assert.match(html, /In summary:/);
|
|
assert.doesNotMatch(html, /<td>In summary:/);
|
|
});
|
|
|
|
test('strips trailing marker after notes followed by tab-separated text', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const markdownIt = new MarkdownIt({ html: false, linkify: true, typographer: true, breaks: true });
|
|
const input = 'Additional Notes\nListeria coverage: Ampicillin should be added for infants <3 months [1].\nVancomycin: Added for resistant pneumococcus [2].\nAge Group\tEmpiric Antibiotics\n<1 month\tAmpicillin + Cefotaxime\n1-23 months\tVancomycin + Cefotaxime or Ceftriaxone\n>=24 months\tVancomycin + Cefotaxime or Ceftriaxone\n**';
|
|
const html = renderAssistantMarkdown(input, sources, { markdownIt });
|
|
assert.match(html, /Additional Notes/);
|
|
assert.doesNotMatch(html, /<table>/);
|
|
assert.doesNotMatch(html, /\*\*/);
|
|
});
|
|
|
|
test('links citation-only cells in markdown pipe tables', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const markdownIt = new MarkdownIt({ html: false, linkify: true, typographer: true, breaks: true });
|
|
const input = '| Step | Purpose | Source(s) |\n|---|---|---|\n| Surgical consult | Urgent assessment | [1, 2] |\n| X-ray | Assess obstruction | [3] |';
|
|
const html = renderAssistantMarkdown(input, sources, { markdownIt });
|
|
assert.match(html, /<table>/);
|
|
assert.match(html, /data-source-number="1"/);
|
|
assert.match(html, /data-source-number="2"/);
|
|
assert.match(html, /data-source-number="3"/);
|
|
assert.doesNotMatch(html, /<td>\[1, 2\]<\/td>/);
|
|
});
|
|
|
|
test('repairs bare source numbers in markdown table source columns', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const markdownIt = new MarkdownIt({ html: false, linkify: true, typographer: true, breaks: true });
|
|
const input = '| Feature | Possible Surgical Cause(s) | Source(s) |\n|---|---|---|\n| Bilious vomiting | Malrotation with volvulus | 1, 2, 3 |\n| Projectile vomiting | Pyloric stenosis | 2 |';
|
|
const html = renderAssistantMarkdown(input, sources, { markdownIt });
|
|
assert.match(html, /<table>/);
|
|
assert.match(html, /data-source-number="1"/);
|
|
assert.match(html, /data-source-number="2"/);
|
|
assert.match(html, /data-source-number="3"/);
|
|
assert.doesNotMatch(html, /<td>1, 2, 3<\/td>/);
|
|
});
|
|
|
|
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 pipe 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\n\n| Situation | Imaging Recommended | Timing/Notes |\n|---|---|---|\n| First febrile UTI | Renal/bladder ultrasound | After recovery |\n| Routine use of DMSA scan | Not recommended | |\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('does not repair malformed tab-separated clinical summary rows into a table', 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.doesNotMatch(html, /<table>/);
|
|
assert.match(html, /Initial Imaging/);
|
|
assert.match(html, /If you need details/);
|
|
});
|
|
|
|
test('a streaming table renders block by block, not as a raw text dump', async () => {
|
|
// Previously a long or pipe-heavy answer gave up on markdown entirely and
|
|
// went into an unstyled <pre> — the dark flash while a table streamed. Now
|
|
// the finished blocks are markdown and only the unfinished tail is text.
|
|
// Behaviour lives in test/assistant-streaming-blocks.test.js; this guards
|
|
// against the old bail-out returning.
|
|
const source = await fs.readFile(path.join(__dirname, '..', 'public', 'js', 'clinicalAssistant.js'), 'utf8');
|
|
assert.doesNotMatch(source, /STREAM_MARKDOWN_LIMIT/);
|
|
assert.doesNotMatch(source, /pipeRows >= 8/);
|
|
assert.match(source, /function settledMarkdownLength\(text, from\)/);
|
|
assert.match(source, /renderStreamingInto\(bubble, partial, streamSources\)/);
|
|
});
|
|
|
|
test('mermaid source survives the sanitiser and round-trips', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const flowchart = 'graph TD; A[Start]-->B[Give O2];';
|
|
const html = renderAssistantMarkdown('```mermaid\n' + flowchart + '\n```', []);
|
|
const attr = html.match(/data-mermaid="([^"]*)"/);
|
|
assert.ok(attr, 'the placeholder must carry the diagram source');
|
|
// Encoded, because DOMPurify strips an attribute whose value contains "-->"
|
|
// and every flowchart has one, which left diagrams stuck on their placeholder.
|
|
assert.ok(!attr[1].includes('-->'), 'the stored value must not contain a raw arrow');
|
|
assert.equal(decodeURIComponent(attr[1].replace(/&/g, '&')).trim(), flowchart);
|
|
});
|
|
|
|
// ── Admin sources display ──────────────────────────────────────────────────
|
|
test('the prompt is identical whether or not sources are displayed', () => {
|
|
const { buildSystemPrompt } = require('../src/utils/clinicalAnswer');
|
|
// Branching the prompt on a display setting would change how the model
|
|
// reasons and cites, so the same question could get a different answer
|
|
// depending on what the admin chose to show. That is the bias this prevents.
|
|
const base = buildSystemPrompt('BEHAVIOR');
|
|
assert.equal(buildSystemPrompt('BEHAVIOR', { citations: false }), base, 'options cannot alter it');
|
|
assert.equal(buildSystemPrompt('BEHAVIOR', { showSources: false }), base);
|
|
assert.equal(buildSystemPrompt('BEHAVIOR', {}), base);
|
|
assert.match(base, /Cite factual claims immediately with numbered citations/,
|
|
'citations are always requested, so the stored answer keeps them');
|
|
});
|
|
|
|
test('hiding sources is display-only and reversible', () => {
|
|
const { stripCitationMarkers } = require('../src/utils/clinicalAnswer');
|
|
assert.equal(stripCitationMarkers('Amoxicillin 90 mg/kg/day [1][2]. Reassess in 48 h [3].'),
|
|
'Amoxicillin 90 mg/kg/day. Reassess in 48 h.');
|
|
assert.equal(stripCitationMarkers('Give fluids [1, 3] and rest [2].'), 'Give fluids and rest.');
|
|
assert.equal(stripCitationMarkers('No citations here.'), 'No citations here.');
|
|
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const route = fs.readFileSync(path.join(__dirname, '..', 'src/routes/clinicalAssistant.js'), 'utf8');
|
|
// Stripping happens on the way OUT, so the answer is generated and stored with
|
|
// citations intact and turning the setting back on restores them.
|
|
assert.match(route, /answer: prepared\.showSources \? answer : stripCitationMarkers\(answer\)/);
|
|
// The same sanitised list either way: showSources gates only what leaves the
|
|
// route, never what was retrieved. Both answer paths compute it, so match the
|
|
// guarantee rather than one spelling of it.
|
|
assert.match(route, /sources: prepared\.showSources \? (sanitizeSourcesForClient\(prepared\.sources\)|fallbackSources|safeSources) : \[\]/);
|
|
assert.match(route, /var fallbackSources = sanitizeSourcesForClient\(prepared\.sources\);/);
|
|
assert.match(route, /content: buildSystemPrompt\(behavior\)/, 'the prompt takes no display argument');
|
|
});
|
|
|
|
test('the sources toggle is a boolean the server enforces, with the legacy key honoured', () => {
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const root = path.join(__dirname, '..');
|
|
const admin = fs.readFileSync(path.join(root, 'src/routes/adminConfig.js'), 'utf8');
|
|
assert.match(admin, /clinical_assistant\.show_sources' && !\['true', 'false'\]/);
|
|
const route = fs.readFileSync(path.join(root, 'src/routes/clinicalAssistant.js'), 'utf8');
|
|
assert.match(route, /clinical_assistant\.citations_enabled', 'true'\)/, 'the old key still applies');
|
|
assert.match(fs.readFileSync(path.join(root, 'public/components/admin.html'), 'utf8'),
|
|
/id="assistant-show-sources"/);
|
|
});
|