Files
hermes-webui/tests/test_new_chat_default_model_frontend.py
T
nesquena-hermesandRod Boev 57a78e9284 fix(#4728): clear stale custom:* base_url + default wins without loaded session (gate findings)
Two gate-found CORE/SILENT edges fixed on-branch:
- api/config.py: switching the main default to a different custom:* provider kept
  the OLD provider's base_url, routing New Chat to the wrong endpoint. The
  base_url-clear on provider-change now covers custom:* too (was exempted).
- static/sessions.js: newSession() skipped the configured default when S.session
  was null but the picker held stale state (e.g. after deleting the last session),
  inheriting the stale model. Default now wins over legacy picker regardless of
  loaded-session state (explicit empty-composer override still takes priority).
- Updated 2 stale source-grep precedence assertions + widened a brittle 6000-char
  newSession() slice window to 7000 (assignment block grew; intent unchanged).
- 2 new non-vacuous regression tests (custom base_url clear; precedence).

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-24 20:17:29 +00:00

180 lines
10 KiB
Python

from pathlib import Path
SESSIONS_JS = Path("static/sessions.js").read_text(encoding="utf-8")
MESSAGES_JS = Path("static/messages.js").read_text(encoding="utf-8")
CHANGELOG = Path("CHANGELOG.md").read_text(encoding="utf-8")
def _extract_function(source: str, signature: str) -> str:
start = source.index(signature)
# Look for the function body's opening brace, not an object literal inside
# a default argument such as `options={}`.
brace = source.index("{\n", start)
depth = 0
for idx in range(brace, len(source)):
ch = source[idx]
if ch == "{":
depth += 1
elif ch == "}":
depth -= 1
if depth == 0:
return source[start : idx + 1]
raise AssertionError(f"Function body not closed for {signature}")
def _new_session_function() -> str:
return _extract_function(SESSIONS_JS, "async function newSession")
def test_new_chat_syncs_model_picker_when_default_provider_changes_but_model_id_matches():
fn = _new_session_function()
assert "currentModelState" in fn
assert "currentProvider" in fn
assert "sessionProvider" in fn
assert "sessionProvider !== currentProvider" in fn
assert "_applyModelToDropdown(S.session.model,modelSel,sessionProvider)" in fn
def test_new_chat_inserts_session_model_when_static_picker_lacks_default():
fn = _new_session_function()
assert "sessionModelApplied" in fn
assert "document.createElement('option')" in fn
assert "opt.value=S.session.model" in fn
assert "opt.dataset.provider=sessionProvider||''" in fn
assert "modelSel.appendChild(opt)" in fn
def test_boot_model_hydration_prefers_active_session_over_persisted_model():
boot_js = Path("static/boot.js").read_text(encoding="utf-8")
marker = "const sessionModelState=S.session&&S.session.model"
assert marker in boot_js
session_branch = boot_js[boot_js.index(marker) : boot_js.index("if(S.session) syncTopbar();", boot_js.index(marker))]
assert "_applyModelToDropdown(sessionModelState.model,$('modelSelect'),sessionModelState.model_provider||null)" in session_branch
assert "savedState" in session_branch
assert session_branch.index("sessionModelState") < session_branch.index("savedState"), (
"active session model must be considered before localStorage so stale saved model preferences cannot override new chats"
)
def test_hard_refresh_hydrates_saved_session_model_before_revealing_model_chip():
boot_js = Path("static/boot.js").read_text(encoding="utf-8")
load_marker = "await loadSession(saved);"
assert load_marker in boot_js
saved_restore = boot_js[boot_js.index(load_marker) : boot_js.index("await checkInflightOnBoot(saved);return;", boot_js.index(load_marker))]
assert "await _startBootModelDropdown();" in saved_restore
assert saved_restore.index("await _startBootModelDropdown();") > saved_restore.index(load_marker)
assert saved_restore.index("await _startBootModelDropdown();") < saved_restore.index("S._bootReady=true;"), (
"hard refresh must hydrate/re-apply the active session model before S._bootReady lets syncModelChip display stale static HTML defaults"
)
def test_hard_refresh_injects_missing_active_session_model_option():
boot_js = Path("static/boot.js").read_text(encoding="utf-8")
marker = "if(!applied&&sessionModelState&&typeof _ensureModelOptionInDropdown==='function')"
assert marker in boot_js
branch = boot_js[boot_js.index(marker) : boot_js.index("else if(!applied&&!sessionModelState", boot_js.index(marker))]
assert "_ensureModelOptionInDropdown(sessionModelState.model,$('modelSelect'),sessionModelState.model_provider||null)" in branch
def test_sync_topbar_preserves_missing_session_model_as_dropdown_option():
ui_js = Path("static/ui.js").read_text(encoding="utf-8")
assert "function _ensureModelOptionInDropdown" in ui_js
sync_topbar = _extract_function(ui_js, "function syncTopbar")
branch_start = sync_topbar.index("const applied=_applyModelToDropdown(currentModel,modelSel,S.session.model_provider||null);")
session_model_branch = sync_topbar[branch_start:]
assert "_ensureModelOptionInDropdown(currentModel,modelSel,S.session.model_provider||null)" in session_model_branch
assert "const fallback=_applySessionModelFallback(modelSel);" in session_model_branch
assert session_model_branch.index("_ensureModelOptionInDropdown(currentModel,modelSel,S.session.model_provider||null)") < session_model_branch.index("const fallback=_applySessionModelFallback(modelSel);"), (
"active session models missing from the current catalog must be injected before fallback can select the static/default model"
)
def test_new_chat_does_not_send_stale_dropdown_model_when_session_has_default_model():
assert "model:S.session.model||$('modelSelect').value" in MESSAGES_JS
assert "model_provider:S.session.model_provider||null" in MESSAGES_JS
def test_new_session_posts_picker_model_before_server_default():
fn = _new_session_function()
assert "reqBody.model=newModelState.model" in fn
assert "explicitModelOverride" in fn
assert "}else if(window._defaultModel){" in fn
assert "modelSelForNew&&modelSelForNew.value&&typeof _modelStateForSelect==='function'" in fn
provider_assignment = fn[fn.index("reqBody.model_provider="):].split(";", 1)[0]
assert "newModelState.model_provider" in provider_assignment
assert "_fallbackProvider" in provider_assignment
assert "window._activeProvider" in fn
assert "S.session&&S.session.model_provider" in fn
pos_override = fn.index("explicitModelOverride")
pos_default = fn.index("}else if(window._defaultModel){")
pos_legacy = fn.index("modelSelForNew&&modelSelForNew.value&&typeof _modelStateForSelect==='function'")
assert pos_override < pos_default < pos_legacy, (
"newSession() must prefer the empty-composer override first, then the configured default, then legacy picker state"
)
assert "_familyMismatch" in fn
assert "_readPersistedModelState" in fn
def test_model_picker_persists_without_active_session():
boot_js = Path("static/boot.js").read_text(encoding="utf-8")
body = boot_js[boot_js.index("$('modelSelect').onchange=async()=>") : boot_js.index("$('msg').addEventListener", boot_js.index("$('modelSelect').onchange=async()=>"))]
assert "_writePersistedModelState(modelState.model,modelState.model_provider)" in body
assert "_rememberEmptyComposerModelOverride(modelState.model,modelState.model_provider)" in body
assert "if(!S.session){" in body
assert body.index("if(!S.session){") < body.index("await api('/api/session/update'")
def test_session_model_changes_do_not_write_empty_composer_override():
boot_js = Path("static/boot.js").read_text(encoding="utf-8")
body = boot_js[boot_js.index("$('modelSelect').onchange=async()=>") : boot_js.index("$('msg').addEventListener", boot_js.index("$('modelSelect').onchange=async()=>"))]
session_branch = body[body.index("if(typeof _rememberPendingSessionModel==='function')"):]
assert "_rememberPendingSessionModel(S.session.session_id,modelState.model,modelState.model_provider)" in body
assert "_rememberEmptyComposerModelOverride(modelState.model,modelState.model_provider)" not in session_branch
def test_new_chat_prefers_explicit_empty_composer_override_before_configured_default():
fn = _new_session_function()
assert "explicitModelOverride" in fn
assert "hasLoadedSession" in fn
assert "consumedExplicitModelOverride" in fn
assert "usingConfiguredDefault" in fn
assert "_clearEmptyComposerModelOverride" in fn
assert "newModelState={model:window._defaultModel,model_provider:null};" in fn
assert fn.index("explicitModelOverride") < fn.index("}else if(window._defaultModel){") < fn.index("_modelStateForSelect"), (
"newSession() must prefer the empty-composer override first, then the configured default, then legacy picker state"
)
def test_new_session_keeps_provider_fallback_guards_after_model_precedence():
fn = _new_session_function()
provider_assignment = fn[fn.index("reqBody.model_provider="):].split(";", 1)[0]
assert "newModelState.model_provider" in provider_assignment
assert "_fallbackProvider" in provider_assignment
assert "_familyMismatch" in provider_assignment
assert "_fallbackIsNamedCustom" in provider_assignment
assert "usingConfiguredDefault?window._activeProvider" in fn
def test_save_settings_syncs_default_model_provider_with_saved_model():
panels_js = Path("static/panels.js").read_text(encoding="utf-8")
save_block = _extract_function(panels_js, "async function saveSettings")
apply_saved_block = _extract_function(panels_js, "function _applySavedSettingsUi")
autosave_block = panels_js[panels_js.index("const pwField=$('settingsPassword');"):panels_js.index("if(!pwDirty&&!modelDirty){", panels_js.index("const pwField=$('settingsPassword');")) + 24]
assert "_captureModelDropdownSelection($('settingsModel'))" in save_block
assert "JSON.stringify({model,provider:modelState.model_provider||null})" in save_block
assert "body.default_model_provider=(modelState&&modelState.model===model)?(modelState.model_provider||null):null;" in save_block
assert "const modelChanged=(model||'')!==(_settingsHermesDefaultModelOnOpen||'')||((modelState.model_provider||null)!==(_settingsHermesDefaultModelProviderOnOpen||null));" in save_block
assert "if(Object.prototype.hasOwnProperty.call(body,'default_model_provider')) window._activeProvider=body.default_model_provider||null;" in apply_saved_block
assert "_settingsHermesDefaultModelProviderOnOpen=(models&&models.active_provider)||null;" in panels_js
assert "if(Object.prototype.hasOwnProperty.call(body,'default_model_provider')) _settingsHermesDefaultModelProviderOnOpen=body.default_model_provider||null;" in apply_saved_block
assert "(modelState.model_provider||null)!==(_settingsHermesDefaultModelProviderOnOpen||null)" in autosave_block
assert "_captureModelDropdownSelection(modelSel)||{model:String((modelSel&&modelSel.value)||''),model_provider:null}" in panels_js
assert "_captureModelDropdownSelection($('settingsModel'))||{model:String(model||''),model_provider:null}" in save_block
def test_changelog_mentions_new_chat_default_model_provider_sync():
unreleased = CHANGELOG.split("## [v0.51.103]", 1)[0]
assert "New conversations now resync" in unreleased
assert "default model provider" in unreleased