""" Regression coverage for model selector drift on fresh browser boot. A stale browser-persisted model (localStorage) must not suppress the configured profile/server default on page load. Restored sessions may still apply their own session model later through loadSession(). The boot fix must not make browser model-state writes pointless or let later model-list refreshes reset a live in-page selection. """ import json import shutil import subprocess from pathlib import Path import pytest REPO = Path(__file__).resolve().parents[1] BOOT_JS = (REPO / "static" / "boot.js").read_text(encoding="utf-8") UI_JS = (REPO / "static" / "ui.js").read_text(encoding="utf-8") NODE = shutil.which("node") _DRIVER_SRC = r""" const fs = require('fs'); const ui = fs.readFileSync(process.argv[2], 'utf8'); function extractFunc(name) { const re = new RegExp('(?:async\\s+)?function\\s+' + name + '\\s*\\('); const start = ui.search(re); if (start < 0) throw new Error(name + ' not found'); let openParen = ui.indexOf('(', start); let i = openParen + 1; let parenDepth = 1; while (parenDepth > 0 && i < ui.length) { if (ui[i] === '(') parenDepth++; else if (ui[i] === ')') parenDepth--; i++; } i = ui.indexOf('{', i); let depth = 1; i++; while (depth > 0 && i < ui.length) { if (ui[i] === '{') depth++; else if (ui[i] === '}') depth--; i++; } return ui.slice(start, i); } const calls = {syncModelChip: 0, renderModelDropdown: 0, positionModelDropdown: 0, liveFetches: []}; let modelSelect; let apiModels; function makeSelect(options, initialValue) { const sel = {id: 'modelSelect', options: [], selectedIndex: -1, selectedOptions: []}; Object.defineProperty(sel, 'value', { get() { return this._value || ''; }, set(v) { this._value = v; const idx = this.options.findIndex(o => o.value === v); this.selectedIndex = idx; this.selectedOptions = idx >= 0 ? [this.options[idx]] : []; } }); Object.defineProperty(sel, 'innerHTML', { get() { return ''; }, set(_v) { this.options = []; this.value = ''; } }); sel.querySelector = function(_selector) { return this.options[0] || null; }; sel.appendChild = function(node) { const incoming = node && node.tagName === 'OPTGROUP' ? node.children : [node]; for (const opt of incoming || []) { opt.parentElement = opt.parentElement || node; this.options.push(opt); if (this.selectedIndex < 0) this.value = opt.value; else if (this._value === opt.value) this.value = opt.value; } }; for (const item of options || []) { const group = {tagName: 'OPTGROUP', dataset: {provider: item.provider || ''}, children: []}; const opt = {tagName: 'OPTION', value: item.value, textContent: item.label || item.value, title: '', dataset: {}, parentElement: group}; group.children.push(opt); sel.appendChild(group); } sel.value = initialValue || ''; return sel; } function $(id) { if (id === 'modelSelect') return modelSelect; if (id === 'composerModelDropdown') return {classList: {contains(){ return false; }}}; return null; } function t(key) { return key; } function getModelLabel(v) { return v; } function syncModelChip() { calls.syncModelChip++; } function renderModelDropdown() { calls.renderModelDropdown++; } function _positionModelDropdown() { calls.positionModelDropdown++; } function _redirectIfUnauth() { return false; } function _fetchLiveModels(provider, _sel) { calls.liveFetches.push(provider); } const document = { baseURI: 'http://127.0.0.1/hermes/', createElement(tag) { const upper = tag.toUpperCase(); if (upper === 'OPTGROUP') { return { tagName: 'OPTGROUP', label: '', dataset: {}, children: [], appendChild(opt) { opt.parentElement = this; this.children.push(opt); }, }; } return {tagName: upper, value: '', textContent: '', title: '', dataset: {}, parentElement: null}; }, }; const localStorage = {getItem(){return null;}, setItem(){}, removeItem(){}}; const window = {_defaultModel: null, _activeProvider: null, _configuredModelBadges: {}}; let _dynamicModelLabels = {}; let _liveModelFetchPending = new Set(); let _liveModelCache = {}; for (const name of [ '_getOptionProviderId', '_providerFromModelValue', '_modelStateForSelect', '_captureModelDropdownSelection', '_findModelInDropdown', '_refreshOpenModelDropdown', '_applyModelToDropdown', '_reconcileModelDropdownSelection', 'populateModelDropdown' ]) { eval(extractFunc(name)); } const args = JSON.parse(process.argv[3]); apiModels = args.apiModels; modelSelect = makeSelect(args.initialOptions, args.initialValue); var S = {session: args.session || null}; fetch = async function(url) { const href = String(url); if (href.includes('api/models')) { return {json: async () => apiModels}; } throw new Error('unexpected fetch ' + href); }; populateModelDropdown(args.opts || {}).then(() => { process.stdout.write(JSON.stringify({ selectValue: modelSelect.value, selectedProvider: modelSelect.selectedOptions[0] ? _getOptionProviderId(modelSelect.selectedOptions[0]) : null, defaultModel: window._defaultModel, activeProvider: window._activeProvider, calls, })); }).catch(err => { console.error(err && err.stack || err); process.exit(1); }); """ @pytest.fixture(scope="module") def populate_driver_path(tmp_path_factory): p = tmp_path_factory.mktemp("model_default_driver") / "driver.js" p.write_text(_DRIVER_SRC, encoding="utf-8") return str(p) def test_boot_settings_applies_default_without_deleting_browser_model_state(): snippet = _boot_default_apply_snippet() assert "Fresh page boot must prefer the profile/server default" in snippet assert "if(sel&&typeof _applyModelToDropdown==='function')" in snippet assert "if(sel&&!savedState&&typeof _applyModelToDropdown==='function')" not in BOOT_JS assert "_clearPersistedModelState" not in snippet assert "localStorage.removeItem('hermes-webui-model')" not in snippet assert "localStorage.removeItem('hermes-webui-model-state')" not in snippet def test_boot_model_dropdown_explicitly_requests_profile_default_precedence(): assert "populateModelDropdown({preferProfileDefaultOnFreshBoot:true})" in BOOT_JS # #2726 invariant: boot path must keep profile/server default ahead of stale # browser-persisted state when a default exists. Post-#2716 cherry-pick onto # post-stage-batch11 master uses `stateToApply` pattern rather than the # original `allowBootSavedModelOverride` variable name. Semantics preserved — # check for the actual gate predicate that's equivalent. assert ( "const allowBootSavedModelOverride=!window._defaultModel" in BOOT_JS or "!window._defaultModel?savedState:null" in BOOT_JS ), "boot.js missing the !window._defaultModel gate for saved-state override" def test_populate_model_dropdown_reconciles_selection_after_rebuild(): assert "const previousSelection=_captureModelDropdownSelection(sel);" in UI_JS assert "_reconcileModelDropdownSelection(sel,data,previousSelection,opts);" in UI_JS snippet = _reconcile_selection_snippet() assert "preferProfileDefaultOnFreshBoot" in snippet # #4363: each branch now routes through _applyOrEnsure, which delegates to # _ensureModelOptionInDropdown so a cross-provider model missing from a # partially-rebuilt catalog is injected as a custom option instead of the # browser silently snapping the