pediatric-ai-scribe-v3/test/admin-clinical-assistant-wiring.test.js
Daniel b992c6600b feat: admin can turn citations off
New setting clinical_assistant.citations_enabled (default true, admin checkbox).
With it off, retrieval, grounding and every other rule are unchanged — answers
are still built only from retrieved sources — but:

- buildSystemPrompt swaps only the citation block: the "cite factual claims with
  [1]" rules are replaced with "do not include citations, source numbers or
  bracketed markers", and the note that the sourcing requirement itself is
  unchanged. Grounding, scope, table formatting and tone rules are byte-identical
  between the two modes.
- The server strips any stray [n] the model emits anyway, from the stored answer
  rather than only the view, so saved chats and exports match what was shown.
- No sources are sent to the client at all, and the status endpoint reports the
  mode so the UI hides the Sources panel and gives its 330px column back to the
  chat instead of showing an empty rail.

Validated as a boolean in adminConfig, like the feature.* keys.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkfrkQwA4YGrGw9LZSpeAq
2026-09-09 23:06:13 +02:00

127 lines
9.3 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { pathToFileURL } = require('node:url');
const { JSDOM } = require('jsdom');
const root = path.join(__dirname, '..');
const read = file => fs.readFileSync(path.join(root, file), 'utf8');
const tick = () => new Promise(resolve => setImmediate(resolve));
function browserGlobals(t, dom, fetch, toasts) {
const values = { window: dom.window, document: dom.window.document, fetch, getAuthHeaders: () => ({ 'X-Test': 'synthetic' }), showToast: (...args) => toasts.push(args) };
const originals = Object.keys(values).map(key => [key, Object.getOwnPropertyDescriptor(global, key)]);
Object.assign(global, values);
Object.assign(dom.window, { fetch, getAuthHeaders: values.getAuthHeaders });
t.after(() => {
for (const [key, descriptor] of originals) { if (descriptor) Object.defineProperty(global, key, descriptor); else delete global[key]; }
dom.window.close();
});
}
test('native admin initializer preserves lazy navigation, assistant actions and read-only ENV budget metadata', async t => {
const dom = new JSDOM('<button class="tab-btn active" data-tab="home">Home</button><button class="tab-btn" data-tab="admin">Admin</button><div id="home-tab" class="tab-content" data-component="home" data-loaded="1"></div><div id="admin-tab" class="tab-content" data-component="admin"></div>', { runScripts: 'outside-only', url: 'https://app.example' });
const calls = []; const toasts = [];
const fetch = async (url, options = {}) => {
calls.push({ url, options });
let data = {};
if (url.startsWith('/components/admin.html?')) return { ok: true, text: async () => read('public/components/admin.html') };
if (url === '/api/models') data = { models: [{ id: 'chat', name: 'Chat' }], defaultModel: 'chat' };
if (url === '/api/admin/config') {
assert.ok(document.getElementById('assistant-chat-model'), 'tabChanged fires after lazy markup exists');
data = { success: true, conversationBudget: { limit: 240000, unit: 'characters', measure: 'UTF-16 code units', env: 'CLINICAL_ASSISTANT_CONVERSATION_CHARS', source: 'environment' }, config: [{ key: 'clinical_assistant.conversation_chars', value: '999999' }, { key: 'clinical_assistant.image_model', value: 'saved-image' }] };
}
if (url === '/api/admin/config/image-models/discover') data = { models: [{ id: 'image', name: 'Image' }] };
if (url === '/api/admin/clinical-assistant/prompt-pool') data = { success: true, meta: { count: 3 }, snapshots: [{ id: 7, count: 3 }] };
if (options.method) data = { success: true, duration: 1, response: 'Synthetic', meta: { count: 4 }, snapshots: [{ id: 7, count: 4 }] };
return { ok: true, json: async () => data };
};
browserGlobals(t, dom, fetch, toasts);
await import(pathToFileURL(path.join(root, 'public/js/admin.js')).href);
assert.equal(calls.length, 0, 'registration must not eagerly load assistant settings');
// JSDOM does not execute native script tags. Import the unmodified ESM above,
// then execute the real classic app entrypoint and its component loader.
await tick();
dom.window.eval(read('public/js/app.js'));
document.dispatchEvent(new dom.window.Event('DOMContentLoaded'));
await tick();
assert.equal(document.getElementById('assistant-chat-model'), null);
document.querySelector('[data-tab="admin"]').click();
await tick(); await tick();
assert.equal(document.getElementById('admin-tab').dataset.loaded, '1');
assert.equal(document.getElementById('assistant-chat-model').options[0].textContent, 'Use global default (chat)');
assert.equal(document.getElementById('assistant-image-model'), null, 'single image-model dropdown lives in workflow-image-settings');
assert.match(document.getElementById('assistant-prompt-pool-status').textContent, /3 prompts/);
const budget = document.getElementById('assistant-conversation-budget');
assert.equal(document.querySelectorAll('#assistant-conversation-chars').length, 0);
assert.equal(budget.type, 'number', 'the conversation budget is an editable admin input');
assert.equal(budget.readOnly, false, 'the budget is admin-editable');
assert.equal(budget.value, '999999', 'prefilled with the saved override');
const budgetMeta = document.getElementById('assistant-conversation-budget-meta').textContent;
assert.match(budgetMeta, /CLINICAL_ASSISTANT_CONVERSATION_CHARS/);
assert.match(budgetMeta, /Clear this field/, 'the admin is told how to get back to the environment value');
assert.match(budgetMeta, /240,000/, 'and what that environment value currently is');
const initialConfigLoads = calls.filter(c => c.url === '/api/admin/config').length;
document.querySelector('[data-tab="home"]').click(); await tick();
document.querySelector('[data-tab="admin"]').click(); await tick();
assert.equal(calls.filter(c => c.url.startsWith('/components/admin.html?')).length, 1);
assert.equal(calls.filter(c => c.url === '/api/admin/config').length, initialConfigLoads, 'initializer loads once across revisits');
const writes = () => calls.filter(c => c.options.method === 'PUT');
const save = document.getElementById('btn-save-assistant-config');
save.click(); await tick();
assert.equal(writes().length, 8);
assert.deepEqual(writes().map(c => c.url.split('/').pop()).sort(), [
'clinical_assistant.allowed_image_models', 'clinical_assistant.allowed_models', 'clinical_assistant.chat_model', 'clinical_assistant.citations_enabled', 'clinical_assistant.context_chars', 'clinical_assistant.conversation_chars', 'clinical_assistant.search_limit', 'clinical_assistant.translate_provider'
]);
assert.ok(toasts.some(([message, kind]) => message === 'Assistant settings saved' && kind === 'success'));
document.getElementById('btn-test-assistant-chat-model').click(); await tick();
assert.deepEqual(JSON.parse(calls.find(c => c.url === '/api/admin/config/models/test').options.body), { modelId: 'chat' });
document.getElementById('btn-regenerate-assistant-prompt-pool').click(); await tick();
assert.equal(document.getElementById('btn-regenerate-assistant-prompt-pool').disabled, false);
document.getElementById('assistant-prompt-pool-snapshots').value = '7';
document.getElementById('btn-restore-assistant-prompt-pool').click(); await tick();
assert.deepEqual(JSON.parse(calls.find(c => c.url.endsWith('/prompt-pool/restore')).options.body), { id: 7 });
assert.equal(document.getElementById('assistant-custom-image-model'), null);
assert.equal(document.getElementById('btn-test-assistant-image-model'), null);
assert.equal(document.getElementById('btn-use-custom-assistant-image-model'), null);
await tick();
});
test('real extracted initializer never invents a cap when metadata is missing, invalid or returns 503', async t => {
const { initClinicalAssistantAdmin } = await import(pathToFileURL(path.join(root, 'public/js/admin/clinicalAssistant.js')).href);
for (const data of [{}, { success: false, error: 'Invalid environment' }, { success: true, conversationBudget: { limit: 1000001 } }]) {
await t.test(JSON.stringify(data), async t => {
const dom = new JSDOM(read('public/components/admin.html'));
browserGlobals(t, dom, async () => ({ status: 503, json: async () => data }), []);
initClinicalAssistantAdmin(value => value);
document.dispatchEvent(new dom.window.CustomEvent('tabChanged', { detail: { tab: 'admin' } }));
await tick();
assert.equal(document.getElementById('assistant-conversation-budget').value, '', 'failed load leaves the budget input empty');
// The failure notice must land on the <p>; writing it to the <input>
// rendered nothing, so admins saw a stale message on a broken load.
assert.match(document.getElementById('assistant-conversation-budget-meta').textContent, /Conversation budget unavailable/);
assert.doesNotMatch(document.getElementById('assistant-conversation-budget-meta').textContent, /Leave empty/, 'no misleading guidance when the load failed');
});
}
});
test('real extracted initializer displays the server default only when returned as metadata', async t => {
const dom = new JSDOM(read('public/components/admin.html'));
browserGlobals(t, dom, async () => ({ ok: true, json: async () => ({ success: true, config: [], models: [], conversationBudget: {
limit: 120000, unit: 'characters', measure: 'UTF-16 code units', env: 'CLINICAL_ASSISTANT_CONVERSATION_CHARS', source: 'default'
} }) }), []);
const { initClinicalAssistantAdmin } = await import(pathToFileURL(path.join(root, 'public/js/admin/clinicalAssistant.js')).href);
initClinicalAssistantAdmin(value => value);
document.dispatchEvent(new dom.window.CustomEvent('tabChanged', { detail: { tab: 'admin' } }));
await tick();
// No saved override: the field stays EMPTY so Save cannot silently promote the
// environment value into a stored override. The number is shown as a placeholder.
const noOverride = document.getElementById('assistant-conversation-budget');
assert.equal(noOverride.value, '', 'no override means an empty field, not a prefilled one');
assert.equal(noOverride.placeholder, '120000', 'the effective limit is shown as a placeholder');
assert.equal(noOverride.readOnly, false);
assert.match(document.getElementById('assistant-conversation-budget-meta').textContent, /built-in default/,
"source 'default' must not be reported as coming from the environment variable");
});