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').value, 'saved-image'); assert.equal(window._assistantImageModelValue, 'saved-image'); 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.image_model', 'clinical_assistant.search_limit' ]); 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 }); document.getElementById('assistant-custom-image-model').value = '\">