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('
', { 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.match(budget.textContent, /240,000 characters \(UTF-16 code units\).*CLINICAL_ASSISTANT_CONVERSATION_CHARS \(environment\)/); assert.doesNotMatch(budget.textContent, /999/); 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, 4); assert.deepEqual(writes().map(c => c.url.split('/').pop()).sort(), [ 'clinical_assistant.chat_model', 'clinical_assistant.context_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.match(document.getElementById('assistant-conversation-budget').textContent, /unavailable/); assert.doesNotMatch(document.getElementById('assistant-conversation-budget').textContent, /120,?000|1,?000,?001/); }); } }); 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(); assert.match(document.getElementById('assistant-conversation-budget').textContent, /120,000 characters \(UTF-16 code units\).*server default; environment unset/); });