Sources (correcting what I built earlier) The previous toggle branched the SYSTEM PROMPT, so the same question could get a different answer depending on a display setting — the bias this was meant to avoid. The prompt is now unconditional: buildSystemPrompt takes no display argument and is byte-identical either way. Hiding sources happens on the way out — the server omits them and strips the now-orphaned [n] markers from the copy it sends. The answer is generated, stored and exported with citations intact, so turning the setting back on restores them without re-asking anything. Renamed to clinical_assistant.show_sources; the old key is still honoured. Signed-out preview (admin opt-in, default off) A visitor may try the assistant; reaching for the workspace asks them to sign in. Deliberately narrow: - Reachable paths are an exact allow-list, not a pattern, so a new endpoint is private unless someone adds it on purpose. - A preview visitor gets no identity at all (id: null), so nothing can be owned, saved, billed or addressed to them. - The image tool is withheld rather than left to fail on a null owner, and no audit rows are written. - A caller presenting a token is authenticated normally, so preview can never downgrade a real session; if the setting cannot be read, authentication is required. - Actions needing an account are hidden rather than offered and refused. Composer The bar above the transcript is gone. Patient take home, Export PDF, Download transcript and Attach images moved into a + menu in the composer, and the model selector moved beside send — shown only when there is more than one model, as before. Both views now start at the same top edge, so switching modes cannot nudge the page up or down. On an empty transcript the tiled ground runs behind and below the composer, which floats on it above centre. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GmpYHPSLGmXGZMyLpn2Lbe
337 lines
20 KiB
JavaScript
337 lines
20 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('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"[^>]*>src<\/a> <a class="assistant-cite"[^>]*>src<\/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"[^>]*>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('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"[^>]*>src<\/a> <a class="assistant-cite"[^>]*>src<\/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"[^>]*>src<\/a> and dehydration <a class="assistant-cite"[^>]*>src<\/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('clinical assistant streams long table answers as lightweight text before final render', async () => {
|
|
const source = await fs.readFile(path.join(__dirname, '..', 'public', 'js', 'clinicalAssistant.js'), 'utf8');
|
|
assert.match(source, /STREAM_MARKDOWN_LIMIT/);
|
|
assert.match(source, /renderStreamingAnswerHtml\(partial, streamSources\)/);
|
|
assert.match(source, /assistant-streaming-text/);
|
|
assert.match(source, /pipeRows >= 8/);
|
|
});
|
|
|
|
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\)/);
|
|
assert.match(route, /sources: prepared\.showSources \? 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"/);
|
|
});
|